feat(i18n): Implement multilingual support with Polish and English translations

- Added translation context and provider for managing language state.
- Integrated translation functionality into existing components (TaskStatusDropdown, Navigation).
- Created LanguageSwitcher component for language selection.
- Updated task statuses and navigation labels to use translations.
- Added Polish translations for various UI elements, including navigation, tasks, projects, and contracts.
- Refactored utility functions to return localized strings for deadlines and date formatting.
This commit is contained in:
Chop
2025-07-27 22:01:15 +02:00
parent 9b6307eabe
commit e828aa660b
16 changed files with 1166 additions and 234 deletions

View File

@@ -12,8 +12,10 @@ import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader"; import PageHeader from "@/components/ui/PageHeader";
import { LoadingState } from "@/components/ui/States"; import { LoadingState } from "@/components/ui/States";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n";
export default function UserManagementPage() { export default function UserManagementPage() {
const { t } = useTranslation();
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -54,7 +56,7 @@ export default function UserManagementPage() {
}; };
const handleDeleteUser = async (userId) => { const handleDeleteUser = async (userId) => {
if (!confirm("Are you sure you want to delete this user?")) return; if (!confirm(t('admin.deleteUser') + "?")) return;
try { try {
const response = await fetch(`/api/admin/users/${userId}`, { const response = await fetch(`/api/admin/users/${userId}`, {
@@ -141,7 +143,7 @@ export default function UserManagementPage() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader title="User Management" description="Manage system users and permissions"> <PageHeader title={t('admin.userManagement')} description={t('admin.subtitle')}>
<Button <Button
variant="primary" variant="primary"
onClick={() => setShowCreateForm(true)} onClick={() => setShowCreateForm(true)}
@@ -149,7 +151,7 @@ export default function UserManagementPage() {
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg> </svg>
Add User {t('admin.newUser')}
</Button> </Button>
</PageHeader> </PageHeader>

View File

@@ -11,8 +11,10 @@ import SearchBar from "@/components/ui/SearchBar";
import FilterBar from "@/components/ui/FilterBar"; import FilterBar from "@/components/ui/FilterBar";
import { LoadingState } from "@/components/ui/States"; import { LoadingState } from "@/components/ui/States";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n";
export default function ContractsMainPage() { export default function ContractsMainPage() {
const { t } = useTranslation();
const [contracts, setContracts] = useState([]); const [contracts, setContracts] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@@ -133,13 +135,13 @@ export default function ContractsMainPage() {
const getStatusBadge = (status) => { const getStatusBadge = (status) => {
switch (status) { switch (status) {
case "active": case "active":
return <Badge variant="success">Aktywna</Badge>; return <Badge variant="success">{t('contracts.active')}</Badge>;
case "completed": case "completed":
return <Badge variant="secondary">Zakończona</Badge>; return <Badge variant="secondary">{t('common.completed')}</Badge>;
case "ongoing": case "ongoing":
return <Badge variant="primary">W trakcie</Badge>; return <Badge variant="primary">{t('contracts.withoutEndDate')}</Badge>;
default: default:
return <Badge>Nieznany</Badge>; return <Badge>{t('common.unknown')}</Badge>;
} }
}; };
@@ -170,17 +172,17 @@ export default function ContractsMainPage() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title="Umowy" title={t('contracts.title')}
description="Zarządzaj swoimi umowami i kontraktami" description={t('contracts.subtitle')}
> >
<Link href="/contracts/new"> <Link href="/contracts/new">
<Button variant="primary" size="lg"> <Button variant="primary" size="lg">
<span className="mr-2"></span> <span className="mr-2"></span>
Nowa umowa {t('contracts.newContract')}
</Button> </Button>
</Link> </Link>
</PageHeader> </PageHeader>
<LoadingState message="Ładowanie umów..." /> <LoadingState message={t('navigation.loading')} />
</PageContainer> </PageContainer>
); );
} }
@@ -225,13 +227,13 @@ export default function ContractsMainPage() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title="Umowy" title={t('contracts.title')}
description="Zarządzaj swoimi umowami i kontraktami" description={t('contracts.subtitle')}
> >
<Link href="/contracts/new"> <Link href="/contracts/new">
<Button variant="primary" size="lg"> <Button variant="primary" size="lg">
<span className="mr-2"></span> <span className="mr-2"></span>
Nowa umowa {t('contracts.newContract')}
</Button> </Button>
</Link>{" "} </Link>{" "}
</PageHeader> </PageHeader>

View File

@@ -2,6 +2,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Navigation from "@/components/ui/Navigation"; import Navigation from "@/components/ui/Navigation";
import { AuthProvider } from "@/components/auth/AuthProvider"; import { AuthProvider } from "@/components/auth/AuthProvider";
import { TranslationProvider } from "@/lib/i18n";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -14,20 +15,22 @@ const geistMono = Geist_Mono({
}); });
export const metadata = { export const metadata = {
title: "Project Management Panel", title: "Panel Zarządzania Projektami",
description: "Professional project management dashboard", description: "Profesjonalny panel zarządzania projektami",
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="pl">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
<AuthProvider> <TranslationProvider initialLanguage="pl">
<Navigation /> <AuthProvider>
<main>{children}</main> <Navigation />
</AuthProvider> <main>{children}</main>
</AuthProvider>
</TranslationProvider>
</body> </body>
</html> </html>
); );

View File

@@ -10,6 +10,7 @@ import TaskStatusDropdownSimple from "@/components/TaskStatusDropdownSimple";
import PageContainer from "@/components/ui/PageContainer"; import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader"; import PageHeader from "@/components/ui/PageHeader";
import { LoadingState } from "@/components/ui/States"; import { LoadingState } from "@/components/ui/States";
import { useTranslation } from "@/lib/i18n";
import { import {
differenceInCalendarDays, differenceInCalendarDays,
parseISO, parseISO,
@@ -26,6 +27,7 @@ import TaskStatusChart from "@/components/ui/TaskStatusChart";
export default function Home() { export default function Home() {
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const { t } = useTranslation();
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalProjects: 0, totalProjects: 0,
activeProjects: 0, activeProjects: 0,
@@ -258,17 +260,17 @@ export default function Home() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title="Dashboard" title={t('dashboard.title')}
description="Overview of your projects, contracts, and tasks" description={t('dashboard.subtitle')}
/> />
<LoadingState message="Loading dashboard data..." /> <LoadingState message={t('common.loading')} />
</PageContainer> </PageContainer>
); );
} }
// Show loading state while session is being fetched // Show loading state while session is being fetched
if (status === "loading") { if (status === "loading") {
return <LoadingState message="Loading authentication..." />; return <LoadingState message={t('navigation.loading')} />;
} }
// Show sign-in prompt if not authenticated // Show sign-in prompt if not authenticated
@@ -277,16 +279,16 @@ export default function Home() {
<PageContainer> <PageContainer>
<div className="text-center py-12"> <div className="text-center py-12">
<h1 className="text-4xl font-bold text-gray-900 mb-6"> <h1 className="text-4xl font-bold text-gray-900 mb-6">
Welcome to Project Management Panel Witamy w Panelu Zarządzania Projektami
</h1> </h1>
<p className="text-xl text-gray-600 mb-8"> <p className="text-xl text-gray-600 mb-8">
Please sign in to access the project management system. Zaloguj się, aby uzyskać dostęp do systemu zarządzania projektami.
</p> </p>
<Link <Link
href="/auth/signin" href="/auth/signin"
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700" className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
> >
Sign In {t('auth.signIn')}
</Link> </Link>
</div> </div>
</PageContainer> </PageContainer>
@@ -296,8 +298,8 @@ export default function Home() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title={`Welcome back, ${session.user.name}!`} title={`Witaj ponownie, ${session.user.name}!`}
description="Overview of your projects, contracts, and tasks" description={t('dashboard.subtitle')}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/projects/new"> <Link href="/projects/new">
@@ -315,7 +317,7 @@ export default function Home() {
d="M12 4v16m8-8H4" d="M12 4v16m8-8H4"
/> />
</svg> </svg>
New Project {t('projects.newProject')}
</Button> </Button>
</Link> </Link>
<Link href="/contracts/new"> <Link href="/contracts/new">
@@ -333,7 +335,7 @@ export default function Home() {
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/> />
</svg> </svg>
New Contract {t('contracts.newContract')}
</Button> </Button>
</Link> </Link>
</div> </div>
@@ -347,13 +349,13 @@ export default function Home() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-gray-600 uppercase tracking-wide"> <p className="text-xs font-medium text-gray-600 uppercase tracking-wide">
Total Projects {t('dashboard.totalProjects')}
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.totalProjects} {stats.totalProjects}
</p> </p>
<p className="text-xs text-green-600 mt-1"> <p className="text-xs text-green-600 mt-1">
{stats.activeProjects} active {stats.activeProjects} {t('dashboard.activeProjects').toLowerCase()}
</p> </p>
</div> </div>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
@@ -380,13 +382,13 @@ export default function Home() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-gray-600 uppercase tracking-wide"> <p className="text-xs font-medium text-gray-600 uppercase tracking-wide">
Contracts {t('dashboard.totalContracts')}
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.totalContracts} {stats.totalContracts}
</p> </p>
<p className="text-xs text-green-600 mt-1"> <p className="text-xs text-green-600 mt-1">
{stats.activeContracts} active {stats.activeContracts} {t('dashboard.activeContracts').toLowerCase()}
</p> </p>
</div> </div>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
@@ -413,12 +415,12 @@ export default function Home() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-gray-600 uppercase tracking-wide"> <p className="text-xs font-medium text-gray-600 uppercase tracking-wide">
Pending Tasks {t('dashboard.pendingTasks')}
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.pendingTasks} {stats.pendingTasks}
</p> </p>
<p className="text-xs text-gray-500 mt-1">Awaiting start</p> <p className="text-xs text-gray-500 mt-1">Oczekują rozpoczęcia</p>
</div> </div>
<div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-yellow-100 rounded-lg flex items-center justify-center">
<svg <svg
@@ -444,12 +446,12 @@ export default function Home() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-gray-600 uppercase tracking-wide"> <p className="text-xs font-medium text-gray-600 uppercase tracking-wide">
In Progress {t('dashboard.inProgressTasks')}
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.inProgressTasks} {stats.inProgressTasks}
</p> </p>
<p className="text-xs text-blue-600 mt-1">Active tasks</p> <p className="text-xs text-blue-600 mt-1">Aktywne zadania</p>
</div> </div>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg <svg
@@ -475,12 +477,12 @@ export default function Home() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-gray-600 uppercase tracking-wide"> <p className="text-xs font-medium text-gray-600 uppercase tracking-wide">
Completed {t('dashboard.completedTasks')}
</p> </p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{stats.completedTasks} {stats.completedTasks}
</p> </p>
<p className="text-xs text-green-600 mt-1">Tasks done</p> <p className="text-xs text-green-600 mt-1">Zadania wykonane</p>
</div> </div>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg <svg

View File

@@ -25,9 +25,9 @@ export default async function ProjectViewPage({ params }) {
<PageContainer> <PageContainer>
<Card> <Card>
<CardContent className="text-center py-8"> <CardContent className="text-center py-8">
<p className="text-red-600 text-lg">Project not found.</p> <p className="text-red-600 text-lg">Projekt nie został znaleziony.</p>
<Link href="/projects" className="mt-4 inline-block"> <Link href="/projects" className="mt-4 inline-block">
<Button variant="primary">Back to Projects</Button> <Button variant="primary">Powrót do projektów</Button>
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
@@ -55,10 +55,10 @@ export default async function ProjectViewPage({ params }) {
{daysRemaining !== null && ( {daysRemaining !== null && (
<Badge variant={getDeadlineVariant(daysRemaining)} size="md"> <Badge variant={getDeadlineVariant(daysRemaining)} size="md">
{daysRemaining === 0 {daysRemaining === 0
? "Due Today" ? "Termin dzisiaj"
: daysRemaining > 0 : daysRemaining > 0
? `${daysRemaining} days left` ? `${daysRemaining} dni pozostało`
: `${Math.abs(daysRemaining)} days overdue`} : `${Math.abs(daysRemaining)} dni po terminie`}
</Badge> </Badge>
)} )}
<Link href="/projects"> <Link href="/projects">
@@ -76,7 +76,7 @@ export default async function ProjectViewPage({ params }) {
d="M15 19l-7-7 7-7" d="M15 19l-7-7 7-7"
/> />
</svg> </svg>
Back to Projects Powrót do projektów
</Button> </Button>
</Link> </Link>
<Link href={`/projects/${id}/edit`}> <Link href={`/projects/${id}/edit`}>
@@ -94,7 +94,7 @@ export default async function ProjectViewPage({ params }) {
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/> />
</svg> </svg>
Edit Project Edytuj projekt
</Button> </Button>
</Link> </Link>
</div> </div>
@@ -108,7 +108,7 @@ export default async function ProjectViewPage({ params }) {
{" "} {" "}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900"> <h2 className="text-xl font-semibold text-gray-900">
Project Information Informacje o projekcie
</h2> </h2>
<Badge <Badge
variant={ variant={
@@ -123,12 +123,12 @@ export default async function ProjectViewPage({ params }) {
size="sm" size="sm"
> >
{project.project_type === "design" {project.project_type === "design"
? "Design (P)" ? "Projektowanie (P)"
: project.project_type === "construction" : project.project_type === "construction"
? "Construction (R)" ? "Realizacja (R)"
: project.project_type === "design+construction" : project.project_type === "design+construction"
? "Design + Construction (P+R)" ? "Projektowanie + Realizacja (P+R)"
: "Unknown"} : "Nieznany"}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
@@ -136,7 +136,7 @@ export default async function ProjectViewPage({ params }) {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Location Lokalizacja
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.city || "N/A"} {project.city || "N/A"}
@@ -144,7 +144,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Address Adres
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.address || "N/A"} {project.address || "N/A"}
@@ -152,7 +152,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Plot Działka
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.plot || "N/A"} {project.plot || "N/A"}
@@ -160,7 +160,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
District Dzielnica
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.district || "N/A"} {project.district || "N/A"}
@@ -168,7 +168,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Unit Jednostka
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.unit || "N/A"} {project.unit || "N/A"}
@@ -176,7 +176,7 @@ export default async function ProjectViewPage({ params }) {
</div>{" "} </div>{" "}
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Deadline Termin zakończenia
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.finish_date {project.finish_date
@@ -194,7 +194,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Investment Number Numer inwestycji
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.investment_number || "N/A"} {project.investment_number || "N/A"}
@@ -205,7 +205,7 @@ export default async function ProjectViewPage({ params }) {
{project.contact && ( {project.contact && (
<div className="border-t pt-4"> <div className="border-t pt-4">
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Contact Kontakt
</span> </span>
<p className="text-gray-900 font-medium">{project.contact}</p> <p className="text-gray-900 font-medium">{project.contact}</p>
</div> </div>
@@ -214,7 +214,7 @@ export default async function ProjectViewPage({ params }) {
{project.coordinates && ( {project.coordinates && (
<div className="border-t pt-4"> <div className="border-t pt-4">
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Coordinates Współrzędne
</span> </span>
<p className="text-gray-900 font-medium font-mono text-sm"> <p className="text-gray-900 font-medium font-mono text-sm">
{formatCoordinates(project.coordinates)} {formatCoordinates(project.coordinates)}
@@ -237,14 +237,14 @@ export default async function ProjectViewPage({ params }) {
<Card> <Card>
<CardHeader> <CardHeader>
<h2 className="text-xl font-semibold text-gray-900"> <h2 className="text-xl font-semibold text-gray-900">
Contract Details Szczegóły umowy
</h2> </h2>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Contract Number Numer umowy
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.contract_number || "N/A"} {project.contract_number || "N/A"}
@@ -252,7 +252,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Contract Name Nazwa umowy
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.contract_name || "N/A"} {project.contract_name || "N/A"}
@@ -260,7 +260,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Customer Klient
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.customer || "N/A"} {project.customer || "N/A"}
@@ -268,7 +268,7 @@ export default async function ProjectViewPage({ params }) {
</div> </div>
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-1"> <span className="text-sm font-medium text-gray-500 block mb-1">
Investor Inwestor
</span> </span>
<p className="text-gray-900 font-medium"> <p className="text-gray-900 font-medium">
{project.investor || "N/A"} {project.investor || "N/A"}
@@ -284,21 +284,21 @@ export default async function ProjectViewPage({ params }) {
<Card> <Card>
<CardHeader> <CardHeader>
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
Project Status Status projektu
</h2> </h2>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{" "} {" "}
<div> <div>
<span className="text-sm font-medium text-gray-500 block mb-2"> <span className="text-sm font-medium text-gray-500 block mb-2">
Current Status Aktualny status
</span> </span>
<ProjectStatusDropdown project={project} size="md" /> <ProjectStatusDropdown project={project} size="md" />
</div> </div>
{daysRemaining !== null && ( {daysRemaining !== null && (
<div className="border-t pt-4"> <div className="border-t pt-4">
<span className="text-sm font-medium text-gray-500 block mb-2"> <span className="text-sm font-medium text-gray-500 block mb-2">
Timeline Harmonogram
</span> </span>
<div className="text-center"> <div className="text-center">
<Badge <Badge
@@ -306,10 +306,10 @@ export default async function ProjectViewPage({ params }) {
size="lg" size="lg"
> >
{daysRemaining === 0 {daysRemaining === 0
? "Due Today" ? "Termin dzisiaj"
: daysRemaining > 0 : daysRemaining > 0
? `${daysRemaining} days remaining` ? `${daysRemaining} dni pozostało`
: `${Math.abs(daysRemaining)} days overdue`} : `${Math.abs(daysRemaining)} dni po terminie`}
</Badge> </Badge>
</div> </div>
</div> </div>
@@ -321,7 +321,7 @@ export default async function ProjectViewPage({ params }) {
<Card> <Card>
<CardHeader> <CardHeader>
<h2 className="text-lg font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
Quick Actions Szybkie akcje
</h2> </h2>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
@@ -344,7 +344,7 @@ export default async function ProjectViewPage({ params }) {
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/> />
</svg> </svg>
Edit Project Edytuj projekt
</Button> </Button>
</Link>{" "} </Link>{" "}
<Link href="/projects" className="block"> <Link href="/projects" className="block">
@@ -366,7 +366,7 @@ export default async function ProjectViewPage({ params }) {
d="M15 19l-7-7 7-7" d="M15 19l-7-7 7-7"
/> />
</svg> </svg>
Back to Projects Powrót do projektów
</Button> </Button>
</Link> </Link>
<Link href="/projects/map" className="block"> <Link href="/projects/map" className="block">
@@ -388,7 +388,7 @@ export default async function ProjectViewPage({ params }) {
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
/> />
</svg> </svg>
View All on Map Zobacz wszystkie na mapie
</Button> </Button>
</Link> </Link>
</CardContent> </CardContent>
@@ -404,7 +404,7 @@ export default async function ProjectViewPage({ params }) {
{" "} {" "}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900"> <h2 className="text-xl font-semibold text-gray-900">
Project Location Lokalizacja projektu
</h2> </h2>
{project.coordinates && ( {project.coordinates && (
<Link <Link
@@ -428,7 +428,7 @@ export default async function ProjectViewPage({ params }) {
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 4m0 13V4m0 0L9 7"
/> />
</svg> </svg>
View on Full Map Zobacz na pełnej mapie
</Button> </Button>
</Link> </Link>
)} )}
@@ -454,7 +454,7 @@ export default async function ProjectViewPage({ params }) {
{/* Notes Section */} {/* Notes Section */}
<Card> <Card>
<CardHeader> <CardHeader>
<h2 className="text-xl font-semibold text-gray-900">Notes</h2> <h2 className="text-xl font-semibold text-gray-900">Notatki</h2>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="mb-6"> <div className="mb-6">
@@ -475,10 +475,10 @@ export default async function ProjectViewPage({ params }) {
</svg> </svg>
</div> </div>
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
No notes yet Brak notatek
</h3> </h3>
<p className="text-gray-500"> <p className="text-gray-500">
Add your first note using the form above. Dodaj swoją pierwszą notatkę używając formularza powyżej.
</p> </p>
</div> </div>
) : ( ) : (

View File

@@ -11,8 +11,10 @@ import PageHeader from "@/components/ui/PageHeader";
import SearchBar from "@/components/ui/SearchBar"; import SearchBar from "@/components/ui/SearchBar";
import { LoadingState } from "@/components/ui/States"; import { LoadingState } from "@/components/ui/States";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n";
export default function ProjectListPage() { export default function ProjectListPage() {
const { t } = useTranslation();
const [projects, setProjects] = useState([]); const [projects, setProjects] = useState([]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [filteredProjects, setFilteredProjects] = useState([]); const [filteredProjects, setFilteredProjects] = useState([]);
@@ -45,7 +47,7 @@ export default function ProjectListPage() {
}, [searchTerm, projects]); }, [searchTerm, projects]);
async function handleDelete(id) { async function handleDelete(id) {
const confirmed = confirm("Are you sure you want to delete this project?"); const confirmed = confirm(t('projects.deleteConfirm'));
if (!confirmed) return; if (!confirmed) return;
const res = await fetch(`/api/projects/${id}`, { const res = await fetch(`/api/projects/${id}`, {
@@ -61,7 +63,7 @@ export default function ProjectListPage() {
}; };
return ( return (
<PageContainer> <PageContainer>
<PageHeader title="Projects" description="Manage and track your projects"> <PageHeader title={t('projects.title')} description={t('projects.subtitle')}>
<div className="flex gap-2"> <div className="flex gap-2">
<Link href="/projects/map"> <Link href="/projects/map">
<Button variant="outline" size="lg"> <Button variant="outline" size="lg">
@@ -78,7 +80,7 @@ export default function ProjectListPage() {
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
/> />
</svg> </svg>
Map View Widok mapy
</Button> </Button>
</Link> </Link>
<Link href="/projects/new"> <Link href="/projects/new">
@@ -96,7 +98,7 @@ export default function ProjectListPage() {
d="M12 4v16m8-8H4" d="M12 4v16m8-8H4"
/> />
</svg> </svg>
Add Project {t('projects.newProject')}
</Button> </Button>
</Link> </Link>
</div> </div>
@@ -105,9 +107,9 @@ export default function ProjectListPage() {
<SearchBar <SearchBar
searchTerm={searchTerm} searchTerm={searchTerm}
onSearchChange={handleSearchChange} onSearchChange={handleSearchChange}
placeholder="Search by project name, WP, plot, or investment number..." placeholder={t('projects.searchPlaceholder')}
resultsCount={filteredProjects.length} resultsCount={filteredProjects.length}
resultsText="projects" resultsText="projektów"
/> />
{filteredProjects.length === 0 && searchTerm ? ( {filteredProjects.length === 0 && searchTerm ? (
<Card> <Card>
@@ -126,14 +128,13 @@ export default function ProjectListPage() {
</svg> </svg>
</div> </div>
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
No projects found {t('common.noResults')}
</h3> </h3>
<p className="text-gray-500 mb-6"> <p className="text-gray-500 mb-6">
No projects match your search criteria. Try adjusting your search Brak projektów pasujących do kryteriów wyszukiwania. Spróbuj zmienić wyszukiwane frazy.
terms.
</p> </p>
<Button variant="outline" onClick={() => setSearchTerm("")}> <Button variant="outline" onClick={() => setSearchTerm("")}>
Clear Search Wyczyść wyszukiwanie
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -154,13 +155,13 @@ export default function ProjectListPage() {
</svg> </svg>
</div> </div>
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
No projects yet {t('projects.noProjects')}
</h3> </h3>
<p className="text-gray-500 mb-6"> <p className="text-gray-500 mb-6">
Get started by creating your first project {t('projects.noProjectsMessage')}
</p> </p>
<Link href="/projects/new"> <Link href="/projects/new">
<Button variant="primary">Create First Project</Button> <Button variant="primary">Utwórz pierwszy projekt</Button>
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
@@ -170,34 +171,34 @@ export default function ProjectListPage() {
<thead> <thead>
<tr className="bg-gray-100 border-b"> <tr className="bg-gray-100 border-b">
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-32"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-32">
No. Nr.
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700">
Project Name {t('projects.projectName')}
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-40"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-40">
WP WP
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20">
City {t('projects.city')}
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-40"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-40">
Address {t('projects.address')}
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20">
Plot {t('projects.plot')}
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
Finish {t('projects.finishDate')}
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-12"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-12">
Type Typ
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24">
Status Status
</th> </th>
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20"> <th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20">
Actions Akcje
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -282,7 +283,7 @@ export default function ProjectListPage() {
size="sm" size="sm"
className="text-xs px-2 py-1" className="text-xs px-2 py-1"
> >
View Wyświetl
</Button> </Button>
</Link> </Link>
</td> </td>

View File

@@ -14,8 +14,10 @@ import PageHeader from "@/components/ui/PageHeader";
import SearchBar from "@/components/ui/SearchBar"; import SearchBar from "@/components/ui/SearchBar";
import FilterBar from "@/components/ui/FilterBar"; import FilterBar from "@/components/ui/FilterBar";
import { LoadingState } from "@/components/ui/States"; import { LoadingState } from "@/components/ui/States";
import { useTranslation } from "@/lib/i18n";
export default function ProjectTasksPage() { export default function ProjectTasksPage() {
const { t } = useTranslation();
const [allTasks, setAllTasks] = useState([]); const [allTasks, setAllTasks] = useState([]);
const [filteredTasks, setFilteredTasks] = useState([]); const [filteredTasks, setFilteredTasks] = useState([]);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@@ -148,25 +150,25 @@ export default function ProjectTasksPage() {
const filterOptions = [ const filterOptions = [
{ {
label: "Status", label: t('tasks.status'),
value: statusFilter, value: statusFilter,
onChange: (e) => setStatusFilter(e.target.value), onChange: (e) => setStatusFilter(e.target.value),
options: [ options: [
{ value: "all", label: "All" }, { value: "all", label: t('common.all') },
{ value: "pending", label: "Pending" }, { value: "pending", label: t('taskStatus.pending') },
{ value: "in_progress", label: "In Progress" }, { value: "in_progress", label: t('taskStatus.in_progress') },
{ value: "completed", label: "Completed" }, { value: "completed", label: t('taskStatus.completed') },
], ],
}, },
{ {
label: "Priority", label: t('tasks.priority'),
value: priorityFilter, value: priorityFilter,
onChange: (e) => setPriorityFilter(e.target.value), onChange: (e) => setPriorityFilter(e.target.value),
options: [ options: [
{ value: "all", label: "All" }, { value: "all", label: t('common.all') },
{ value: "high", label: "High" }, { value: "high", label: t('tasks.high') },
{ value: "normal", label: "Normal" }, { value: "normal", label: t('tasks.medium') },
{ value: "low", label: "Low" }, { value: "low", label: t('tasks.low') },
], ],
}, },
]; ];
@@ -174,8 +176,8 @@ export default function ProjectTasksPage() {
return ( return (
<PageContainer> <PageContainer>
<PageHeader <PageHeader
title="Project Tasks" title={t('tasks.title')}
description="Monitor and manage tasks across all projects" description={t('tasks.subtitle')}
/> />
<SearchBar <SearchBar
searchTerm={searchTerm} searchTerm={searchTerm}
@@ -206,7 +208,7 @@ export default function ProjectTasksPage() {
</svg> </svg>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Tasks</p> <p className="text-sm font-medium text-gray-600">{t('dashboard.totalTasks')}</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{statusCounts.all} {statusCounts.all}
</p> </p>
@@ -233,7 +235,7 @@ export default function ProjectTasksPage() {
</svg> </svg>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">Pending</p> <p className="text-sm font-medium text-gray-600">{t('taskStatus.pending')}</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{statusCounts.pending} {statusCounts.pending}
</p> </p>
@@ -260,7 +262,7 @@ export default function ProjectTasksPage() {
</svg> </svg>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">In Progress</p> <p className="text-sm font-medium text-gray-600">{t('taskStatus.in_progress')}</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{statusCounts.in_progress} {statusCounts.in_progress}
</p> </p>
@@ -287,7 +289,7 @@ export default function ProjectTasksPage() {
</svg> </svg>
</div> </div>
<div className="ml-4"> <div className="ml-4">
<p className="text-sm font-medium text-gray-600">Completed</p> <p className="text-sm font-medium text-gray-600">{t('taskStatus.completed')}</p>
<p className="text-2xl font-bold text-gray-900"> <p className="text-2xl font-bold text-gray-900">
{statusCounts.completed} {statusCounts.completed}
</p> </p>

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "@/lib/i18n";
export default function NoteForm({ projectId }) { export default function NoteForm({ projectId }) {
const { t } = useTranslation();
const [note, setNote] = useState(""); const [note, setNote] = useState("");
const [status, setStatus] = useState(null); const [status, setStatus] = useState(null);
@@ -17,10 +19,10 @@ export default function NoteForm({ projectId }) {
if (res.ok) { if (res.ok) {
setNote(""); setNote("");
setStatus("Note added"); setStatus(t("common.addNoteSuccess"));
window.location.reload(); window.location.reload();
} else { } else {
setStatus("Failed to save note"); setStatus(t("common.addNoteError"));
} }
} }
@@ -29,7 +31,7 @@ export default function NoteForm({ projectId }) {
<textarea <textarea
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
placeholder="Add a new note..." placeholder={t("common.addNotePlaceholder")}
className="border p-2 w-full" className="border p-2 w-full"
rows={3} rows={3}
required required
@@ -38,7 +40,7 @@ export default function NoteForm({ projectId }) {
type="submit" type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded" className="bg-blue-600 text-white px-4 py-2 rounded"
> >
Add Note {t("common.addNote")}
</button> </button>
{status && <p className="text-sm text-gray-600">{status}</p>} {status && <p className="text-sm text-gray-600">{status}</p>}
</form> </form>

View File

@@ -6,8 +6,10 @@ import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { formatDateForInput } from "@/lib/utils"; import { formatDateForInput } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n";
export default function ProjectForm({ initialData = null }) { export default function ProjectForm({ initialData = null }) {
const { t } = useTranslation();
const [form, setForm] = useState({ const [form, setForm] = useState({
contract_id: "", contract_id: "",
project_name: "", project_name: "",
@@ -146,7 +148,7 @@ export default function ProjectForm({ initialData = null }) {
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Project Type <span className="text-red-500">*</span> Typ projektu <span className="text-red-500">*</span>
</label> </label>
<select <select
name="project_type" name="project_type"
@@ -155,17 +157,17 @@ export default function ProjectForm({ initialData = null }) {
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
required required
> >
<option value="design">Design (Projektowanie)</option> <option value="design">{t('projectType.design')}</option>
<option value="construction">Construction (Realizacja)</option> <option value="construction">{t('projectType.construction')}</option>
<option value="design+construction"> <option value="design+construction">
Design + Construction (Projektowanie + Realizacja) {t('projectType.design+construction')}
</option> </option>
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Assigned To {t('projects.assignedTo')}
</label> </label>
<select <select
name="assigned_to" name="assigned_to"
@@ -173,7 +175,7 @@ export default function ProjectForm({ initialData = null }) {
onChange={handleChange} onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
> >
<option value="">Unassigned</option> <option value="">{t('projects.unassigned')}</option>
{users.map((user) => ( {users.map((user) => (
<option key={user.id} value={user.id}> <option key={user.id} value={user.id}>
{user.name} ({user.email}) {user.name} ({user.email})
@@ -186,92 +188,92 @@ export default function ProjectForm({ initialData = null }) {
{/* Basic Information Section */} {/* Basic Information Section */}
<div className="border-t pt-6"> <div className="border-t pt-6">
<h3 className="text-lg font-medium text-gray-900 mb-4"> <h3 className="text-lg font-medium text-gray-900 mb-4">
Basic Information {t('projects.basicInformation')}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Project Name <span className="text-red-500">*</span> {t('projects.projectName')} <span className="text-red-500">*</span>
</label> </label>
<Input <Input
type="text" type="text"
name="project_name" name="project_name"
value={form.project_name || ""} value={form.project_name || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter project name" placeholder={t('projects.enterProjectName')}
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
City {t('projects.city')}
</label> </label>
<Input <Input
type="text" type="text"
name="city" name="city"
value={form.city || ""} value={form.city || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter city" placeholder={t('projects.enterCity')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Address {t('projects.address')}
</label> </label>
<Input <Input
type="text" type="text"
name="address" name="address"
value={form.address || ""} value={form.address || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter address" placeholder={t('projects.enterAddress')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Plot {t('projects.plot')}
</label> </label>
<Input <Input
type="text" type="text"
name="plot" name="plot"
value={form.plot || ""} value={form.plot || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter plot number" placeholder={t('projects.enterPlotNumber')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
District {t('projects.district')}
</label> </label>
<Input <Input
type="text" type="text"
name="district" name="district"
value={form.district || ""} value={form.district || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter district" placeholder={t('projects.enterDistrict')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Unit {t('projects.unit')}
</label> </label>
<Input <Input
type="text" type="text"
name="unit" name="unit"
value={form.unit || ""} value={form.unit || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter unit" placeholder={t('projects.enterUnit')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Finish Date {t('projects.finishDate')}
</label>{" "} </label>
<Input <Input
type="date" type="date"
name="finish_date" name="finish_date"
@@ -316,33 +318,33 @@ export default function ProjectForm({ initialData = null }) {
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Contact Information {t('projects.contact')}
</label> </label>
<Input <Input
type="text" type="text"
name="contact" name="contact"
value={form.contact || ""} value={form.contact || ""}
onChange={handleChange} onChange={handleChange}
placeholder="Enter contact details" placeholder={t('projects.placeholders.contact')}
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Coordinates {t('projects.coordinates')}
</label> </label>
<Input <Input
type="text" type="text"
name="coordinates" name="coordinates"
value={form.coordinates || ""} value={form.coordinates || ""}
onChange={handleChange} onChange={handleChange}
placeholder="e.g., 49.622958,20.629562" placeholder={t('projects.placeholders.coordinates')}
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Notes {t('projects.notes')}
</label> </label>
<textarea <textarea
name="notes" name="notes"
@@ -350,7 +352,7 @@ export default function ProjectForm({ initialData = null }) {
onChange={handleChange} onChange={handleChange}
rows={4} rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter any additional notes" placeholder={t('projects.placeholders.notes')}
/> />
</div> </div>
</div> </div>
@@ -364,7 +366,7 @@ export default function ProjectForm({ initialData = null }) {
onClick={() => router.back()} onClick={() => router.back()}
disabled={loading} disabled={loading}
> >
Cancel {t('common.cancel')}
</Button> </Button>
<Button type="submit" variant="primary" disabled={loading}> <Button type="submit" variant="primary" disabled={loading}>
{loading ? ( {loading ? (
@@ -389,7 +391,7 @@ export default function ProjectForm({ initialData = null }) {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
{isEdit ? "Updating..." : "Creating..."} {isEdit ? t('projects.updating') : t('projects.creating')}
</> </>
) : ( ) : (
<> <>
@@ -408,7 +410,7 @@ export default function ProjectForm({ initialData = null }) {
d="M5 13l4 4L19 7" d="M5 13l4 4L19 7"
/> />
</svg> </svg>
Update Project {t('projects.updateProject')}
</> </>
) : ( ) : (
<> <>
@@ -425,7 +427,7 @@ export default function ProjectForm({ initialData = null }) {
d="M12 4v16m8-8H4" d="M12 4v16m8-8H4"
/> />
</svg> </svg>
Create Project {t('projects.createProject')}
</> </>
)} )}
</> </>

View File

@@ -3,12 +3,14 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import { useTranslation } from "@/lib/i18n";
export default function ProjectStatusDropdown({ export default function ProjectStatusDropdown({
project, project,
size = "md", size = "md",
showDropdown = true, showDropdown = true,
}) { }) {
const { t } = useTranslation();
const [status, setStatus] = useState(project.project_status); const [status, setStatus] = useState(project.project_status);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -21,19 +23,19 @@ export default function ProjectStatusDropdown({
const statusConfig = { const statusConfig = {
registered: { registered: {
label: "Registered", label: t("projectStatus.registered"),
variant: "secondary", variant: "secondary",
}, },
in_progress_design: { in_progress_design: {
label: "In Progress (Design)", label: t("projectStatus.in_progress_design"),
variant: "primary", variant: "primary",
}, },
in_progress_construction: { in_progress_construction: {
label: "In Progress (Construction)", label: t("projectStatus.in_progress_construction"),
variant: "primary", variant: "primary",
}, },
fulfilled: { fulfilled: {
label: "Completed", label: t("projectStatus.fulfilled"),
variant: "success", variant: "success",
}, },
}; };

View File

@@ -7,8 +7,10 @@ import { Card, CardHeader, CardContent } from "./ui/Card";
import Button from "./ui/Button"; import Button from "./ui/Button";
import Badge from "./ui/Badge"; import Badge from "./ui/Badge";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n";
export default function ProjectTasksSection({ projectId }) { export default function ProjectTasksSection({ projectId }) {
const { t } = useTranslation();
const [projectTasks, setProjectTasks] = useState([]); const [projectTasks, setProjectTasks] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [taskNotes, setTaskNotes] = useState({}); const [taskNotes, setTaskNotes] = useState({});
@@ -180,14 +182,14 @@ export default function ProjectTasksSection({ projectId }) {
if (res.ok) { if (res.ok) {
refetchTasks(); // Refresh the list refetchTasks(); // Refresh the list
} else { } else {
alert("Failed to update task status"); alert(t("errors.generic"));
} }
} catch (error) { } catch (error) {
alert("Error updating task status"); alert(t("errors.generic"));
} }
}; };
const handleDeleteTask = async (taskId) => { const handleDeleteTask = async (taskId) => {
if (!confirm("Are you sure you want to delete this task?")) return; if (!confirm(t("common.deleteConfirm"))) return;
try { try {
const res = await fetch(`/api/project-tasks/${taskId}`, { const res = await fetch(`/api/project-tasks/${taskId}`, {
method: "DELETE", method: "DELETE",
@@ -197,7 +199,7 @@ export default function ProjectTasksSection({ projectId }) {
refetchTasks(); // Refresh the list refetchTasks(); // Refresh the list
} else { } else {
const errorData = await res.json(); const errorData = await res.json();
alert("Failed to delete task: " + (errorData.error || "Unknown error")); alert(t("errors.generic") + ": " + (errorData.error || t("errors.unknown")));
} }
} catch (error) { } catch (error) {
alert("Error deleting task: " + error.message); alert("Error deleting task: " + error.message);
@@ -227,10 +229,10 @@ export default function ProjectTasksSection({ projectId }) {
setTaskNotes((prev) => ({ ...prev, [taskId]: notes })); setTaskNotes((prev) => ({ ...prev, [taskId]: notes }));
setNewNote((prev) => ({ ...prev, [taskId]: "" })); setNewNote((prev) => ({ ...prev, [taskId]: "" }));
} else { } else {
alert("Failed to add note"); alert(t("errors.generic"));
} }
} catch (error) { } catch (error) {
alert("Error adding note"); alert(t("errors.generic"));
} finally { } finally {
setLoadingNotes((prev) => ({ ...prev, [taskId]: false })); setLoadingNotes((prev) => ({ ...prev, [taskId]: false }));
} }
@@ -278,10 +280,10 @@ export default function ProjectTasksSection({ projectId }) {
refetchTasks(); refetchTasks();
handleCloseEditModal(); handleCloseEditModal();
} else { } else {
alert("Failed to update task"); alert(t("errors.generic"));
} }
} catch (error) { } catch (error) {
alert("Error updating task"); alert(t("errors.generic"));
} }
}; };
@@ -327,10 +329,10 @@ export default function ProjectTasksSection({ projectId }) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">Project Tasks</h2> <h2 className="text-xl font-semibold text-gray-900">{t("tasks.title")}</h2>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Badge variant="default" className="text-sm"> <Badge variant="default" className="text-sm">
{projectTasks.length} {projectTasks.length === 1 ? "task" : "tasks"} {projectTasks.length} {projectTasks.length === 1 ? t("tasks.task") : t("tasks.tasks")}
</Badge> </Badge>
<Button <Button
variant="primary" variant="primary"
@@ -350,7 +352,7 @@ export default function ProjectTasksSection({ projectId }) {
d="M12 4v16m8-8H4" d="M12 4v16m8-8H4"
/> />
</svg> </svg>
Add Task {t("tasks.newTask")}
</Button> </Button>
</div> </div>
</div>{" "} </div>{" "}
@@ -404,7 +406,7 @@ export default function ProjectTasksSection({ projectId }) {
{/* Current Tasks */} {/* Current Tasks */}
<Card> <Card>
<CardHeader> <CardHeader>
<h3 className="text-lg font-medium text-gray-900">Current Tasks</h3> <h3 className="text-lg font-medium text-gray-900">{t("tasks.title")}</h3>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
{loading ? ( {loading ? (
@@ -430,10 +432,10 @@ export default function ProjectTasksSection({ projectId }) {
</svg> </svg>
</div> </div>
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
No tasks assigned to this project yet. {t("tasks.noTasksMessage")}
</p> </p>
<p className="text-gray-400 text-xs mt-1"> <p className="text-gray-400 text-xs mt-1">
Add a task above to get started. {t("tasks.addTaskMessage")}
</p> </p>
</div> </div>
) : ( ) : (
@@ -442,22 +444,22 @@ export default function ProjectTasksSection({ projectId }) {
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 border-b border-gray-200">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Task {t("tasks.taskName")}
</th> </th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Priority {t("tasks.priority")}
</th>{" "} </th>{" "}
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Max Wait {t("tasks.maxWait")}
</th> </th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date Started {t("tasks.dateStarted")}
</th> </th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status {t("tasks.status")}
</th> </th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48"> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-48">
Actions {t("tasks.actions")}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -503,16 +505,16 @@ export default function ProjectTasksSection({ projectId }) {
variant={getPriorityVariant(task.priority)} variant={getPriorityVariant(task.priority)}
size="sm" size="sm"
> >
{task.priority} {t(`tasks.${task.priority}`)}
</Badge> </Badge>
</td> </td>
<td className="px-4 py-4 text-sm text-gray-600"> <td className="px-4 py-4 text-sm text-gray-600">
{task.max_wait_days} days {task.max_wait_days} {t("tasks.days")}
</td>{" "} </td>{" "}
<td className="px-4 py-4 text-sm text-gray-600"> <td className="px-4 py-4 text-sm text-gray-600">
{task.date_started {task.date_started
? formatDate(task.date_started) ? formatDate(task.date_started)
: "Not started"} : t("tasks.notStarted")}
</td> </td>
<td className="px-4 py-4"> <td className="px-4 py-4">
<TaskStatusDropdownSimple <TaskStatusDropdownSimple
@@ -564,7 +566,7 @@ export default function ProjectTasksSection({ projectId }) {
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/> />
</svg> </svg>
Edit {t("common.edit")}
</Button> </Button>
<Button <Button
variant="danger" variant="danger"
@@ -585,7 +587,7 @@ export default function ProjectTasksSection({ projectId }) {
d="19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
Delete {t("common.delete")}
</Button> </Button>
</div> </div>
</td> </td>
@@ -596,7 +598,7 @@ export default function ProjectTasksSection({ projectId }) {
<td colSpan="6" className="px-4 py-3"> <td colSpan="6" className="px-4 py-3">
<div className="text-sm text-gray-700"> <div className="text-sm text-gray-700">
<span className="font-medium text-gray-900"> <span className="font-medium text-gray-900">
Description: {t("tasks.description")}:
</span> </span>
<p className="mt-1">{task.description}</p> <p className="mt-1">{task.description}</p>
</div> </div>
@@ -609,7 +611,7 @@ export default function ProjectTasksSection({ projectId }) {
<td colSpan="6" className="px-4 py-4"> <td colSpan="6" className="px-4 py-4">
<div className="space-y-3"> <div className="space-y-3">
<h5 className="text-sm font-medium text-gray-900"> <h5 className="text-sm font-medium text-gray-900">
Notes ({taskNotes[task.id]?.length || 0}) {t("tasks.comments")} ({taskNotes[task.id]?.length || 0})
</h5> </h5>
{/* Existing Notes */} {/* Existing Notes */}
@@ -629,7 +631,7 @@ export default function ProjectTasksSection({ projectId }) {
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{note.is_system ? ( {note.is_system ? (
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium"> <span className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded-full font-medium">
System {t("admin.system")}
</span> </span>
) : null} ) : null}
{note.created_by_name && ( {note.created_by_name && (
@@ -670,7 +672,7 @@ export default function ProjectTasksSection({ projectId }) {
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
placeholder="Add a note..." placeholder={t("tasks.addComment")}
value={newNote[task.id] || ""} value={newNote[task.id] || ""}
onChange={(e) => onChange={(e) =>
setNewNote((prev) => ({ setNewNote((prev) => ({
@@ -694,7 +696,7 @@ export default function ProjectTasksSection({ projectId }) {
!newNote[task.id]?.trim() !newNote[task.id]?.trim()
} }
> >
{loadingNotes[task.id] ? "Adding..." : "Add"} {loadingNotes[task.id] ? t("common.saving") : t("common.add")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -715,7 +717,7 @@ export default function ProjectTasksSection({ projectId }) {
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4"> <div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">
Edit Task:{" "} {t("tasks.editTask")}:{" "}
{editingTask?.custom_task_name || editingTask?.task_name} {editingTask?.custom_task_name || editingTask?.task_name}
</h3> </h3>
<button <button
@@ -740,7 +742,7 @@ export default function ProjectTasksSection({ projectId }) {
{/* Assignment */} {/* Assignment */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Assigned To {t("tasks.assignedTo")}
</label> </label>
<select <select
value={editTaskForm.assigned_to} value={editTaskForm.assigned_to}
@@ -752,7 +754,7 @@ export default function ProjectTasksSection({ projectId }) {
} }
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
> >
<option value="">Unassigned</option> <option value="">{t("projects.unassigned")}</option>
{users.map((user) => ( {users.map((user) => (
<option key={user.id} value={user.id}> <option key={user.id} value={user.id}>
{user.name} {user.name}
@@ -764,7 +766,7 @@ export default function ProjectTasksSection({ projectId }) {
{/* Priority */} {/* Priority */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Priority {t("tasks.priority")}
</label> </label>
<select <select
value={editTaskForm.priority} value={editTaskForm.priority}
@@ -776,18 +778,18 @@ export default function ProjectTasksSection({ projectId }) {
} }
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
> >
<option value="">Select Priority</option> <option value="">{t("common.selectOption")}</option>
<option value="low">Low</option> <option value="low">{t("tasks.low")}</option>
<option value="normal">Normal</option> <option value="normal">{t("tasks.normal")}</option>
<option value="high">High</option> <option value="high">{t("tasks.high")}</option>
<option value="urgent">Urgent</option> <option value="urgent">{t("tasks.urgent")}</option>
</select> </select>
</div> </div>
{/* Date Started */} {/* Date Started */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Date Started {t("tasks.dateStarted")}
</label> </label>
<input <input
type="date" type="date"
@@ -805,7 +807,7 @@ export default function ProjectTasksSection({ projectId }) {
{/* Status */} {/* Status */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Status {t("tasks.status")}
</label> </label>
<select <select
value={editTaskForm.status} value={editTaskForm.status}
@@ -817,21 +819,21 @@ export default function ProjectTasksSection({ projectId }) {
} }
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
> >
<option value="">Select Status</option> <option value="">{t("common.selectOption")}</option>
<option value="pending">Pending</option> <option value="pending">{t("taskStatus.pending")}</option>
<option value="in_progress">In Progress</option> <option value="in_progress">{t("taskStatus.in_progress")}</option>
<option value="completed">Completed</option> <option value="completed">{t("taskStatus.completed")}</option>
<option value="cancelled">Cancelled</option> <option value="cancelled">{t("taskStatus.cancelled")}</option>
</select> </select>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 mt-6"> <div className="flex items-center justify-end gap-3 mt-6">
<Button variant="outline" onClick={handleCloseEditModal}> <Button variant="outline" onClick={handleCloseEditModal}>
Cancel {t("common.cancel")}
</Button> </Button>
<Button variant="primary" onClick={handleUpdateTask}> <Button variant="primary" onClick={handleUpdateTask}>
Update Task {t("tasks.updateTask")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -3,13 +3,16 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import Badge from "@/components/ui/Badge"; import Badge from "@/components/ui/Badge";
import { useTranslation } from "@/lib/i18n";
export default function TaskStatusDropdown({ export default function TaskStatusDropdown({
task, task,
size = "sm", size = "sm",
showDropdown = true, showDropdown = true,
onStatusChange, onStatusChange,
}) { const [status, setStatus] = useState(task.status); }) {
const { t } = useTranslation();
const [status, setStatus] = useState(task.status);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0, position: 'bottom' }); const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0, position: 'bottom' });
@@ -23,19 +26,19 @@ export default function TaskStatusDropdown({
const statusConfig = { const statusConfig = {
pending: { pending: {
label: "Pending", label: t("taskStatus.pending"),
variant: "warning", variant: "warning",
}, },
in_progress: { in_progress: {
label: "In Progress", label: t("taskStatus.in_progress"),
variant: "primary", variant: "primary",
}, },
completed: { completed: {
label: "Completed", label: t("taskStatus.completed"),
variant: "success", variant: "success",
}, },
cancelled: { cancelled: {
label: "Cancelled", label: t("taskStatus.cancelled"),
variant: "danger", variant: "danger",
}, },
}; };

View File

@@ -0,0 +1,42 @@
"use client";
import { useTranslation } from '@/lib/i18n';
const LanguageSwitcher = ({ className = '' }) => {
const { language, changeLanguage, availableLanguages, t } = useTranslation();
const languageNames = {
pl: 'Polski',
en: 'English'
};
return (
<div className={`relative ${className}`}>
<select
value={language}
onChange={(e) => changeLanguage(e.target.value)}
className="appearance-none bg-white border border-gray-300 rounded-md px-3 py-1 pr-8 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
title={t('common.selectLanguage')}
>
{availableLanguages.map((lang) => (
<option key={lang} value={lang}>
{languageNames[lang] || lang.toUpperCase()}
</option>
))}
</select>
{/* Custom dropdown arrow */}
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
<svg
className="fill-current h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
>
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
</svg>
</div>
</div>
);
};
export default LanguageSwitcher;

View File

@@ -3,10 +3,13 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useSession, signOut } from "next-auth/react"; import { useSession, signOut } from "next-auth/react";
import { useTranslation } from "@/lib/i18n";
import LanguageSwitcher from "./LanguageSwitcher";
const Navigation = () => { const Navigation = () => {
const pathname = usePathname(); const pathname = usePathname();
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const { t } = useTranslation();
const isActive = (path) => { const isActive = (path) => {
if (path === "/") return pathname === "/"; if (path === "/") return pathname === "/";
@@ -18,16 +21,16 @@ const Navigation = () => {
}; };
const navItems = [ const navItems = [
{ href: "/", label: "Dashboard" }, { href: "/", label: t('navigation.dashboard') },
{ href: "/projects", label: "Projects" }, { href: "/projects", label: t('navigation.projects') },
{ href: "/tasks/templates", label: "Task Templates" }, { href: "/tasks/templates", label: t('navigation.taskTemplates') },
{ href: "/project-tasks", label: "Project Tasks" }, { href: "/project-tasks", label: t('navigation.projectTasks') },
{ href: "/contracts", label: "Contracts" }, { href: "/contracts", label: t('navigation.contracts') },
]; ];
// Add admin-only items // Add admin-only items
if (session?.user?.role === 'admin') { if (session?.user?.role === 'admin') {
navItems.push({ href: "/admin/users", label: "User Management" }); navItems.push({ href: "/admin/users", label: t('navigation.userManagement') });
} }
const handleSignOut = async () => { const handleSignOut = async () => {
@@ -45,13 +48,13 @@ const Navigation = () => {
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
<div className="flex items-center"> <div className="flex items-center">
<Link href="/" className="text-xl font-bold text-gray-900"> <Link href="/" className="text-xl font-bold text-gray-900">
Project Panel {t('navigation.projectPanel')}
</Link> </Link>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{status === "loading" ? ( {status === "loading" ? (
<div className="text-gray-500">Loading...</div> <div className="text-gray-500">{t('navigation.loading')}</div>
) : session ? ( ) : session ? (
<> <>
<div className="flex space-x-8"> <div className="flex space-x-8">
@@ -71,10 +74,12 @@ const Navigation = () => {
</div> </div>
<div className="flex items-center space-x-4 ml-8 pl-8 border-l border-gray-200"> <div className="flex items-center space-x-4 ml-8 pl-8 border-l border-gray-200">
<LanguageSwitcher />
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<div className="text-sm"> <div className="text-sm">
<div className="font-medium text-gray-900">{session.user.name}</div> <div className="font-medium text-gray-900">{session.user.name}</div>
<div className="text-gray-500 capitalize">{session.user.role?.replace('_', ' ')}</div> <div className="text-gray-500 capitalize">{t(`userRoles.${session.user.role}`) || session.user.role?.replace('_', ' ')}</div>
</div> </div>
</div> </div>
@@ -82,17 +87,20 @@ const Navigation = () => {
onClick={handleSignOut} onClick={handleSignOut}
className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-md text-sm font-medium transition-colors" className="bg-gray-100 hover:bg-gray-200 text-gray-700 px-3 py-2 rounded-md text-sm font-medium transition-colors"
> >
Sign Out {t('navigation.signOut')}
</button> </button>
</div> </div>
</> </>
) : ( ) : (
<Link <>
href="/auth/signin" <LanguageSwitcher />
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors" <Link
> href="/auth/signin"
Sign In className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition-colors"
</Link> >
{t('navigation.signIn')}
</Link>
</>
)} )}
</div> </div>
</div> </div>

859
src/lib/i18n.js Normal file
View File

@@ -0,0 +1,859 @@
"use client";
import { createContext, useContext, useState, useEffect } from 'react';
// Translation context
const TranslationContext = createContext();
// Translation data
const translations = {
pl: {
// Navigation
navigation: {
dashboard: "Panel główny",
projects: "Projekty",
taskTemplates: "Szablony zadań",
projectTasks: "Zadania projektów",
contracts: "Umowy",
userManagement: "Zarządzanie użytkownikami",
projectPanel: "Panel Projektów",
loading: "Ładowanie...",
signOut: "Wyloguj się",
signIn: "Zaloguj się"
},
// Common UI elements
common: {
save: "Zapisz",
cancel: "Anuluj",
delete: "Usuń",
edit: "Edytuj",
add: "Dodaj",
create: "Utwórz",
update: "Aktualizuj",
search: "Szukaj",
filter: "Filtruj",
loading: "Ładowanie...",
saving: "Zapisywanie...",
creating: "Tworzenie...",
updating: "Aktualizowanie...",
yes: "Tak",
no: "Nie",
confirm: "Potwierdź",
close: "Zamknij",
back: "Wstecz",
next: "Dalej",
previous: "Poprzedni",
page: "Strona",
of: "z",
items: "pozycji",
all: "Wszystkie",
none: "Brak",
required: "wymagane",
optional: "opcjonalne",
selectOption: "Wybierz opcję",
selectLanguage: "Wybierz język",
noResults: "Brak wyników",
error: "Błąd",
success: "Sukces",
warning: "Ostrzeżenie",
info: "Informacja",
deleteConfirm: "Czy na pewno chcesz to usunąć?",
addNote: "Dodaj notatkę",
addNoteSuccess: "Notatka została dodana",
addNoteError: "Nie udało się zapisać notatki",
addNotePlaceholder: "Dodaj nową notatkę..."
},
// Dashboard
dashboard: {
title: "Panel główny",
subtitle: "Przegląd twoich projektów i zadań",
totalProjects: "Wszystkie projekty",
activeProjects: "Aktywne projekty",
completedProjects: "Ukończone projekty",
overdueProjects: "Przeterminowane projekty",
totalContracts: "Wszystkie umowy",
activeContracts: "Aktywne umowy",
pendingTasks: "Oczekujące zadania",
inProgressTasks: "Zadania w trakcie",
completedTasks: "Ukończone zadania",
overdueTasks: "Przeterminowane zadania",
recentProjects: "Najnowsze projekty",
recentTasks: "Najnowsze zadania",
upcomingDeadlines: "Nadchodzące terminy",
projectsThisWeek: "Projekty w tym tygodniu",
tasksThisWeek: "Zadania w tym tygodniu",
completionRate: "Wskaźnik ukończenia",
viewAll: "Zobacz wszystkie",
noRecentProjects: "Brak najnowszych projektów",
noRecentTasks: "Brak najnowszych zadań",
noUpcomingDeadlines: "Brak nadchodzących terminów"
},
// Project statuses
projectStatus: {
registered: "Zarejestrowany",
in_progress_design: "W realizacji (projektowanie)",
in_progress_construction: "W realizacji (realizacja)",
fulfilled: "Zakończony",
unknown: "Nieznany"
},
// Project types
projectType: {
design: "Projektowanie",
construction: "Realizacja",
"design+construction": "Projektowanie + Realizacja"
},
// Task statuses
taskStatus: {
pending: "Oczekujące",
in_progress: "W trakcie",
completed: "Ukończone",
cancelled: "Anulowane",
on_hold: "Wstrzymane"
},
// Projects
projects: {
title: "Projekty",
subtitle: "Zarządzaj swoimi projektami",
newProject: "Nowy projekt",
editProject: "Edytuj projekt",
deleteProject: "Usuń projekt",
projectName: "Nazwa projektu",
city: "Miasto",
address: "Adres",
plot: "Działka",
district: "Dzielnica",
unit: "Jednostka",
finishDate: "Data zakończenia",
contact: "Kontakt",
coordinates: "Współrzędne",
notes: "Notatki",
assignedTo: "Przypisane do",
unassigned: "Nieprzypisane",
createdBy: "Utworzone przez",
lastModified: "Ostatnia modyfikacja",
basicInformation: "Informacje podstawowe",
locationDetails: "Szczegóły lokalizacji",
projectDetails: "Szczegóły projektu",
additionalInfo: "Dodatkowe informacje",
searchPlaceholder: "Szukaj projektów po nazwie, mieście lub adresie...",
noProjects: "Brak projektów",
noProjectsMessage: "Rozpocznij od utworzenia swojego pierwszego projektu.",
deleteConfirm: "Czy na pewno chcesz usunąć ten projekt?",
deleteSuccess: "Projekt został pomyślnie usunięty",
createSuccess: "Projekt został pomyślnie utworzony",
updateSuccess: "Projekt został pomyślnie zaktualizowany",
enterProjectName: "Wprowadź nazwę projektu",
enterCity: "Wprowadź miasto",
enterAddress: "Wprowadź adres",
enterPlotNumber: "Wprowadź numer działki",
enterDistrict: "Wprowadź dzielnicę",
enterUnit: "Wprowadź jednostkę",
enterContactDetails: "Wprowadź dane kontaktowe",
coordinatesPlaceholder: "np. 49.622958,20.629562",
enterNotes: "Wprowadź notatki..."
},
// Contracts
contracts: {
title: "Umowy",
subtitle: "Zarządzaj swoimi umowami i kontraktami",
newContract: "Nowa umowa",
editContract: "Edytuj umowę",
deleteContract: "Usuń umowę",
contractNumber: "Numer umowy",
contractName: "Nazwa umowy",
customerContractNumber: "Numer umowy klienta",
customer: "Klient",
investor: "Inwestor",
dateSigned: "Data zawarcia",
finishDate: "Data zakończenia",
searchPlaceholder: "Szukaj umów po numerze, nazwie, kliencie lub inwestorze...",
noContracts: "Brak umów",
noContractsMessage: "Rozpocznij od utworzenia swojej pierwszej umowy.",
noMatchingContracts: "Brak pasujących umów",
changeSearchCriteria: "Spróbuj zmienić kryteria wyszukiwania lub filtry.",
all: "Wszystkie",
active: "Aktywne",
withoutEndDate: "W trakcie",
expired: "Przeterminowane",
createContract: "Utwórz umowę",
enterContractNumber: "Wprowadź numer umowy",
enterContractName: "Wprowadź nazwę umowy",
enterCustomerContractNumber: "Wprowadź numer umowy klienta",
enterCustomerName: "Wprowadź nazwę klienta",
enterInvestorName: "Wprowadź nazwę inwestora",
signedOn: "Zawarcie:",
finishOn: "Zakończenie:",
customerLabel: "Zleceniodawca:",
investorLabel: "Inwestor:"
},
// Tasks
tasks: {
title: "Zadania",
task: "zadanie",
tasks: "zadania",
subtitle: "Zarządzaj zadaniami projektów",
newTask: "Nowe zadanie",
editTask: "Edytuj zadanie",
deleteTask: "Usuń zadanie",
taskName: "Nazwa zadania",
description: "Opis",
priority: "Priorytet",
assignedTo: "Przypisane do",
dueDate: "Termin wykonania",
status: "Status",
project: "Projekt",
template: "Szablon",
searchPlaceholder: "Szukaj zadań...",
noTasks: "Brak zadań",
noTasksMessage: "Brak zadań przypisanych do tego projektu.",
addTaskMessage: "Dodaj zadanie powyżej, aby rozpocząć.",
filterBy: "Filtruj według",
sortBy: "Sortuj według",
allTasks: "Wszystkie zadania",
myTasks: "Moje zadania",
overdue: "Przeterminowane",
dueSoon: "Niedługo termin",
high: "Wysoki",
normal: "Normalny",
medium: "Średni",
low: "Niski",
dateCreated: "Data utworzenia",
dateModified: "Data modyfikacji",
daysLeft: "dni pozostało",
daysOverdue: "dni przeterminowane",
dueToday: "Termin dzisiaj",
startTask: "Rozpocznij zadanie",
completeTask: "Zakończ zadanie",
comments: "Komentarze",
addComment: "Dodaj komentarz",
noComments: "Brak komentarzy",
maxWait: "Maksymalne oczekiwanie",
dateStarted: "Data rozpoczęcia",
actions: "Działania",
urgent: "Pilne",
updateTask: "Aktualizuj zadanie",
notStarted: "Nie rozpoczęte",
days: "dni"
},
// Task Templates
taskTemplates: {
title: "Szablony zadań",
subtitle: "Zarządzaj szablonami zadań",
newTemplate: "Nowy szablon",
editTemplate: "Edytuj szablon",
deleteTemplate: "Usuń szablon",
templateName: "Nazwa szablonu",
templateDescription: "Opis szablonu",
defaultPriority: "Domyślny priorytet",
estimatedDuration: "Szacowany czas trwania",
category: "Kategoria",
searchPlaceholder: "Szukaj szablonów...",
noTemplates: "Brak szablonów",
noTemplatesMessage: "Rozpocznij od utworzenia swojego pierwszego szablonu zadania.",
useTemplate: "Użyj szablonu",
duplicateTemplate: "Duplikuj szablon"
},
// Forms
forms: {
validation: {
required: "To pole jest wymagane",
email: "Wprowadź prawidłowy adres email",
minLength: "Minimalna długość: {min} znaków",
maxLength: "Maksymalna długość: {max} znaków",
invalidDate: "Nieprawidłowa data",
invalidCoordinates: "Nieprawidłowe współrzędne"
},
placeholders: {
enterText: "Wprowadź tekst...",
selectDate: "Wybierz datę",
selectOption: "Wybierz opcję",
searchResults: "Szukaj wyników..."
}
},
// Sorting and filtering
sorting: {
sortBy: "Sortuj według",
orderBy: "Kolejność",
ascending: "Rosnąco",
descending: "Malejąco",
name: "Nazwa",
date: "Data",
status: "Status",
priority: "Priorytet",
dateCreated: "Data utworzenia",
dateModified: "Data modyfikacji",
startDate: "Data rozpoczęcia",
finishDate: "Data zakończenia"
},
// Date formats
dates: {
today: "Dzisiaj",
yesterday: "Wczoraj",
tomorrow: "Jutro",
daysAgo: "{count} dni temu",
inDays: "za {count} dni",
invalidDate: "Nieprawidłowa data",
selectDate: "Wybierz datę"
},
// Map layers
mapLayers: {
polishOrthophoto: "🛰️ Polska Ortofotomapa",
polishCadastral: "📋 Polskie Dane Katastralne",
polishSpatialPlanning: "🏗️ Polskie Planowanie Przestrzenne",
lpPortalRoads: "🛣️ LP-Portal Drogi",
lpPortalStreetNames: "🏷️ LP-Portal Nazwy Ulic",
lpPortalParcels: "📐 LP-Portal Działki",
lpPortalSurveyMarkers: "📍 LP-Portal Punkty Pomiarowe",
openStreetMap: "🗺️ OpenStreetMap",
googleSatellite: "🛰️ Google Satelita",
googleRoads: "🛣️ Google Drogi",
layerControl: "Kontrola warstw",
baseLayers: "Warstwy bazowe",
overlayLayers: "Warstwy nakładkowe",
opacity: "Przezroczystość"
},
// Authentication
auth: {
signIn: "Zaloguj się",
signOut: "Wyloguj się",
email: "Email",
password: "Hasło",
rememberMe: "Zapamiętaj mnie",
forgotPassword: "Zapomniałeś hasła?",
signInWith: "Zaloguj się przez",
signUp: "Zarejestruj się",
createAccount: "Utwórz konto",
alreadyHaveAccount: "Masz już konto?",
dontHaveAccount: "Nie masz konta?",
signInError: "Błąd logowania",
signOutError: "Błąd wylogowania",
emailRequired: "Email jest wymagany",
passwordRequired: "Hasło jest wymagane",
invalidCredentials: "Nieprawidłowe dane logowania"
},
// User roles
userRoles: {
admin: "Administrator",
user: "Użytkownik",
manager: "Menedżer",
viewer: "Czytelnik"
},
// Error messages
errors: {
generic: "Wystąpił błąd. Spróbuj ponownie.",
network: "Błąd połączenia sieciowego",
unauthorized: "Brak autoryzacji",
forbidden: "Brak uprawnień",
notFound: "Nie znaleziono",
validation: "Błąd walidacji danych",
server: "Błąd serwera",
timeout: "Przekroczono limit czasu",
unknown: "Nieznany błąd"
},
// Success messages
success: {
saved: "Zapisano pomyślnie",
created: "Utworzono pomyślnie",
updated: "Zaktualizowano pomyślnie",
deleted: "Usunięto pomyślnie",
sent: "Wysłano pomyślnie",
completed: "Ukończono pomyślnie"
},
// Admin section
admin: {
title: "Administracja",
subtitle: "Zarządzaj użytkownikami systemu i uprawnieniami",
userManagement: "Zarządzanie użytkownikami",
auditLogs: "Logi audytu",
systemSettings: "Ustawienia systemu",
users: "Użytkownicy",
newUser: "Nowy użytkownik",
editUser: "Edytuj użytkownika",
deleteUser: "Usuń użytkownika",
userName: "Nazwa użytkownika",
userEmail: "Email użytkownika",
userRole: "Rola użytkownika",
active: "Aktywny",
inactive: "Nieaktywny",
lastLogin: "Ostatnie logowanie",
createdAt: "Data utworzenia",
noUsers: "Brak użytkowników",
system: "System"
}
},
en: {
// English translations (fallback)
navigation: {
dashboard: "Dashboard",
projects: "Projects",
taskTemplates: "Task Templates",
projectTasks: "Project Tasks",
contracts: "Contracts",
userManagement: "User Management",
projectPanel: "Project Panel",
loading: "Loading...",
signOut: "Sign Out",
signIn: "Sign In"
},
common: {
save: "Save",
cancel: "Cancel",
delete: "Delete",
edit: "Edit",
add: "Add",
create: "Create",
update: "Update",
search: "Search",
filter: "Filter",
loading: "Loading...",
saving: "Saving...",
creating: "Creating...",
updating: "Updating...",
yes: "Yes",
no: "No",
confirm: "Confirm",
close: "Close",
back: "Back",
next: "Next",
previous: "Previous",
page: "Page",
of: "of",
items: "items",
all: "All",
none: "None",
required: "required",
optional: "optional",
selectOption: "Select option",
selectLanguage: "Select language",
noResults: "No results",
error: "Error",
success: "Success",
warning: "Warning",
info: "Info",
deleteConfirm: "Are you sure you want to delete this?",
addNote: "Add Note",
addNoteSuccess: "Note added",
addNoteError: "Failed to save note",
addNotePlaceholder: "Add a new note..."
},
dashboard: {
title: "Dashboard",
subtitle: "Overview of your projects and tasks",
totalProjects: "Total Projects",
activeProjects: "Active Projects",
completedProjects: "Completed Projects",
overdueProjects: "Overdue Projects",
totalContracts: "Total Contracts",
activeContracts: "Active Contracts",
pendingTasks: "Pending Tasks",
inProgressTasks: "In Progress Tasks",
completedTasks: "Completed Tasks",
overdueTasks: "Overdue Tasks",
recentProjects: "Recent Projects",
recentTasks: "Recent Tasks",
upcomingDeadlines: "Upcoming Deadlines",
projectsThisWeek: "Projects This Week",
tasksThisWeek: "Tasks This Week",
completionRate: "Completion Rate",
viewAll: "View All",
noRecentProjects: "No recent projects",
noRecentTasks: "No recent tasks",
noUpcomingDeadlines: "No upcoming deadlines"
},
projectStatus: {
registered: "Registered",
in_progress_design: "In Progress (Design)",
in_progress_construction: "In Progress (Construction)",
fulfilled: "Completed",
unknown: "Unknown"
},
projectType: {
design: "Design",
construction: "Construction",
"design+construction": "Design + Construction"
},
taskStatus: {
pending: "Pending",
in_progress: "In Progress",
completed: "Completed",
cancelled: "Cancelled",
on_hold: "On Hold"
},
projects: {
title: "Projects",
subtitle: "Manage your projects",
newProject: "New Project",
editProject: "Edit Project",
deleteProject: "Delete Project",
projectName: "Project Name",
city: "City",
address: "Address",
plot: "Plot",
district: "District",
unit: "Unit",
finishDate: "Finish Date",
contact: "Contact",
coordinates: "Coordinates",
notes: "Notes",
assignedTo: "Assigned To",
unassigned: "Unassigned",
createdBy: "Created By",
lastModified: "Last Modified",
basicInformation: "Basic Information",
locationDetails: "Location Details",
projectDetails: "Project Details",
additionalInfo: "Additional Information",
searchPlaceholder: "Search projects by name, city or address...",
noProjects: "No projects",
noProjectsMessage: "Get started by creating your first project.",
deleteConfirm: "Are you sure you want to delete this project?",
deleteSuccess: "Project deleted successfully",
createSuccess: "Project created successfully",
updateSuccess: "Project updated successfully",
enterProjectName: "Enter project name",
enterCity: "Enter city",
enterAddress: "Enter address",
enterPlotNumber: "Enter plot number",
enterDistrict: "Enter district",
enterUnit: "Enter unit",
enterContactDetails: "Enter contact details",
coordinatesPlaceholder: "e.g., 49.622958,20.629562",
enterNotes: "Enter notes..."
},
contracts: {
title: "Contracts",
subtitle: "Manage your contracts and agreements",
newContract: "New Contract",
editContract: "Edit Contract",
deleteContract: "Delete Contract",
contractNumber: "Contract Number",
contractName: "Contract Name",
customerContractNumber: "Customer Contract Number",
customer: "Customer",
investor: "Investor",
dateSigned: "Date Signed",
finishDate: "Finish Date",
searchPlaceholder: "Search contracts by number, name, customer or investor...",
noContracts: "No contracts",
noContractsMessage: "Get started by creating your first contract.",
noMatchingContracts: "No matching contracts",
changeSearchCriteria: "Try changing your search criteria or filters.",
all: "All",
active: "Active",
withoutEndDate: "In Progress",
expired: "Expired",
createContract: "Create Contract",
enterContractNumber: "Enter contract number",
enterContractName: "Enter contract name",
enterCustomerContractNumber: "Enter customer contract number",
enterCustomerName: "Enter customer name",
enterInvestorName: "Enter investor name",
signedOn: "Signed:",
finishOn: "Finish:",
customerLabel: "Customer:",
investorLabel: "Investor:"
},
tasks: {
title: "Tasks",
task: "task",
tasks: "tasks",
subtitle: "Manage project tasks",
newTask: "New Task",
editTask: "Edit Task",
deleteTask: "Delete Task",
taskName: "Task Name",
description: "Description",
priority: "Priority",
assignedTo: "Assigned To",
dueDate: "Due Date",
status: "Status",
project: "Project",
template: "Template",
searchPlaceholder: "Search tasks...",
noTasks: "No tasks",
noTasksMessage: "No tasks assigned to this project yet.",
addTaskMessage: "Add a task above to get started.",
filterBy: "Filter by",
sortBy: "Sort by",
allTasks: "All Tasks",
myTasks: "My Tasks",
overdue: "Overdue",
dueSoon: "Due Soon",
high: "High",
normal: "Normal",
medium: "Medium",
low: "Low",
dateCreated: "Date Created",
dateModified: "Date Modified",
daysLeft: "days left",
daysOverdue: "days overdue",
dueToday: "Due Today",
startTask: "Start Task",
completeTask: "Complete Task",
comments: "Comments",
addComment: "Add Comment",
noComments: "No comments",
maxWait: "Max Wait",
dateStarted: "Date Started",
actions: "Actions",
urgent: "Urgent",
updateTask: "Update Task",
notStarted: "Not started",
days: "days"
},
taskTemplates: {
title: "Task Templates",
subtitle: "Manage task templates",
newTemplate: "New Template",
editTemplate: "Edit Template",
deleteTemplate: "Delete Template",
templateName: "Template Name",
templateDescription: "Template Description",
defaultPriority: "Default Priority",
estimatedDuration: "Estimated Duration",
category: "Category",
searchPlaceholder: "Search templates...",
noTemplates: "No templates",
noTemplatesMessage: "Get started by creating your first task template.",
useTemplate: "Use Template",
duplicateTemplate: "Duplicate Template"
},
forms: {
validation: {
required: "This field is required",
email: "Please enter a valid email address",
minLength: "Minimum length: {min} characters",
maxLength: "Maximum length: {max} characters",
invalidDate: "Invalid date",
invalidCoordinates: "Invalid coordinates"
},
placeholders: {
enterText: "Enter text...",
selectDate: "Select date",
selectOption: "Select option",
searchResults: "Search results..."
}
},
sorting: {
sortBy: "Sort by",
orderBy: "Order by",
ascending: "Ascending",
descending: "Descending",
name: "Name",
date: "Date",
status: "Status",
priority: "Priority",
dateCreated: "Date Created",
dateModified: "Date Modified",
startDate: "Start Date",
finishDate: "Finish Date"
},
dates: {
today: "Today",
yesterday: "Yesterday",
tomorrow: "Tomorrow",
daysAgo: "{count} days ago",
inDays: "in {count} days",
invalidDate: "Invalid date",
selectDate: "Select date"
},
mapLayers: {
polishOrthophoto: "🛰️ Polish Orthophoto",
polishCadastral: "📋 Polish Cadastral Data",
polishSpatialPlanning: "🏗️ Polish Spatial Planning",
lpPortalRoads: "🛣️ LP-Portal Roads",
lpPortalStreetNames: "🏷️ LP-Portal Street Names",
lpPortalParcels: "📐 LP-Portal Parcels",
lpPortalSurveyMarkers: "📍 LP-Portal Survey Markers",
openStreetMap: "🗺️ OpenStreetMap",
googleSatellite: "🛰️ Google Satellite",
googleRoads: "🛣️ Google Roads",
layerControl: "Layer Control",
baseLayers: "Base Layers",
overlayLayers: "Overlay Layers",
opacity: "Opacity"
},
auth: {
signIn: "Sign In",
signOut: "Sign Out",
email: "Email",
password: "Password",
rememberMe: "Remember Me",
forgotPassword: "Forgot Password?",
signInWith: "Sign in with",
signUp: "Sign Up",
createAccount: "Create Account",
alreadyHaveAccount: "Already have an account?",
dontHaveAccount: "Don't have an account?",
signInError: "Sign in error",
signOutError: "Sign out error",
emailRequired: "Email is required",
passwordRequired: "Password is required",
invalidCredentials: "Invalid credentials"
},
userRoles: {
admin: "Administrator",
user: "User",
manager: "Manager",
viewer: "Viewer"
},
errors: {
generic: "An error occurred. Please try again.",
network: "Network connection error",
unauthorized: "Unauthorized",
forbidden: "Forbidden",
notFound: "Not found",
validation: "Validation error",
server: "Server error",
timeout: "Request timeout",
unknown: "Unknown error"
},
success: {
saved: "Saved successfully",
created: "Created successfully",
updated: "Updated successfully",
deleted: "Deleted successfully",
sent: "Sent successfully",
completed: "Completed successfully"
},
admin: {
title: "Administration",
userManagement: "User Management",
auditLogs: "Audit Logs",
systemSettings: "System Settings",
users: "Users",
newUser: "New User",
editUser: "Edit User",
deleteUser: "Delete User",
userName: "User Name",
userEmail: "User Email",
userRole: "User Role",
active: "Active",
inactive: "Inactive",
lastLogin: "Last Login",
createdAt: "Created At",
noUsers: "No users",
system: "System"
}
}
};
// Translation provider component
export function TranslationProvider({ children, initialLanguage = 'pl' }) {
const [language, setLanguage] = useState(initialLanguage);
// Load language from localStorage on mount
useEffect(() => {
const savedLanguage = localStorage.getItem('app-language');
if (savedLanguage && translations[savedLanguage]) {
setLanguage(savedLanguage);
}
}, []);
// Save language to localStorage when changed
useEffect(() => {
localStorage.setItem('app-language', language);
}, [language]);
const changeLanguage = (newLanguage) => {
if (translations[newLanguage]) {
setLanguage(newLanguage);
}
};
const t = (key, replacements = {}) => {
const keys = key.split('.');
let value = translations[language];
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
// Fallback to English if key not found in current language
value = translations.en;
for (const fallbackKey of keys) {
if (value && typeof value === 'object' && fallbackKey in value) {
value = value[fallbackKey];
} else {
console.warn(`Translation key not found: ${key} (language: ${language})`);
return key; // Return the key itself as fallback
}
}
break;
}
}
if (typeof value !== 'string') {
console.warn(`Translation value is not a string: ${key} (language: ${language})`);
return key;
}
// Replace placeholders like {count}, {min}, {max}, etc.
let result = value;
Object.keys(replacements).forEach(placeholder => {
result = result.replace(new RegExp(`{${placeholder}}`, 'g'), replacements[placeholder]);
});
return result;
};
return (
<TranslationContext.Provider value={{
t,
language,
changeLanguage,
availableLanguages: Object.keys(translations)
}}>
{children}
</TranslationContext.Provider>
);
}
// Hook to use translations
export function useTranslation() {
const context = useContext(TranslationContext);
if (!context) {
throw new Error('useTranslation must be used within a TranslationProvider');
}
return context;
}
// Export translations for direct access if needed
export { translations };

View File

@@ -34,9 +34,9 @@ export const formatProjectType = (type) => {
}; };
export const getDeadlineText = (daysRemaining) => { export const getDeadlineText = (daysRemaining) => {
if (daysRemaining === 0) return "Due Today"; if (daysRemaining === 0) return "Termin dzisiaj";
if (daysRemaining > 0) return `${daysRemaining} days left`; if (daysRemaining > 0) return `${daysRemaining} dni pozostało`;
return `${Math.abs(daysRemaining)} days overdue`; return `${Math.abs(daysRemaining)} dni przeterminowane`;
}; };
export const formatDate = (date, options = {}) => { export const formatDate = (date, options = {}) => {
@@ -46,7 +46,7 @@ export const formatDate = (date, options = {}) => {
const dateObj = typeof date === "string" ? new Date(date) : date; const dateObj = typeof date === "string" ? new Date(date) : date;
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return "Invalid date"; return "Nieprawidłowa data";
} }
// Default to DD.MM.YYYY format // Default to DD.MM.YYYY format
@@ -63,7 +63,7 @@ export const formatDate = (date, options = {}) => {
return `${day}.${month}.${year}`; return `${day}.${month}.${year}`;
} catch (error) { } catch (error) {
console.error("Error formatting date:", error); console.error("Error formatting date:", error);
return "Invalid date"; return "Nieprawidłowa data";
} }
}; };