feat: Implement project search functionality and task management features
- Added search functionality to the Project List page, allowing users to filter projects by name, WP, plot, or investment number. - Created a new Project Tasks page to manage tasks across all projects, including filtering by status and priority. - Implemented task status updates and deletion functionality. - Added a new Task Template Edit page for modifying existing task templates. - Enhanced Task Template Form to include a description field and loading state during submission. - Updated UI components for better user experience, including badges for task status and priority. - Introduced new database queries for managing contracts and projects, including fetching tasks related to projects. - Added migrations to the database for new columns and improved data handling.
This commit is contained in:
206
src/app/contracts/[id]/page.js
Normal file
206
src/app/contracts/[id]/page.js
Normal file
@@ -0,0 +1,206 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
export default function ContractDetailsPage() {
|
||||
const params = useParams();
|
||||
const contractId = params.id;
|
||||
const [contract, setContract] = useState(null);
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchContractDetails() {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Fetch contract details
|
||||
const contractRes = await fetch(`/api/contracts/${contractId}`);
|
||||
if (contractRes.ok) {
|
||||
const contractData = await contractRes.json();
|
||||
setContract(contractData);
|
||||
}
|
||||
|
||||
// Fetch projects for this contract
|
||||
const projectsRes = await fetch(
|
||||
`/api/projects?contract_id=${contractId}`
|
||||
);
|
||||
if (projectsRes.ok) {
|
||||
const projectsData = await projectsRes.json();
|
||||
setProjects(projectsData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching contract details:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (contractId) {
|
||||
fetchContractDetails();
|
||||
}
|
||||
}, [contractId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
<div className="text-center">Ładowanie szczegółów umowy...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!contract) {
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
<div className="text-center text-red-600">Nie znaleziono umowy.</div>
|
||||
<div className="text-center mt-4">
|
||||
<Link href="/contracts" className="text-blue-600 hover:underline">
|
||||
← Powrót do listy umów
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-2xl font-bold">Umowa {contract.contract_number}</h1>
|
||||
<Link href="/contracts" className="text-blue-600 hover:underline">
|
||||
← Powrót do listy umów
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Contract Details */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Szczegóły umowy</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Numer umowy
|
||||
</label>
|
||||
<p className="text-lg">{contract.contract_number}</p>
|
||||
</div>
|
||||
|
||||
{contract.contract_name && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Nazwa umowy
|
||||
</label>
|
||||
<p className="text-lg">{contract.contract_name}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contract.customer_contract_number && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Numer umowy (Klienta)
|
||||
</label>
|
||||
<p>{contract.customer_contract_number}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contract.customer && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Zleceniodawca
|
||||
</label>
|
||||
<p>{contract.customer}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contract.investor && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Inwestor
|
||||
</label>
|
||||
<p>{contract.investor}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contract.date_signed && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Data zawarcia
|
||||
</label>
|
||||
<p>
|
||||
{new Date(contract.date_signed).toLocaleDateString("pl-PL")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contract.finish_date && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Data zakończenia
|
||||
</label>
|
||||
<p>
|
||||
{new Date(contract.finish_date).toLocaleDateString("pl-PL")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Linked Projects */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Projekty w ramach umowy ({projects.length})
|
||||
</h2>
|
||||
<Link
|
||||
href={`/projects/new?contract_id=${contractId}`}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
>
|
||||
➕ Dodaj projekt
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-8">
|
||||
Brak projektów przypisanych do tej umowy.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{projects.map((project) => (
|
||||
<div
|
||||
key={project.project_id}
|
||||
className="border border-gray-100 rounded p-4 hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium">{project.project_name}</h3>
|
||||
<p className="text-sm text-gray-600">
|
||||
{project.project_number}
|
||||
</p>
|
||||
{project.address && (
|
||||
<p className="text-sm text-gray-500">
|
||||
📍 {project.address}
|
||||
</p>
|
||||
)}
|
||||
{project.finish_date && (
|
||||
<p className="text-sm text-gray-500">
|
||||
⏰ Termin:{" "}
|
||||
{new Date(project.finish_date).toLocaleDateString(
|
||||
"pl-PL"
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href={`/projects/${project.project_id}`}
|
||||
className="text-blue-600 hover:underline text-sm"
|
||||
>
|
||||
Szczegóły →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
632
src/app/contracts/page.js
Normal file
632
src/app/contracts/page.js
Normal file
@@ -0,0 +1,632 @@
|
||||
"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";
|
||||
|
||||
export default function ContractsMainPage() {
|
||||
const [contracts, setContracts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filteredContracts, setFilteredContracts] = useState([]);
|
||||
const [sortBy, setSortBy] = useState("contract_number");
|
||||
const [sortOrder, setSortOrder] = useState("asc");
|
||||
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?.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 === "completed" && contract.finish_date) {
|
||||
return new Date(contract.finish_date) < currentDate;
|
||||
} else if (statusFilter === "no_end_date") {
|
||||
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 completed = 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, completed, 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" : "completed";
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return <Badge variant="success">Aktywna</Badge>;
|
||||
case "completed":
|
||||
return <Badge variant="secondary">Zakończona</Badge>;
|
||||
case "ongoing":
|
||||
return <Badge variant="primary">W trakcie</Badge>;
|
||||
default:
|
||||
return <Badge>Nieznany</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 (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-2 text-gray-600">Ładowanie umów...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const stats = getContractStats();
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Umowy</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Zarządzaj swoimi umowami i kontraktami
|
||||
</p>
|
||||
</div>{" "}
|
||||
<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"
|
||||
>
|
||||
<span className="mr-2">➕</span>
|
||||
Nowa umowa
|
||||
</Link>
|
||||
</div>
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<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">Zakończone</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{stats.completed}
|
||||
</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>
|
||||
{/* Filters and Search */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col lg:flex-row gap-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="search"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Wyszukaj
|
||||
</label>
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
placeholder="Szukaj po numerze, nazwie, kliencie lub inwestorze..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="w-full lg:w-48">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Wszystkie</option>
|
||||
<option value="active">Aktywne</option>
|
||||
<option value="completed">Zakończone</option>
|
||||
<option value="no_end_date">W trakcie</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
<div className="w-full lg:w-48">
|
||||
<label
|
||||
htmlFor="sort"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Sortuj według
|
||||
</label>
|
||||
<select
|
||||
id="sort"
|
||||
value={`${sortBy}-${sortOrder}`}
|
||||
onChange={(e) => {
|
||||
const [field, order] = e.target.value.split("-");
|
||||
setSortBy(field);
|
||||
setSortOrder(order);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="contract_number-asc">Numer umowy (A-Z)</option>
|
||||
<option value="contract_number-desc">Numer umowy (Z-A)</option>
|
||||
<option value="contract_name-asc">Nazwa (A-Z)</option>
|
||||
<option value="contract_name-desc">Nazwa (Z-A)</option>
|
||||
<option value="customer-asc">Klient (A-Z)</option>
|
||||
<option value="customer-desc">Klient (Z-A)</option>
|
||||
<option value="date_signed-desc">
|
||||
Data zawarcia (najnowsze)
|
||||
</option>
|
||||
<option value="date_signed-asc">
|
||||
Data zawarcia (najstarsze)
|
||||
</option>
|
||||
<option value="finish_date-desc">
|
||||
Data zakończenia (najnowsze)
|
||||
</option>
|
||||
<option value="finish_date-asc">
|
||||
Data zakończenia (najstarsze)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>{" "}
|
||||
{/* 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">
|
||||
{new Date(
|
||||
contract.date_signed
|
||||
).toLocaleDateString("pl-PL")}
|
||||
</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">
|
||||
{new Date(
|
||||
contract.finish_date
|
||||
).toLocaleDateString("pl-PL")}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import db from "@/lib/db";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const contracts = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT contract_id, contract_number, contract_name FROM contracts
|
||||
`
|
||||
)
|
||||
.all();
|
||||
return NextResponse.json(contracts);
|
||||
}
|
||||
|
||||
export async function POST(req) {
|
||||
const data = await req.json();
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO contracts (
|
||||
contract_number,
|
||||
contract_name,
|
||||
customer_contract_number,
|
||||
customer,
|
||||
investor,
|
||||
date_signed,
|
||||
finish_date
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
).run(
|
||||
data.contract_number,
|
||||
data.contract_name,
|
||||
data.customer_contract_number,
|
||||
data.customer,
|
||||
data.investor,
|
||||
data.date_signed,
|
||||
data.finish_date
|
||||
);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
Reference in New Issue
Block a user