feat: Implement mobile-friendly filter toggle and enhance project list filters
This commit is contained in:
@@ -99,62 +99,140 @@ export default function ProjectViewPage() {
|
|||||||
if (days <= 7) return "warning";
|
if (days <= 7) return "warning";
|
||||||
return "success";
|
return "success";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<PageHeader
|
{/* Mobile: Full-width title, Desktop: Standard PageHeader */}
|
||||||
title={project.project_name}
|
<div className="block sm:hidden mb-6">
|
||||||
description={`${project.city} • ${project.address}`}
|
{/* Mobile Layout */}
|
||||||
action={
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-3">
|
{/* Full-width title */}
|
||||||
<ProjectStatusDropdown project={project} size="sm" />
|
<div className="w-full">
|
||||||
{daysRemaining !== null && (
|
<h1 className="text-2xl font-bold text-gray-900 break-words">
|
||||||
<Badge variant={getDeadlineVariant(daysRemaining)} size="md">
|
{project.project_name}
|
||||||
{daysRemaining === 0
|
</h1>
|
||||||
? "Termin dzisiaj"
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
: daysRemaining > 0
|
{project.city} • {project.address}
|
||||||
? `${daysRemaining} dni pozostało`
|
</p>
|
||||||
: `${Math.abs(daysRemaining)} dni po terminie`}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Link href="/projects">
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 mr-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M15 19l-7-7 7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Powrót do projektów
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Link href={`/projects/${params.id}/edit`}>
|
|
||||||
<Button variant="primary">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 mr-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
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>
|
|
||||||
Edytuj projekt
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
/>{" "}
|
{/* Mobile action bar */}
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
{/* Status and deadline badges */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<ProjectStatusDropdown project={project} size="sm" />
|
||||||
|
{daysRemaining !== null && (
|
||||||
|
<Badge variant={getDeadlineVariant(daysRemaining)} size="sm" className="text-xs">
|
||||||
|
{daysRemaining === 0
|
||||||
|
? "Termin dzisiaj"
|
||||||
|
: daysRemaining > 0
|
||||||
|
? `${daysRemaining} dni pozostało`
|
||||||
|
: `${Math.abs(daysRemaining)} dni po terminie`}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons - full width */}
|
||||||
|
<div className="flex gap-2 w-full">
|
||||||
|
<Link href="/projects" className="flex-1">
|
||||||
|
<Button variant="outline" size="sm" className="w-full text-xs">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-1"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Powrót
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href={`/projects/${params.id}/edit`} className="flex-1">
|
||||||
|
<Button variant="primary" size="sm" className="w-full text-xs">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-1"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
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>
|
||||||
|
Edytuj
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop: Standard PageHeader */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<PageHeader
|
||||||
|
title={project.project_name}
|
||||||
|
description={`${project.city} • ${project.address}`}
|
||||||
|
action={
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ProjectStatusDropdown project={project} size="sm" />
|
||||||
|
{daysRemaining !== null && (
|
||||||
|
<Badge variant={getDeadlineVariant(daysRemaining)} size="md">
|
||||||
|
{daysRemaining === 0
|
||||||
|
? "Termin dzisiaj"
|
||||||
|
: daysRemaining > 0
|
||||||
|
? `${daysRemaining} dni pozostało`
|
||||||
|
: `${Math.abs(daysRemaining)} dni po terminie`}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
<Link href="/projects">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Powrót do projektów
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link href={`/projects/${params.id}/edit`}>
|
||||||
|
<Button variant="primary">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
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>
|
||||||
|
Edytuj projekt
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
{/* Main Project Information */}
|
{/* Main Project Information */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
@@ -446,7 +524,7 @@ export default function ProjectViewPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>{" "}
|
</div>
|
||||||
{/* Project Location Map */}
|
{/* Project Location Map */}
|
||||||
{project.coordinates && (
|
{project.coordinates && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default function ProjectListPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [customers, setCustomers] = useState([]);
|
const [customers, setCustomers] = useState([]);
|
||||||
|
const [filtersExpanded, setFiltersExpanded] = useState(true); // Start expanded on mobile so users know filters exist
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/projects")
|
fetch("/api/projects")
|
||||||
@@ -113,6 +114,21 @@ export default function ProjectListPage() {
|
|||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleFilters = () => {
|
||||||
|
setFiltersExpanded(!filtersExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasActiveFilters = filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || searchTerm.trim() !== '';
|
||||||
|
|
||||||
|
const getActiveFilterCount = () => {
|
||||||
|
let count = 0;
|
||||||
|
if (filters.status !== 'all') count++;
|
||||||
|
if (filters.type !== 'all') count++;
|
||||||
|
if (filters.customer !== 'all') count++;
|
||||||
|
if (searchTerm.trim()) count++;
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
const getStatusLabel = (status) => {
|
const getStatusLabel = (status) => {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case "registered": return t('projectStatus.registered');
|
case "registered": return t('projectStatus.registered');
|
||||||
@@ -185,76 +201,186 @@ export default function ProjectListPage() {
|
|||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
<CardContent className="p-4">
|
{/* Mobile collapsible header */}
|
||||||
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center">
|
<div
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50 transition-colors md:hidden"
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
onClick={toggleFilters}
|
||||||
{t('common.status') || 'Status'}:
|
>
|
||||||
</label>
|
<div className="flex items-center space-x-3">
|
||||||
<select
|
<svg
|
||||||
value={filters.status}
|
className={`w-5 h-5 text-gray-500 transition-transform duration-200 ${filtersExpanded ? 'rotate-180' : ''}`}
|
||||||
onChange={(e) => handleFilterChange('status', e.target.value)}
|
fill="none"
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
stroke="currentColor"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<option value="all">{t('common.all')}</option>
|
>
|
||||||
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
<path
|
||||||
<option value="registered">{t('projectStatus.registered')}</option>
|
strokeLinecap="round"
|
||||||
<option value="in_progress_design">{t('projectStatus.in_progress_design')}</option>
|
strokeLinejoin="round"
|
||||||
<option value="in_progress_construction">{t('projectStatus.in_progress_construction')}</option>
|
strokeWidth={2}
|
||||||
<option value="fulfilled">{t('projectStatus.fulfilled')}</option>
|
d="M19 9l-7 7-7-7"
|
||||||
</select>
|
/>
|
||||||
</div>
|
</svg>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900">
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
{t('common.filters') || 'Filtry'}
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
{hasActiveFilters && (
|
||||||
{t('common.type') || 'Typ'}:
|
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
</label>
|
{getActiveFilterCount()}
|
||||||
<select
|
</span>
|
||||||
value={filters.type}
|
)}
|
||||||
onChange={(e) => handleFilterChange('type', e.target.value)}
|
</h3>
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
</div>
|
||||||
>
|
<div className="flex items-center space-x-2">
|
||||||
<option value="all">{t('common.all')}</option>
|
{hasActiveFilters && (
|
||||||
<option value="design">{t('projectType.design')}</option>
|
|
||||||
<option value="construction">{t('projectType.construction')}</option>
|
|
||||||
<option value="design+construction">{t('projectType.design+construction')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
|
||||||
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
|
||||||
{t('contracts.customer') || 'Klient'}:
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.customer}
|
|
||||||
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
|
||||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
|
||||||
>
|
|
||||||
<option value="all">{t('common.all')}</option>
|
|
||||||
{customers.map((customer) => (
|
|
||||||
<option key={customer} value={customer}>
|
|
||||||
{customer}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || searchTerm) && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={clearAllFilters}
|
onClick={(e) => {
|
||||||
className="text-xs self-start md:self-auto"
|
e.stopPropagation();
|
||||||
|
clearAllFilters();
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{t('common.clearAllFilters') || 'Wyczyść wszystkie filtry'}
|
{t('common.clearAll') || 'Wyczyść'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
<div className="text-sm text-gray-500 md:ml-auto md:text-right">
|
|
||||||
{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`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile collapsible content */}
|
||||||
|
<div className={`overflow-hidden transition-all duration-300 ease-in-out md:hidden ${filtersExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'}`}>
|
||||||
|
<div className="px-4 pb-4 border-t border-gray-100">
|
||||||
|
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center pt-4">
|
||||||
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
||||||
|
{t('common.status') || 'Status'}:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.status}
|
||||||
|
onChange={(e) => handleFilterChange('status', e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
||||||
|
<option value="registered">{t('projectStatus.registered')}</option>
|
||||||
|
<option value="in_progress_design">{t('projectStatus.in_progress_design')}</option>
|
||||||
|
<option value="in_progress_construction">{t('projectStatus.in_progress_construction')}</option>
|
||||||
|
<option value="fulfilled">{t('projectStatus.fulfilled')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
||||||
|
{t('common.type') || 'Typ'}:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.type}
|
||||||
|
onChange={(e) => handleFilterChange('type', e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
<option value="design">{t('projectType.design')}</option>
|
||||||
|
<option value="construction">{t('projectType.construction')}</option>
|
||||||
|
<option value="design+construction">{t('projectType.design+construction')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
||||||
|
{t('contracts.customer') || 'Klient'}:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.customer}
|
||||||
|
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
{customers.map((customer) => (
|
||||||
|
<option key={customer} value={customer}>
|
||||||
|
{customer}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop always visible content */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex flex-col space-y-4 md:flex-row md:flex-wrap md:gap-4 md:space-y-0 md:items-center">
|
||||||
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
||||||
|
{t('common.status') || 'Status'}:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.status}
|
||||||
|
onChange={(e) => handleFilterChange('status', e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
<option value="not_finished">{t('projects.notFinished') || 'Nie zakończone'}</option>
|
||||||
|
<option value="registered">{t('projectStatus.registered')}</option>
|
||||||
|
<option value="in_progress_design">{t('projectStatus.in_progress_design')}</option>
|
||||||
|
<option value="in_progress_construction">{t('projectStatus.in_progress_construction')}</option>
|
||||||
|
<option value="fulfilled">{t('projectStatus.fulfilled')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
||||||
|
{t('common.type') || 'Typ'}:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.type}
|
||||||
|
onChange={(e) => handleFilterChange('type', e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
<option value="design">{t('projectType.design')}</option>
|
||||||
|
<option value="construction">{t('projectType.construction')}</option>
|
||||||
|
<option value="design+construction">{t('projectType.design+construction')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col space-y-2 md:flex-row md:items-center md:space-y-0 md:space-x-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 md:text-xs md:whitespace-nowrap">
|
||||||
|
{t('contracts.customer') || 'Klient'}:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filters.customer}
|
||||||
|
onChange={(e) => handleFilterChange('customer', e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 md:px-3 md:py-1 md:text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">{t('common.all')}</option>
|
||||||
|
{customers.map((customer) => (
|
||||||
|
<option key={customer} value={customer}>
|
||||||
|
{customer}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || searchTerm) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearAllFilters}
|
||||||
|
className="text-xs self-start md:self-auto"
|
||||||
|
>
|
||||||
|
{t('common.clearAllFilters') || 'Wyczyść wszystkie filtry'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500 md:ml-auto md:text-right">
|
||||||
|
{t('projects.showingResults', { shown: filteredProjects.length, total: projects.length }) || `Wyświetlono ${filteredProjects.length} z ${projects.length} projektów`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{filteredProjects.length === 0 && searchTerm ? (
|
{filteredProjects.length === 0 && searchTerm ? (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -314,37 +440,37 @@ export default function ProjectListPage() {
|
|||||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||||
{/* Mobile scroll container */}
|
{/* Mobile scroll container */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[800px] table-fixed">
|
<table className="w-full min-w-[600px] table-fixed">
|
||||||
<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-24 md:w-32">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-24">
|
||||||
Nr.
|
Nr.
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 min-w-[200px]">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-[200px] md:w-[250px]">
|
||||||
{t('projects.projectName')}
|
{t('projects.projectName')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-40 hidden sm:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-16 md:w-20 hidden sm:table-cell">
|
||||||
WP
|
WP
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-16 md:w-20 hidden md:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-14 md:w-16 hidden md:table-cell">
|
||||||
{t('projects.city')}
|
{t('projects.city')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-24 md:w-40 hidden lg:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-24 hidden lg:table-cell">
|
||||||
{t('projects.address')}
|
{t('projects.address')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-16 md:w-20 hidden sm:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-14 md:w-16 hidden sm:table-cell">
|
||||||
{t('projects.plot')}
|
{t('projects.plot')}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-24 hidden md:table-cell">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-18 md:w-20 hidden md:table-cell">
|
||||||
{t('projects.finishDate')}
|
{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-10">
|
||||||
{t('common.type') || 'Typ'}
|
{t('common.type') || 'Typ'}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-20 md:w-24">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-16 md:w-20">
|
||||||
{t('common.status') || 'Status'}
|
{t('common.status') || 'Status'}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-16 md:w-20">
|
<th className="text-left px-2 py-3 font-semibold text-xs text-gray-700 w-14 md:w-16">
|
||||||
{t('common.actions') || 'Akcje'}
|
{t('common.actions') || 'Akcje'}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -362,15 +488,15 @@ export default function ProjectListPage() {
|
|||||||
{project.project_number}
|
{project.project_number}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-2 py-3">
|
<td className="px-2 py-3 w-[200px] md:w-[250px]">
|
||||||
<Link
|
<Link
|
||||||
href={`/projects/${project.project_id}`}
|
href={`/projects/${project.project_id}`}
|
||||||
className="font-medium text-blue-600 hover:text-blue-800 transition-colors text-sm truncate block"
|
className="font-medium text-blue-600 hover:text-blue-800 transition-colors text-sm truncate block"
|
||||||
title={project.project_name}
|
title={project.project_name}
|
||||||
>
|
>
|
||||||
<span className="block sm:hidden">
|
<span className="block sm:hidden">
|
||||||
{project.project_name.length > 30
|
{project.project_name.length > 20
|
||||||
? `${project.project_name.substring(0, 30)}...`
|
? `${project.project_name.substring(0, 20)}...`
|
||||||
: project.project_name}
|
: project.project_name}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden sm:block">
|
<span className="hidden sm:block">
|
||||||
|
|||||||
Reference in New Issue
Block a user