feat: Implement internationalization for task management components

- Added translation support for task-related strings in ProjectTaskForm and ProjectTasksSection components.
- Integrated translation for navigation items in the Navigation component.
- Created ProjectCalendarWidget component with Polish translations for project statuses and deadlines.
- Developed Tooltip component for enhanced user experience with tooltips.
- Established a field change history logging system in the database with associated queries.
- Enhanced task update logging to include translated status and priority changes.
- Introduced server-side translations for system messages to improve localization.
This commit is contained in:
2025-09-11 15:49:07 +02:00
parent 50adc50a24
commit 0dd988730f
24 changed files with 1945 additions and 114 deletions

361
src/app/calendar/page.js Normal file
View File

@@ -0,0 +1,361 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { Card, CardHeader, CardContent } from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import Badge from "@/components/ui/Badge";
import PageContainer from "@/components/ui/PageContainer";
import PageHeader from "@/components/ui/PageHeader";
import { LoadingState } from "@/components/ui/States";
import { formatDate } from "@/lib/utils";
import { useTranslation } from "@/lib/i18n";
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
addDays,
isSameMonth,
isSameDay,
addMonths,
subMonths,
parseISO,
isAfter,
isBefore,
startOfDay,
addWeeks
} from "date-fns";
import { pl } from "date-fns/locale";
const statusColors = {
registered: "bg-blue-100 text-blue-800",
approved: "bg-green-100 text-green-800",
pending: "bg-yellow-100 text-yellow-800",
in_progress: "bg-orange-100 text-orange-800",
fulfilled: "bg-gray-100 text-gray-800",
};
const statusTranslations = {
registered: "Zarejestrowany",
approved: "Zatwierdzony",
pending: "Oczekujący",
in_progress: "W trakcie",
fulfilled: "Zakończony",
};
export default function ProjectCalendarPage() {
const { t } = useTranslation();
const [projects, setProjects] = useState([]);
const [loading, setLoading] = useState(true);
const [currentDate, setCurrentDate] = useState(new Date());
const [viewMode, setViewMode] = useState('month'); // 'month' or 'upcoming'
useEffect(() => {
fetch("/api/projects")
.then((res) => res.json())
.then((data) => {
// Filter projects that have finish dates and are not fulfilled
const projectsWithDates = data.filter(p =>
p.finish_date && p.project_status !== 'fulfilled'
);
setProjects(projectsWithDates);
setLoading(false);
})
.catch((error) => {
console.error("Error fetching projects:", error);
setLoading(false);
});
}, []);
const getProjectsForDate = (date) => {
return projects.filter(project => {
if (!project.finish_date) return false;
try {
const projectDate = parseISO(project.finish_date);
return isSameDay(projectDate, date);
} catch (error) {
return false;
}
});
};
const getUpcomingProjects = () => {
const today = startOfDay(new Date());
const nextMonth = addWeeks(today, 4);
return projects
.filter(project => {
if (!project.finish_date) return false;
try {
const projectDate = parseISO(project.finish_date);
return isAfter(projectDate, today) && isBefore(projectDate, nextMonth);
} catch (error) {
return false;
}
})
.sort((a, b) => {
const dateA = parseISO(a.finish_date);
const dateB = parseISO(b.finish_date);
return dateA - dateB;
});
};
const getOverdueProjects = () => {
const today = startOfDay(new Date());
return projects
.filter(project => {
if (!project.finish_date) return false;
try {
const projectDate = parseISO(project.finish_date);
return isBefore(projectDate, today);
} catch (error) {
return false;
}
})
.sort((a, b) => {
const dateA = parseISO(a.finish_date);
const dateB = parseISO(b.finish_date);
return dateB - dateA; // Most recently overdue first
});
};
const renderCalendarGrid = () => {
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const calendarStart = startOfWeek(monthStart, { weekStartsOn: 1 });
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
const days = [];
let day = calendarStart;
while (day <= calendarEnd) {
days.push(day);
day = addDays(day, 1);
}
const weekdays = ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nie'];
return (
<div className="bg-white rounded-lg shadow">
{/* Calendar Header */}
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
{format(currentDate, 'LLLL yyyy', { locale: pl })}
</h2>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentDate(subMonths(currentDate, 1))}
>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentDate(new Date())}
>
Dziś
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentDate(addMonths(currentDate, 1))}
>
</Button>
</div>
</div>
</div>
{/* Weekday Headers */}
<div className="grid grid-cols-7 border-b border-gray-200">
{weekdays.map(weekday => (
<div key={weekday} className="p-2 text-sm font-medium text-gray-500 text-center">
{weekday}
</div>
))}
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7">
{days.map((day, index) => {
const dayProjects = getProjectsForDate(day);
const isCurrentMonth = isSameMonth(day, currentDate);
const isToday = isSameDay(day, new Date());
return (
<div
key={index}
className={`min-h-[120px] p-2 border-r border-b border-gray-100 ${
!isCurrentMonth ? 'bg-gray-50' : 'bg-white'
} ${isToday ? 'bg-blue-50' : ''}`}
>
<div className={`text-sm font-medium mb-2 ${
!isCurrentMonth ? 'text-gray-400' : isToday ? 'text-blue-600' : 'text-gray-900'
}`}>
{format(day, 'd')}
</div>
{dayProjects.length > 0 && (
<div className="space-y-1">
{dayProjects.slice(0, 3).map(project => (
<Link
key={project.project_id}
href={`/projects/${project.project_id}`}
className="block"
>
<div className={`text-xs p-1 rounded truncate ${
statusColors[project.project_status] || statusColors.registered
} hover:opacity-80 transition-opacity`}>
{project.project_name}
</div>
</Link>
))}
{dayProjects.length > 3 && (
<div className="text-xs text-gray-500 p-1">
+{dayProjects.length - 3} więcej
</div>
)}
</div>
)}
</div>
);
})}
</div>
</div>
);
};
const renderUpcomingView = () => {
const upcomingProjects = getUpcomingProjects();
const overdueProjects = getOverdueProjects();
return (
<div className="space-y-6">
{/* Overdue Projects */}
{overdueProjects.length > 0 && (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold text-red-600">
Projekty przeterminowane ({overdueProjects.length})
</h3>
</CardHeader>
<CardContent>
<div className="space-y-3">
{overdueProjects.map(project => (
<div key={project.project_id} className="flex items-center justify-between p-3 bg-red-50 rounded-lg border border-red-200">
<div className="flex-1">
<Link
href={`/projects/${project.project_id}`}
className="font-medium text-gray-900 hover:text-blue-600"
>
{project.project_name}
</Link>
<div className="text-sm text-gray-600 mt-1">
{project.customer && `${project.customer}`}
{project.address}
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-red-600">
{formatDate(project.finish_date)}
</div>
<Badge className={statusColors[project.project_status] || statusColors.registered}>
{statusTranslations[project.project_status] || project.project_status}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Upcoming Projects */}
<Card>
<CardHeader>
<h3 className="text-lg font-semibold text-gray-900">
Nadchodzące terminy ({upcomingProjects.length})
</h3>
</CardHeader>
<CardContent>
{upcomingProjects.length > 0 ? (
<div className="space-y-3">
{upcomingProjects.map(project => {
const daysUntilDeadline = Math.ceil((parseISO(project.finish_date) - new Date()) / (1000 * 60 * 60 * 24));
return (
<div key={project.project_id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<Link
href={`/projects/${project.project_id}`}
className="font-medium text-gray-900 hover:text-blue-600"
>
{project.project_name}
</Link>
<div className="text-sm text-gray-600 mt-1">
{project.customer && `${project.customer}`}
{project.address}
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-gray-900">
{formatDate(project.finish_date)}
</div>
<div className="text-xs text-gray-500">
za {daysUntilDeadline} dni
</div>
<Badge className={statusColors[project.project_status] || statusColors.registered}>
{statusTranslations[project.project_status] || project.project_status}
</Badge>
</div>
</div>
);
})}
</div>
) : (
<p className="text-gray-500 text-center py-8">
Brak nadchodzących projektów w następnych 4 tygodniach
</p>
)}
</CardContent>
</Card>
</div>
);
};
if (loading) {
return <LoadingState />;
}
return (
<PageContainer>
<PageHeader
title="Kalendarz projektów"
subtitle={`${projects.length} aktywnych projektów z terminami`}
>
<div className="flex space-x-2">
<Button
variant={viewMode === 'month' ? 'primary' : 'outline'}
onClick={() => setViewMode('month')}
>
Kalendarz
</Button>
<Button
variant={viewMode === 'upcoming' ? 'primary' : 'outline'}
onClick={() => setViewMode('upcoming')}
>
Lista terminów
</Button>
</div>
</PageHeader>
{viewMode === 'month' ? renderCalendarGrid() : renderUpcomingView()}
</PageContainer>
);
}