Files
panel/src/app/contracts/page.js

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>
);
}