'use client'; import { useState } from 'react'; import { Button } from '@/components/ui/button'; import { DataTable } from '@/components/common/data-table'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { projectService } from '@/lib/services/project.service'; import { ColumnDef } from '@tanstack/react-table'; import { Pencil, Trash, Plus, Search } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; import { toast } from 'sonner'; import apiClient from '@/lib/api/client'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { contractService } from '@/lib/services/contract.service'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Skeleton } from '@/components/ui/skeleton'; import { SearchContractDto, CreateContractDto, UpdateContractDto } from '@/types/dto/contract/contract.dto'; import { AxiosError } from 'axios'; import { Contract, getContractPublicId, getProjectPublicId } from '@/types/contract'; interface _Project { publicId: string; // ADR-019: uuid exposed as 'publicId' (string) projectCode: string; projectName: string; } const contractSchema = z.object({ contractCode: z.string().min(1, 'Contract Code is required'), contractName: z.string().min(1, 'Contract Name is required'), projectId: z.string().min(1, 'Project is required'), description: z.string().optional(), startDate: z.string().optional(), endDate: z.string().optional(), }); type ContractFormData = z.infer; const useContracts = (params?: SearchContractDto) => { return useQuery({ queryKey: ['contracts', params], queryFn: () => contractService.getAll(params), }); }; const useProjectsList = () => { return useQuery({ queryKey: ['projects-list'], queryFn: () => projectService.getAll(), }); }; export default function ContractsPage() { const [search, setSearch] = useState(''); const { data: contracts, isLoading } = useContracts({ search: search || undefined }); const { data: projects } = useProjectsList() as { data: _Project[] | undefined }; const queryClient = useQueryClient(); const createContract = useMutation({ mutationFn: (data: CreateContractDto) => apiClient.post('/contracts', data).then((res) => res.data), onSuccess: () => { toast.success('Contract created successfully'); queryClient.invalidateQueries({ queryKey: ['contracts'] }); setDialogOpen(false); }, onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || 'Failed to create contract'), }); const updateContract = useMutation({ mutationFn: ({ uuid, data }: { uuid: string; data: UpdateContractDto }) => apiClient.patch(`/contracts/${uuid}`, data).then((res) => res.data), onSuccess: () => { toast.success('Contract updated successfully'); queryClient.invalidateQueries({ queryKey: ['contracts'] }); setDialogOpen(false); }, onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || 'Failed to update contract'), }); const deleteContract = useMutation({ mutationFn: (uuid: string) => apiClient.delete(`/contracts/${uuid}`).then((res) => res.data), onSuccess: () => { toast.success('Contract deleted successfully'); queryClient.invalidateQueries({ queryKey: ['contracts'] }); }, onError: (err: AxiosError<{ message: string }>) => toast.error(err.response?.data?.message || 'Failed to delete contract'), }); const [dialogOpen, setDialogOpen] = useState(false); const [editingUuid, setEditingUuid] = useState(null); const [editingContract, setEditingContract] = useState(null); // Stats for Delete Dialog const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [contractToDelete, setContractToDelete] = useState(null); const handleDeleteClick = (contract: Contract) => { setContractToDelete(contract); setDeleteDialogOpen(true); }; const confirmDelete = () => { if (contractToDelete) { const contractUuid = getContractPublicId(contractToDelete); if (!contractUuid) { toast.error('Invalid contract UUID'); return; } deleteContract.mutate(contractUuid, { onSuccess: () => { setDeleteDialogOpen(false); setContractToDelete(null); }, }); } }; const { register, handleSubmit, reset, setValue, watch, formState: { errors }, } = useForm({ resolver: zodResolver(contractSchema), defaultValues: { contractCode: '', contractName: '', projectId: '', description: '', }, }); const columns: ColumnDef[] = [ { accessorKey: 'contractCode', header: 'Code', cell: ({ row }) => {row.original.contractCode}, }, { accessorKey: 'contractName', header: 'Name' }, { accessorKey: 'project.projectCode', header: 'Project', cell: ({ row }) => row.original.project?.projectCode || '-', }, { accessorKey: 'startDate', header: 'Start Date' }, { accessorKey: 'endDate', header: 'End Date' }, { id: 'actions', header: 'Actions', cell: ({ row }) => ( handleEdit(row.original)}> Edit handleDeleteClick(row.original)} > Delete ), }, ]; const handleEdit = (contract: Contract) => { const contractUuid = getContractPublicId(contract); setEditingUuid(contractUuid || null); setEditingContract(contract); // Store contract for caption display // ADR-019: resolve nested project UUID from canonical field const pId = getProjectPublicId(contract.project); reset({ contractCode: contract.contractCode, contractName: contract.contractName, projectId: pId, description: contract.description || '', startDate: contract.startDate ? new Date(contract.startDate).toISOString().split('T')[0] : '', endDate: contract.endDate ? new Date(contract.endDate).toISOString().split('T')[0] : '', }); setDialogOpen(true); }; const handleCreate = () => { setEditingUuid(null); setEditingContract(null); // Clear editing contract reset({ contractCode: '', contractName: '', projectId: '', description: '', startDate: '', endDate: '', }); setDialogOpen(true); }; const onSubmit = (data: ContractFormData) => { // ADR-019: Resolve projectId (ID or UUID) // ADR-019: projectId is now a UUID string — backend resolveProjectId handles both const submitData = { ...data, projectId: data.projectId, }; if (editingUuid) { updateContract.mutate({ uuid: editingUuid, data: submitData }); } else { createContract.mutate(submitData); } }; return (

Contracts

Manage construction contracts

setSearch(e.target.value)} className="pl-8 bg-background" />
{isLoading ? (
{[1, 2, 3, 4, 5].map((i) => (
))}
) : ( )} {editingUuid ? `Edit Contract: ${editingContract?.contractCode || '...'}` : 'New Contract'}
{errors.projectId &&

{errors.projectId.message}

}
{errors.contractCode &&

{errors.contractCode.message}

}
{errors.contractName &&

{errors.contractName.message}

}
Are you absolutely sure? This action cannot be undone. This will permanently delete the contract {contractToDelete?.contractCode} and remove it from the system. Cancel {deleteContract.isPending ? 'Deleting...' : 'Delete Contract'}
); }