xtablo-source/ui/src/pages/devis.tsx
Arthur Belleville 00b1ee61a3
Add bookings.tsx
2025-09-15 09:01:08 +02:00

296 lines
9 KiB
TypeScript

import { NotebookPenIcon } from "lucide-react";
import {
AllCommunityModule,
ColDef,
ModuleRegistry,
themeQuartz,
} from "ag-grid-community";
import { AgGridReact } from "ag-grid-react";
import {
useDevisList,
useCreateDevis,
useDeleteDevis,
useUpdateDevis,
} from "@ui/hooks/devis";
import { useState } from "react";
import { Database } from "@ui/types/database.types";
import { CalendarDate, DateValue } from "@internationalized/date";
import { RowActionMenu } from "@ui/components/RowActionMenu";
import { CustomLoadingOverlay } from "@ui/components/CustomLoadingOverlay";
import {
EmptyState,
EmptyStateActions,
EmptyStateDescription,
EmptyStateHeading,
EmptyStateIcon,
} from "@ui/ui-library/empty-state";
import { CreateDevisModal } from "@ui/components/devis/CreateDevisModal";
import {
calculateTotal,
calculateTax,
exportDevisToPdf,
statusToText,
} from "@ui/utils/helpers";
import { ViewDevisModal } from "@ui/components/devis/ViewDevisModal";
import { Badge, BadgeColor } from "@ui/ui-library/badge";
import { Select, SelectButton } from "@ui/ui-library/select";
import { SelectListBox } from "@ui/ui-library/select";
import { SelectPopover } from "@ui/ui-library/select";
import { SelectListItem } from "@ui/ui-library/select";
ModuleRegistry.registerModules([AllCommunityModule]);
type Devis = Database["public"]["Tables"]["devis"]["Row"];
const statusToVariant: Record<Devis["status"], BadgeColor> = {
draft: "zinc",
sent: "indigo",
accepted: "green",
rejected: "red",
expired: "yellow",
};
export const DevisPage = () => {
const { data: devisData, isLoading } = useDevisList();
const createDevis = useCreateDevis();
const updateDevis = useUpdateDevis();
const deleteDevis = useDeleteDevis();
const [dueDateError, setDueDateError] = useState("");
const [selectedDevis, setSelectedDevis] = useState<Devis | null>(null);
const [isViewModalOpen, setIsViewModalOpen] = useState(false);
const validateDueDate = (date: DateValue, dueDate: DateValue) => {
if (dueDate.compare(date) < 0) {
return "La date d'échéance doit être postérieure à la date de création";
}
return "";
};
const handleCreate = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const payload = Object.fromEntries(new FormData(form));
const email = payload.client_email as string;
const date = payload.date as string;
const due_date = payload.due_date as string;
const notes = payload.notes as string;
const terms = payload.terms as string;
const amount = parseFloat(payload.amount as string) || 0;
const tax_rate = parseFloat(payload.tax_rate as string) || 0;
const tax = calculateTax(amount, tax_rate);
const total = calculateTotal(amount, tax);
const dueDateError = validateDueDate(
new CalendarDate(
parseInt(date.split("-")[0]),
parseInt(date.split("-")[1]) - 1,
parseInt(date.split("-")[2])
),
new CalendarDate(
parseInt(due_date.split("-")[0]),
parseInt(due_date.split("-")[1]) - 1,
parseInt(due_date.split("-")[2])
)
);
setDueDateError(dueDateError);
if (dueDateError) {
return;
}
createDevis.mutate({
client_email: email,
date,
due_date,
notes,
terms,
status: "draft",
subtotal: amount,
tax,
total,
items: [],
number: `DEV-${Date.now()}`,
});
form.reset();
};
// const handleEdit = (devis: Devis) => {
// console.log("Edit devis:", devis);
// };
const confirmDeleteAction = (devisId: string) => {
deleteDevis.mutate(devisId);
};
// Add handler for exporting
const handleExport = exportDevisToPdf;
const columnDefs: ColDef<Devis>[] = [
{
field: "date",
headerName: "Date",
valueFormatter: (params) => {
if (!params.value) return "";
return new Date(params.value).toLocaleDateString("fr-FR");
},
},
{ field: "client_email", headerName: "Client" },
{
field: "tax",
headerName: "TVA",
valueFormatter: (params) => {
if (params.value == null) return "";
return params.value.toFixed(2) + " €";
},
flex: 1,
},
{
field: "total",
headerName: "Montant",
valueFormatter: (params) => {
return params.value.toFixed(2) + " €";
},
},
{
field: "status",
headerName: "Status",
cellStyle: { padding: 4 },
cellRenderer: (params: { data: Devis }) => {
const currentStatus = params.data.status;
return (
<Select
className="flex flex-col justify-center"
aria-label="Status"
selectedKey={currentStatus}
onSelectionChange={(key) => {
updateDevis.mutate({
id: params.data.id,
status: key as Devis["status"],
});
}}
>
<SelectButton className="w-32 h-8" />
<SelectPopover>
<SelectListBox checkIconPlacement="start">
{Object.entries(statusToVariant).map(([status]) => (
<SelectListItem key={status} id={status} textValue={status}>
<Badge color={statusToVariant[status as Devis["status"]]}>
{statusToText[status as Devis["status"]]}
</Badge>
</SelectListItem>
))}
</SelectListBox>
</SelectPopover>
</Select>
);
},
},
{
headerName: "Actions",
pinned: "right",
width: 130,
cellStyle: { padding: 2 },
colId: "actions-column",
cellRenderer: (params: { data: Devis; node: { id: string | null } }) => {
if (!params.data) return null;
return (
<div className="flex justify-center items-center h-full">
<RowActionMenu
devis={params.data}
onDelete={confirmDeleteAction}
onExport={handleExport}
/>
</div>
);
},
lockPosition: true,
suppressNavigable: true,
suppressMovable: true,
filter: false,
sortable: false,
resizable: false,
suppressSizeToFit: true,
},
];
return (
<div className="min-h-screen">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Vos devis
</h1>
<CreateDevisModal
handleCreate={handleCreate}
dueDateError={dueDateError}
setDueDateError={setDueDateError}
/>
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
{devisData?.length === 0 ? (
<EmptyState className="h-screen">
<EmptyStateIcon>
<NotebookPenIcon strokeWidth="1" />
</EmptyStateIcon>
<EmptyStateHeading>Aucun devis</EmptyStateHeading>
<EmptyStateDescription>
Créez un nouveau devis pour commencer.
</EmptyStateDescription>
<EmptyStateActions>
<CreateDevisModal
handleCreate={handleCreate}
dueDateError={dueDateError}
setDueDateError={setDueDateError}
/>
</EmptyStateActions>
</EmptyState>
) : (
<div className="ag-theme-alpine dark:ag-theme-alpine-dark w-full h-[400px]">
<AgGridReact<Devis>
rowData={devisData}
loading={isLoading}
gridOptions={{
rowHeight: 44,
theme: themeQuartz,
onRowDoubleClicked: (event) => {
if (event.data) {
setSelectedDevis(event.data);
setIsViewModalOpen(true);
}
},
suppressHorizontalScroll: true,
domLayout: "autoHeight",
loadingOverlayComponent: CustomLoadingOverlay,
loadingOverlayComponentParams: {
loadingMessage: "Chargement des devis...",
},
defaultColDef: {
sortable: true,
filter: false,
flex: 1,
minWidth: 100,
resizable: false,
},
}}
columnDefs={columnDefs}
/>
</div>
)}
</div>
</div>
</main>
<ViewDevisModal
selectedDevis={selectedDevis as Devis}
isOpen={isViewModalOpen}
setIsOpen={setIsViewModalOpen}
/>
</div>
);
};