From 84f63c37cecb5ab5fc56223a3834fe7ed0495fc3 Mon Sep 17 00:00:00 2001 From: chop Date: Thu, 22 Jan 2026 21:43:08 +0100 Subject: [PATCH] feat: Implement sortable project list with dynamic sorting functionality --- src/app/projects/page.js | 190 ++++++++++++++++++++++++++++++++------- src/lib/i18n.js | 8 +- 2 files changed, 161 insertions(+), 37 deletions(-) diff --git a/src/app/projects/page.js b/src/app/projects/page.js index e07c795..63e927c 100644 --- a/src/app/projects/page.js +++ b/src/app/projects/page.js @@ -29,6 +29,10 @@ export default function ProjectListPage() { }); const [customers, setCustomers] = useState([]); + const [sortConfig, setSortConfig] = useState({ + key: 'finish_date', + direction: 'desc' + }); // Load phoneOnly filter from localStorage after mount to avoid hydration issues useEffect(() => { @@ -125,8 +129,44 @@ export default function ProjectListPage() { setSearchMatchType(null); } + // Apply sorting + if (sortConfig.key) { + filtered = [...filtered].sort((a, b) => { + let aVal = a[sortConfig.key]; + let bVal = b[sortConfig.key]; + + // Handle null/undefined + if (!aVal && !bVal) return 0; + if (!aVal) return sortConfig.direction === 'asc' ? 1 : -1; + if (!bVal) return sortConfig.direction === 'asc' ? -1 : 1; + + // Handle dates + if (sortConfig.key === 'finish_date') { + aVal = new Date(aVal); + bVal = new Date(bVal); + } + // Handle numbers (project_number) + else if (sortConfig.key === 'project_number') { + // Extract numeric part if it's a string like "P-123" or "123" + const aNum = parseInt(String(aVal).replace(/\D/g, '')) || 0; + const bNum = parseInt(String(bVal).replace(/\D/g, '')) || 0; + aVal = aNum; + bVal = bNum; + } + // Handle strings + else if (typeof aVal === 'string') { + aVal = aVal.toLowerCase(); + bVal = String(bVal).toLowerCase(); + } + + if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1; + if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1; + return 0; + }); + } + setFilteredProjects(filtered); - }, [searchTerm, projects, filters, session]); + }, [searchTerm, projects, filters, session, sortConfig]); async function handleDelete(id) { const confirmed = confirm(t('projects.deleteConfirm')); @@ -171,6 +211,13 @@ export default function ProjectListPage() { setSearchTerm(''); }; + const handleSort = (key) => { + setSortConfig(prev => ({ + key, + direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc' + })); + }; + const handleExportExcel = async () => { try { const response = await fetch('/api/projects/export'); @@ -228,6 +275,42 @@ export default function ProjectListPage() { default: return "-"; } }; + + // Sortable header component + const SortableHeader = ({ columnKey, label, className = "" }) => { + const isSorted = sortConfig.key === columnKey; + const direction = isSorted ? sortConfig.direction : null; + + return ( + handleSort(columnKey)} + title={`Sort by ${label}`} + > +
+ {label} + + {!isSorted && ( + + + + )} + {isSorted && direction === 'asc' && ( + + + + )} + {isSorted && direction === 'desc' && ( + + + + )} + +
+ + ); + }; + return ( @@ -606,8 +689,29 @@ export default function ProjectListPage() { {/* Results and clear button row */}
-
- {t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`} +
+
+ {t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`} +
+ {sortConfig.key && ( +
+ + Sortowanie: + + {sortConfig.key === 'project_number' && 'Nr.'} + {sortConfig.key === 'project_name' && t('projects.projectName')} + {sortConfig.key === 'address' && t('projects.address')} + {sortConfig.key === 'wp' && 'WP'} + {sortConfig.key === 'city' && t('projects.city')} + {sortConfig.key === 'plot' && t('projects.plot')} + {sortConfig.key === 'finish_date' && t('projects.finishDate')} + {sortConfig.key === 'project_type' && t('common.type')} + {sortConfig.key === 'project_status' && t('common.status')} + {sortConfig.key === 'assigned_to' && t('projects.assigned')} + + {sortConfig.direction === 'asc' ? '↑' : '↓'} +
+ )}
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm) && ( @@ -699,36 +803,56 @@ export default function ProjectListPage() { - - - - - - - - - - + + + + + + + + + + diff --git a/src/lib/i18n.js b/src/lib/i18n.js index e3561d5..e003336 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -186,7 +186,7 @@ const translations = { plot: "Działka", district: "Jednostka ewidencyjna", unit: "Obręb", - finishDate: "Data zakończenia", + finishDate: "Termin zakończenia", type: "Typ", contact: "Kontakt", coordinates: "Współrzędne", @@ -294,7 +294,7 @@ const translations = { customer: "Klient", investor: "Inwestor", dateSigned: "Data zawarcia", - finishDate: "Data zakończenia", + finishDate: "Termin zakończenia", searchPlaceholder: "Szukaj umów po numerze, nazwie, kliencie lub inwestorze...", noContracts: "Brak umów", noContractsMessage: "Rozpocznij od utworzenia swojej pierwszej umowy.", @@ -325,7 +325,7 @@ const translations = { customer: "Klient", investor: "Inwestor", dateSigned: "Data zawarcia", - finishDate: "Data zakończenia", + finishDate: "Termin zakończenia", summary: "Podsumowanie", projectsCount: "Liczba projektów", projects: "projektów", @@ -534,7 +534,7 @@ const translations = { dateCreated: "Data utworzenia", dateModified: "Data modyfikacji", startDate: "Data rozpoczęcia", - finishDate: "Data zakończenia" + finishDate: "Termin zakończenia" }, // Date formats
- Nr. - - {t('projects.projectName')} - - {t('projects.address')} - - WP - - {t('projects.city')} - - {t('projects.plot')} - - {t('projects.finishDate')} - - {t('common.type') || 'Typ'} - - {t('common.status') || 'Status'} - - {t('projects.assigned') || 'Przypisany'} -