feat: Implement sortable project list with dynamic sorting functionality
This commit is contained in:
@@ -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 (
|
||||
<th
|
||||
className={`text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors select-none ${className}`}
|
||||
onClick={() => handleSort(columnKey)}
|
||||
title={`Sort by ${label}`}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{label}</span>
|
||||
<span className="text-gray-400 flex-shrink-0">
|
||||
{!isSorted && (
|
||||
<svg className="w-3 h-3 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
)}
|
||||
{isSorted && direction === 'asc' && (
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 14l5-5 5 5H7z" />
|
||||
</svg>
|
||||
)}
|
||||
{isSorted && direction === 'desc' && (
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 10l5 5 5-5H7z" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader title={t('projects.title')} description={t('projects.subtitle')}>
|
||||
@@ -606,8 +689,29 @@ export default function ProjectListPage() {
|
||||
|
||||
{/* Results and clear button row */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
|
||||
<div className="text-sm text-gray-500">
|
||||
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm text-gray-500">
|
||||
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
|
||||
</div>
|
||||
{sortConfig.key && (
|
||||
<div className="text-xs text-gray-500 flex items-center gap-1">
|
||||
<span>•</span>
|
||||
<span>Sortowanie:</span>
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">
|
||||
{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')}
|
||||
</span>
|
||||
<span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm) && (
|
||||
@@ -699,36 +803,56 @@ export default function ProjectListPage() {
|
||||
<table className="w-full min-w-[600px] table-fixed">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 dark:bg-gray-700 border-b dark:border-gray-600">
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-20 md:w-24">
|
||||
Nr.
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-[200px] md:w-[250px]">
|
||||
{t('projects.projectName')}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-20 md:w-24 hidden lg:table-cell">
|
||||
{t('projects.address')}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-16 md:w-20 hidden sm:table-cell">
|
||||
WP
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16 hidden md:table-cell">
|
||||
{t('projects.city')}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16 hidden sm:table-cell">
|
||||
{t('projects.plot')}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-18 md:w-20 hidden md:table-cell">
|
||||
{t('projects.finishDate')}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-10">
|
||||
{t('common.type') || 'Typ'}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-10">
|
||||
{t('common.status') || 'Status'}
|
||||
</th>
|
||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 dark:text-gray-300 w-14 md:w-16">
|
||||
{t('projects.assigned') || 'Przypisany'}
|
||||
</th>
|
||||
<SortableHeader
|
||||
columnKey="project_number"
|
||||
label="Nr."
|
||||
className="w-20 md:w-24"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="project_name"
|
||||
label={t('projects.projectName')}
|
||||
className="w-[200px] md:w-[250px]"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="address"
|
||||
label={t('projects.address')}
|
||||
className="w-20 md:w-24 hidden lg:table-cell"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="wp"
|
||||
label="WP"
|
||||
className="w-16 md:w-20 hidden sm:table-cell"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="city"
|
||||
label={t('projects.city')}
|
||||
className="w-14 md:w-16 hidden md:table-cell"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="plot"
|
||||
label={t('projects.plot')}
|
||||
className="w-14 md:w-16 hidden sm:table-cell"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="finish_date"
|
||||
label={t('projects.finishDate')}
|
||||
className="w-18 md:w-20 hidden md:table-cell"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="project_type"
|
||||
label={t('common.type') || 'Typ'}
|
||||
className="w-10"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="project_status"
|
||||
label={t('common.status') || 'Status'}
|
||||
className="w-10"
|
||||
/>
|
||||
<SortableHeader
|
||||
columnKey="assigned_to"
|
||||
label={t('projects.assigned') || 'Przypisany'}
|
||||
className="w-14 md:w-16"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user