626 lines
19 KiB
JavaScript
626 lines
19 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 PageContainer from "@/components/ui/PageContainer";
|
|
import PageHeader from "@/components/ui/PageHeader";
|
|
import SearchBar from "@/components/ui/SearchBar";
|
|
import FilterBar from "@/components/ui/FilterBar";
|
|
import { LoadingState } from "@/components/ui/States";
|
|
import { formatDate } from "@/lib/utils";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
|
|
export default function ContractsMainPage() {
|
|
const { t } = useTranslation();
|
|
const [contracts, setContracts] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [filteredContracts, setFilteredContracts] = useState([]);
|
|
const [sortBy, setSortBy] = useState("date_signed");
|
|
const [sortOrder, setSortOrder] = useState("desc");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
|
|
useEffect(() => {
|
|
async function fetchContracts() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/contracts");
|
|
const data = await res.json();
|
|
setContracts(data);
|
|
setFilteredContracts(data);
|
|
} catch (error) {
|
|
console.error("Error fetching contracts:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
fetchContracts();
|
|
}, []);
|
|
// Filter and sort contracts
|
|
useEffect(() => {
|
|
let filtered = [...contracts];
|
|
|
|
// Apply search filter
|
|
if (searchTerm) {
|
|
filtered = filtered.filter(
|
|
(contract) =>
|
|
contract.contract_number
|
|
?.toLowerCase()
|
|
.includes(searchTerm.toLowerCase()) ||
|
|
contract.contract_name
|
|
?.toLowerCase()
|
|
.includes(searchTerm.toLowerCase()) ||
|
|
contract.customer_contract_number
|
|
?.toLowerCase()
|
|
.includes(searchTerm.toLowerCase()) ||
|
|
contract.customer?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
contract.investor?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Apply status filter
|
|
if (statusFilter !== "all") {
|
|
const currentDate = new Date();
|
|
filtered = filtered.filter((contract) => {
|
|
if (statusFilter === "active" && contract.finish_date) {
|
|
return new Date(contract.finish_date) >= currentDate;
|
|
} else if (statusFilter === "expired" && contract.finish_date) {
|
|
return new Date(contract.finish_date) < currentDate;
|
|
} else if (statusFilter === "ongoing") {
|
|
return !contract.finish_date;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Apply sorting
|
|
filtered.sort((a, b) => {
|
|
let aVal = a[sortBy] || "";
|
|
let bVal = b[sortBy] || "";
|
|
|
|
if (sortBy.includes("date")) {
|
|
aVal = new Date(aVal || "1900-01-01");
|
|
bVal = new Date(bVal || "1900-01-01");
|
|
}
|
|
|
|
if (aVal < bVal) return sortOrder === "asc" ? -1 : 1;
|
|
if (aVal > bVal) return sortOrder === "asc" ? 1 : -1;
|
|
return 0;
|
|
});
|
|
setFilteredContracts(filtered);
|
|
}, [searchTerm, contracts, sortBy, sortOrder, statusFilter]);
|
|
|
|
async function handleDelete(id) {
|
|
const confirmed = confirm("Czy na pewno chcesz usunąć tę umowę?");
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/contracts/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (res.ok) {
|
|
setContracts(contracts.filter((c) => c.contract_id !== id));
|
|
} else {
|
|
alert("Błąd podczas usuwania umowy.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting contract:", error);
|
|
alert("Błąd podczas usuwania umowy.");
|
|
}
|
|
}
|
|
|
|
// Get contract statistics
|
|
const getContractStats = () => {
|
|
const currentDate = new Date();
|
|
const total = contracts.length;
|
|
const active = contracts.filter(
|
|
(c) => !c.finish_date || new Date(c.finish_date) >= currentDate
|
|
).length;
|
|
const expired = contracts.filter(
|
|
(c) => c.finish_date && new Date(c.finish_date) < currentDate
|
|
).length;
|
|
const withoutEndDate = contracts.filter((c) => !c.finish_date).length;
|
|
|
|
return { total, active, expired, withoutEndDate };
|
|
};
|
|
|
|
const getContractStatus = (contract) => {
|
|
if (!contract.finish_date) return "ongoing";
|
|
const currentDate = new Date();
|
|
const finishDate = new Date(contract.finish_date);
|
|
return finishDate >= currentDate ? "active" : "expired";
|
|
};
|
|
|
|
const getStatusBadge = (status) => {
|
|
switch (status) {
|
|
case "active":
|
|
return <Badge variant="success">{t('contracts.active')}</Badge>;
|
|
case "expired":
|
|
return <Badge variant="danger">{t('contracts.expired')}</Badge>;
|
|
case "ongoing":
|
|
return <Badge variant="primary">{t('contracts.withoutEndDate')}</Badge>;
|
|
default:
|
|
return <Badge>{t('common.unknown')}</Badge>;
|
|
}
|
|
};
|
|
|
|
async function handleDelete(id) {
|
|
const confirmed = confirm("Czy na pewno chcesz usunąć tę umowę?");
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/contracts/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (res.ok) {
|
|
setContracts(contracts.filter((c) => c.contract_id !== id));
|
|
} else {
|
|
alert("Błąd podczas usuwania umowy.");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting contract:", error);
|
|
alert("Błąd podczas usuwania umowy.");
|
|
}
|
|
}
|
|
|
|
const handleSearchChange = (e) => {
|
|
setSearchTerm(e.target.value);
|
|
};
|
|
if (loading) {
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader
|
|
title={t('contracts.title')}
|
|
description={t('contracts.subtitle')}
|
|
>
|
|
<Link href="/contracts/new">
|
|
<Button variant="primary" size="lg">
|
|
<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('contracts.newContract')}
|
|
</Button>
|
|
</Link>
|
|
</PageHeader>
|
|
<LoadingState message={t('navigation.loading')} />
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
const stats = getContractStats();
|
|
|
|
const filterOptions = [
|
|
{
|
|
label: "Status",
|
|
value: statusFilter,
|
|
onChange: (e) => setStatusFilter(e.target.value),
|
|
options: [
|
|
{ value: "all", label: "Wszystkie" },
|
|
{ value: "active", label: "Aktywne" },
|
|
{ value: "expired", label: "Przeterminowane" },
|
|
{ value: "ongoing", label: "W trakcie" },
|
|
],
|
|
},
|
|
{
|
|
label: "Sortuj według",
|
|
value: sortBy,
|
|
onChange: (e) => setSortBy(e.target.value),
|
|
options: [
|
|
{ value: "contract_number", label: "Numer umowy" },
|
|
{ value: "contract_name", label: "Nazwa umowy" },
|
|
{ value: "customer", label: "Klient" },
|
|
{ value: "date_signed", label: "Data podpisania" },
|
|
{ value: "finish_date", label: "Data zakończenia" },
|
|
],
|
|
},
|
|
{
|
|
label: "Kolejność",
|
|
value: sortOrder,
|
|
onChange: (e) => setSortOrder(e.target.value),
|
|
options: [
|
|
{ value: "asc", label: "Rosnąco" },
|
|
{ value: "desc", label: "Malejąco" },
|
|
],
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageContainer>
|
|
<PageHeader
|
|
title={t('contracts.title')}
|
|
description={t('contracts.subtitle')}
|
|
>
|
|
<Link href="/contracts/new">
|
|
<Button variant="primary" size="lg">
|
|
<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('contracts.newContract')}
|
|
</Button>
|
|
</Link>{" "}
|
|
</PageHeader>
|
|
{/* Statistics Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-blue-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-blue-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Wszystkie</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{stats.total}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-green-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-green-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Aktywne</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{stats.active}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-gray-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-gray-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M5 13l4 4L19 7"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Przeterminowane</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{stats.expired}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center">
|
|
<div className="p-2 bg-yellow-100 rounded-lg">
|
|
<svg
|
|
className="w-6 h-6 text-yellow-600"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">W trakcie</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{stats.withoutEndDate}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>{" "}
|
|
</Card>
|
|
</div>
|
|
<SearchBar
|
|
searchTerm={searchTerm}
|
|
onSearchChange={handleSearchChange}
|
|
placeholder="Szukaj umów po numerze, nazwie, kliencie lub inwestorze..."
|
|
resultsCount={filteredContracts.length}
|
|
resultsText="umów"
|
|
/>
|
|
<FilterBar filters={filterOptions} className="mb-6" />{" "}
|
|
{/* Contracts List */}
|
|
{filteredContracts.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-8 text-center">
|
|
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
|
<svg
|
|
className="w-8 h-8 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
{searchTerm || statusFilter !== "all"
|
|
? "Brak pasujących umów"
|
|
: "Brak umów"}
|
|
</h3>
|
|
<p className="text-gray-500 mb-4">
|
|
{searchTerm || statusFilter !== "all"
|
|
? "Spróbuj zmienić kryteria wyszukiwania lub filtry."
|
|
: "Rozpocznij od dodania pierwszej umowy."}
|
|
</p>{" "}
|
|
{!searchTerm && statusFilter === "all" && (
|
|
<Link
|
|
href="/contracts/new"
|
|
className="inline-flex items-center justify-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
>
|
|
Dodaj pierwszą umowę
|
|
</Link>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{filteredContracts.map((contract) => {
|
|
const status = getContractStatus(contract);
|
|
return (
|
|
<Card
|
|
key={contract.contract_id}
|
|
className="hover:shadow-md transition-shadow"
|
|
>
|
|
<CardContent className="p-6">
|
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
|
{contract.contract_number}
|
|
</h3>
|
|
{getStatusBadge(status)}
|
|
{contract.contract_name && (
|
|
<span className="text-gray-600 truncate">
|
|
— {contract.contract_name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Details Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 text-sm">
|
|
{contract.customer && (
|
|
<div className="flex items-center gap-2">
|
|
<svg
|
|
className="w-4 h-4 text-gray-400"
|
|
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>
|
|
<span className="font-medium text-gray-700">
|
|
Zleceniodawca:
|
|
</span>
|
|
<span className="text-gray-600 truncate">
|
|
{contract.customer}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{contract.investor && (
|
|
<div className="flex items-center gap-2">
|
|
<svg
|
|
className="w-4 h-4 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
|
/>
|
|
</svg>
|
|
<span className="font-medium text-gray-700">
|
|
Inwestor:
|
|
</span>
|
|
<span className="text-gray-600 truncate">
|
|
{contract.investor}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{contract.date_signed && (
|
|
<div className="flex items-center gap-2">
|
|
<svg
|
|
className="w-4 h-4 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
/>
|
|
</svg>
|
|
<span className="font-medium text-gray-700">
|
|
Zawarcie:
|
|
</span>{" "}
|
|
<span className="text-gray-600">
|
|
{formatDate(contract.date_signed)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{contract.finish_date && (
|
|
<div className="flex items-center gap-2">
|
|
<svg
|
|
className="w-4 h-4 text-gray-400"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<span className="font-medium text-gray-700">
|
|
Zakończenie:
|
|
</span>{" "}
|
|
<span className="text-gray-600">
|
|
{formatDate(contract.finish_date)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>{" "}
|
|
{/* Actions */}
|
|
<div className="flex gap-2 flex-shrink-0">
|
|
<Link
|
|
href={`/contracts/${contract.contract_id}`}
|
|
className="inline-flex items-center justify-center px-3 py-1.5 text-sm border border-blue-600 text-blue-600 hover:bg-blue-50 rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
>
|
|
<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 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
/>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
/>
|
|
</svg>
|
|
Szczegóły
|
|
</Link>
|
|
<Button
|
|
variant="danger"
|
|
size="sm"
|
|
onClick={() => handleDelete(contract.contract_id)}
|
|
>
|
|
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
Usuń
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{/* Results Summary */}
|
|
{filteredContracts.length > 0 && (
|
|
<div className="mt-6 text-center">
|
|
<p className="text-sm text-gray-500">
|
|
Wyświetlono {filteredContracts.length} z {contracts.length} umów
|
|
{(searchTerm || statusFilter !== "all") && (
|
|
<button
|
|
onClick={() => {
|
|
setSearchTerm("");
|
|
setStatusFilter("all");
|
|
}}
|
|
className="ml-2 text-blue-600 hover:text-blue-800 underline"
|
|
>
|
|
Wyczyść filtry
|
|
</button>
|
|
)}
|
|
</p>{" "}
|
|
</div>
|
|
)}
|
|
</PageContainer>
|
|
);
|
|
}
|