662 lines
25 KiB
JavaScript
662 lines
25 KiB
JavaScript
"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 { Input } from "@/components/ui/Input";
|
||
import PageContainer from "@/components/ui/PageContainer";
|
||
import PageHeader from "@/components/ui/PageHeader";
|
||
import SearchBar from "@/components/ui/SearchBar";
|
||
import { LoadingState } from "@/components/ui/States";
|
||
import { formatDate } from "@/lib/utils";
|
||
import { useTranslation } from "@/lib/i18n";
|
||
import { useSession } from "next-auth/react";
|
||
|
||
export default function ProjectListPage() {
|
||
const { t } = useTranslation();
|
||
const { data: session } = useSession();
|
||
const [projects, setProjects] = useState([]);
|
||
const [searchTerm, setSearchTerm] = useState("");
|
||
const [filteredProjects, setFilteredProjects] = useState([]);
|
||
const [filters, setFilters] = useState({
|
||
status: 'all',
|
||
type: 'all',
|
||
customer: 'all',
|
||
mine: false
|
||
});
|
||
|
||
const [customers, setCustomers] = useState([]);
|
||
const [filtersExpanded, setFiltersExpanded] = useState(true); // Start expanded on mobile so users know filters exist
|
||
|
||
useEffect(() => {
|
||
fetch("/api/projects")
|
||
.then((res) => res.json())
|
||
.then((data) => {
|
||
setProjects(data);
|
||
setFilteredProjects(data);
|
||
|
||
// Extract unique customers for filter
|
||
const uniqueCustomers = [...new Set(data.map(p => p.customer).filter(Boolean))];
|
||
setCustomers(uniqueCustomers);
|
||
});
|
||
}, []);
|
||
|
||
// Filter projects based on search term and filters
|
||
useEffect(() => {
|
||
let filtered = projects;
|
||
|
||
// Apply status filter
|
||
if (filters.status !== 'all') {
|
||
if (filters.status === 'not_finished') {
|
||
filtered = filtered.filter(project => project.project_status !== 'fulfilled');
|
||
} else {
|
||
filtered = filtered.filter(project => project.project_status === filters.status);
|
||
}
|
||
}
|
||
|
||
// Apply type filter
|
||
if (filters.type !== 'all') {
|
||
filtered = filtered.filter(project => project.project_type === filters.type);
|
||
}
|
||
|
||
// Apply customer filter
|
||
if (filters.customer !== 'all') {
|
||
filtered = filtered.filter(project => project.customer === filters.customer);
|
||
}
|
||
|
||
// Apply mine filter
|
||
if (filters.mine && session?.user?.id) {
|
||
filtered = filtered.filter(project => project.assigned_to === session.user.id);
|
||
}
|
||
|
||
// Apply search term
|
||
if (searchTerm.trim()) {
|
||
const searchLower = searchTerm.toLowerCase();
|
||
filtered = filtered.filter((project) => {
|
||
return (
|
||
project.project_name?.toLowerCase().includes(searchLower) ||
|
||
project.wp?.toLowerCase().includes(searchLower) ||
|
||
project.plot?.toLowerCase().includes(searchLower) ||
|
||
project.investment_number?.toLowerCase().includes(searchLower) ||
|
||
project.address?.toLowerCase().includes(searchLower) ||
|
||
project.customer?.toLowerCase().includes(searchLower) ||
|
||
project.investor?.toLowerCase().includes(searchLower)
|
||
);
|
||
});
|
||
}
|
||
|
||
setFilteredProjects(filtered);
|
||
}, [searchTerm, projects, filters, session]);
|
||
|
||
async function handleDelete(id) {
|
||
const confirmed = confirm(t('projects.deleteConfirm'));
|
||
if (!confirmed) return;
|
||
|
||
const res = await fetch(`/api/projects/${id}`, {
|
||
method: "DELETE",
|
||
});
|
||
if (res.ok) {
|
||
setProjects((prev) => prev.filter((p) => p.project_id !== id));
|
||
}
|
||
}
|
||
|
||
const handleSearchChange = (e) => {
|
||
setSearchTerm(e.target.value);
|
||
};
|
||
|
||
const handleFilterChange = (filterType, value) => {
|
||
setFilters(prev => ({
|
||
...prev,
|
||
[filterType]: value
|
||
}));
|
||
};
|
||
|
||
const clearAllFilters = () => {
|
||
setFilters({
|
||
status: 'all',
|
||
type: 'all',
|
||
customer: 'all',
|
||
mine: false
|
||
});
|
||
setSearchTerm('');
|
||
};
|
||
|
||
const handleExportExcel = async () => {
|
||
try {
|
||
const response = await fetch('/api/projects/export');
|
||
if (response.ok) {
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `projects_export_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
} else {
|
||
alert('Failed to export projects. Please try again.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Export error:', error);
|
||
alert('An error occurred during export. Please try again.');
|
||
}
|
||
};
|
||
|
||
const toggleFilters = () => {
|
||
setFiltersExpanded(!filtersExpanded);
|
||
};
|
||
|
||
const hasActiveFilters = filters.status !== 'all' || filters.type !== 'all' || filters.customer !== 'all' || filters.mine || searchTerm.trim() !== '';
|
||
|
||
const getActiveFilterCount = () => {
|
||
let count = 0;
|
||
if (filters.status !== 'all') count++;
|
||
if (filters.type !== 'all') count++;
|
||
if (filters.customer !== 'all') count++;
|
||
if (filters.mine) count++;
|
||
if (searchTerm.trim()) count++;
|
||
return count;
|
||
};
|
||
|
||
const getStatusLabel = (status) => {
|
||
switch(status) {
|
||
case "registered": return t('projectStatus.registered');
|
||
case "in_progress_design": return t('projectStatus.in_progress_design');
|
||
case "in_progress_construction": return t('projectStatus.in_progress_construction');
|
||
case "fulfilled": return t('projectStatus.fulfilled');
|
||
case "cancelled": return t('projectStatus.cancelled');
|
||
default: return "-";
|
||
}
|
||
};
|
||
|
||
const getTypeLabel = (type) => {
|
||
switch(type) {
|
||
case "design": return t('projectType.design');
|
||
case "construction": return t('projectType.construction');
|
||
case "design+construction": return t('projectType.design+construction');
|
||
default: return "-";
|
||
}
|
||
};
|
||
return (
|
||
<PageContainer>
|
||
<PageHeader title={t('projects.title')} description={t('projects.subtitle')}>
|
||
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-2 sm:gap-2">
|
||
<Link href="/projects/map" className="w-full sm:w-auto">
|
||
<Button variant="outline" size="lg" className="w-full">
|
||
<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="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>
|
||
{t('projects.mapView') || 'Widok mapy'}
|
||
</Button>
|
||
</Link>
|
||
{session?.user && (
|
||
<Button
|
||
variant={filters.mine ? "primary" : "outline"}
|
||
size="lg"
|
||
className="w-full sm:w-auto"
|
||
onClick={() => handleFilterChange('mine', !filters.mine)}
|
||
>
|
||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||
/>
|
||
</svg>
|
||
{t('projects.mine') || 'Moje'}
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="outline"
|
||
size="lg"
|
||
className="w-full sm:w-auto"
|
||
onClick={handleExportExcel}
|
||
>
|
||
<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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||
{t('projects.exportExcel') || 'Export to Excel'}
|
||
</Button>
|
||
<Link href="/projects/new" className="w-full sm:w-auto">
|
||
<Button variant="primary" size="lg" className="w-full">
|
||
<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"
|
||
/>
|
||
</svg>
|
||
{t('projects.newProject')}
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</PageHeader>
|
||
|
||
<SearchBar
|
||
searchTerm={searchTerm}
|
||
onSearchChange={handleSearchChange}
|
||
placeholder={t('projects.searchPlaceholder')}
|
||
resultsCount={filteredProjects.length}
|
||
resultsText={t('projects.projects') || 'projektów'}
|
||
/>
|
||
|
||
{/* Filters */}
|
||
<Card className="mb-6">
|
||
{/* Mobile collapsible header */}
|
||
<div
|
||
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50 transition-colors md:hidden"
|
||
onClick={toggleFilters}
|
||
>
|
||
<div className="flex items-center space-x-3">
|
||
<svg
|
||
className={`w-5 h-5 text-gray-500 transition-transform duration-200 ${filtersExpanded ? 'rotate-180' : ''}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M19 9l-7 7-7-7"
|
||
/>
|
||
</svg>
|
||
<h3 className="text-sm font-medium text-gray-900">
|
||
{t('common.filters') || 'Filtry'}
|
||
{hasActiveFilters && (
|
||
<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">
|
||
{getActiveFilterCount()}
|
||
</span>
|
||
)}
|
||
</h3>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
{hasActiveFilters && (
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
clearAllFilters();
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
{t('common.clearAll') || 'Wyczyść'}
|
||
</Button>
|
||
)}
|
||
<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>
|
||
</div>
|
||
</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 dark:text-gray-300 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 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 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 dark:text-gray-300 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 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 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 dark:text-gray-300 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 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 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 dark:text-gray-300 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 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 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 dark:text-gray-300 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 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 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 dark:text-gray-300 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 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 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>
|
||
{filteredProjects.length === 0 && searchTerm ? (
|
||
<Card>
|
||
<CardContent className="text-center py-12">
|
||
<div className="text-gray-400 mb-4">
|
||
<svg
|
||
className="w-16 h-16 mx-auto"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path
|
||
fillRule="evenodd"
|
||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||
clipRule="evenodd"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||
{t('common.noResults')}
|
||
</h3>
|
||
<p className="text-gray-500 mb-6">
|
||
{t('projects.noMatchingResults') || 'Brak projektów pasujących do kryteriów wyszukiwania. Spróbuj zmienić wyszukiwane frazy.'}
|
||
</p>
|
||
<Button variant="outline" onClick={() => setSearchTerm("")}>
|
||
{t('common.clearSearch') || 'Wyczyść wyszukiwanie'}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
) : projects.length === 0 ? (
|
||
<Card>
|
||
<CardContent className="text-center py-12">
|
||
<div className="text-gray-400 mb-4">
|
||
<svg
|
||
className="w-16 h-16 mx-auto"
|
||
fill="currentColor"
|
||
viewBox="0 0 20 20"
|
||
>
|
||
<path
|
||
fillRule="evenodd"
|
||
d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V8zm0 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1v-2z"
|
||
clipRule="evenodd"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||
{t('projects.noProjects')}
|
||
</h3>
|
||
<p className="text-gray-500 mb-6">
|
||
{t('projects.noProjectsMessage')}
|
||
</p>
|
||
<Link href="/projects/new">
|
||
<Button variant="primary">{t('projects.createFirstProject') || 'Utwórz pierwszy projekt'}</Button>
|
||
</Link>
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||
{/* Mobile scroll container */}
|
||
<div className="overflow-x-auto">
|
||
<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>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filteredProjects.map((project, index) => (
|
||
<tr
|
||
key={project.project_id}
|
||
className={`border-b dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors ${
|
||
index % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-25 dark:bg-gray-750"
|
||
}`}
|
||
>
|
||
<td className="px-1 py-3">
|
||
<Badge variant="secondary" size="sm" className="text-xs">
|
||
{project.project_number}
|
||
</Badge>
|
||
</td>
|
||
<td className="px-2 py-3 w-[200px] md:w-[250px]">
|
||
<Link
|
||
href={`/projects/${project.project_id}`}
|
||
className="font-medium text-blue-600 hover:text-blue-800 transition-colors text-sm truncate block"
|
||
title={project.project_name}
|
||
>
|
||
<span className="block sm:hidden">
|
||
{project.project_name.length > 20
|
||
? `${project.project_name.substring(0, 20)}...`
|
||
: project.project_name}
|
||
</span>
|
||
<span className="hidden sm:block">
|
||
{project.project_name}
|
||
</span>
|
||
</Link>
|
||
</td>
|
||
<td
|
||
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden lg:table-cell"
|
||
title={project.address}
|
||
>
|
||
{project.address || "N/A"}
|
||
</td>
|
||
<td
|
||
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden sm:table-cell"
|
||
title={project.wp}
|
||
>
|
||
{project.wp || "N/A"}
|
||
</td>
|
||
<td
|
||
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden md:table-cell"
|
||
title={project.city}
|
||
>
|
||
{project.city || "N/A"}
|
||
</td>
|
||
<td
|
||
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden sm:table-cell"
|
||
title={project.plot}
|
||
>
|
||
{project.plot || "N/A"}
|
||
</td>
|
||
<td
|
||
className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 truncate hidden md:table-cell"
|
||
title={project.finish_date}
|
||
>
|
||
{project.finish_date
|
||
? formatDate(project.finish_date)
|
||
: "N/A"}
|
||
</td>
|
||
<td className="px-2 py-3 text-xs text-gray-600 dark:text-gray-400 text-center font-semibold">
|
||
{project.project_type === "design"
|
||
? "P"
|
||
: project.project_type === "construction"
|
||
? "B"
|
||
: project.project_type === "design+construction"
|
||
? "P+B"
|
||
: "-"}
|
||
</td>
|
||
<td className="px-2 py-3 text-xs text-gray-600">
|
||
<div className="flex justify-center items-center h-full">
|
||
{project.project_status === 'registered' ? (
|
||
<span className="text-red-500 font-bold text-sm" title={t('projectStatus.registered')}>N</span>
|
||
) : project.project_status === 'in_progress_design' ? (
|
||
<span className="inline-block w-3 h-3 bg-blue-500 rounded-full" title={t('projectStatus.in_progress_design')}></span>
|
||
) : project.project_status === 'in_progress_construction' ? (
|
||
<span className="inline-block w-3 h-3 bg-yellow-400 rounded-full" title={t('projectStatus.in_progress_construction')}></span>
|
||
) : project.project_status === 'fulfilled' ? (
|
||
<span className="inline-block w-3 h-3 bg-green-500 rounded-full" title={t('projectStatus.fulfilled')}></span>
|
||
) : project.project_status === 'cancelled' ? (
|
||
<span className="text-red-500 font-bold text-lg" title={t('projectStatus.cancelled')}>×</span>
|
||
) : (
|
||
<span title="Unknown status">-</span>
|
||
)}
|
||
</div>
|
||
</td>
|
||
<td className="px-2 py-3">
|
||
{project.assigned_to_initial ? (
|
||
<div className="flex items-center justify-center">
|
||
<span className="inline-flex items-center justify-center w-8 h-8 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-xs font-semibold">
|
||
{project.assigned_to_initial}
|
||
</span>
|
||
</div>
|
||
) : (
|
||
<span className="text-xs text-gray-400">-</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</PageContainer>
|
||
);
|
||
}
|