Add empty state + fix logout issue

This commit is contained in:
Arthur Belleville 2025-04-13 11:22:50 +02:00
parent bbe02bff13
commit e5c0cab7e9
No known key found for this signature in database
4 changed files with 258 additions and 97 deletions

View file

@ -0,0 +1,35 @@
import React from "react";
import icon from "../assets/icon.jpg"; // Import the image
// Define props type (adjust if needed)
interface CustomLoadingOverlayProps {
loadingMessage?: string;
}
export const CustomLoadingOverlay: React.FC<CustomLoadingOverlayProps> = ({
loadingMessage = "Loading...",
}) => {
return (
<div className="ag-overlay-loading-center" role="presentation">
<div
style={{
display: "flex",
flexDirection: "column", // Arrange icon and text vertically
alignItems: "center",
justifyContent: "center",
}}
>
{/* Use the imported image */}
<img
src={icon}
alt="Loading icon"
className="animate-spin h-10 w-10" // Apply spin animation and size
/>
{/* Display the loading message passed via props */}
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
{loadingMessage}
</p>
</div>
</div>
);
};

View file

@ -0,0 +1,48 @@
import React from "react";
import { Button } from "@ui/ui-library/button";
import { PencilIcon, TrashIcon } from "lucide-react";
import { Database } from "@ui/types/db";
type Devis = Database["public"]["Tables"]["devis"]["Row"];
interface RowActionMenuProps {
devis: Devis;
onEdit: (devis: Devis) => void;
onDelete: (devis: Devis) => void;
}
export const RowActionMenu: React.FC<RowActionMenuProps> = ({
devis,
onEdit,
onDelete,
}) => {
return (
<div
className={`
w-full h-full
flex items-center justify-center p-1 space-x-1
bg-transparent
`}
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
>
<Button
variant="outline"
size="sm"
onPress={() => onEdit(devis)}
aria-label="Modifier le devis"
>
<PencilIcon className="w-4 h-4" />
</Button>
<Button
variant="outline"
color="destructive"
size="sm"
onPress={() => onDelete(devis)}
aria-label="Supprimer le devis"
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
);
};

View file

@ -8,6 +8,7 @@ import {
Session,
createClient,
} from "@supabase/supabase-js";
import { queryClient } from "@ui/lib/api";
export type User = SupabaseUser & {
user_metadata: {
@ -153,6 +154,7 @@ export function useLogout() {
mutationFn: async () => {
const { error } = await supabase.auth.signOut();
if (error) throw error;
queryClient.removeQueries();
},
});
}

View file

@ -1,7 +1,8 @@
import { Button } from "@ui/ui-library/button";
import { PlusIcon, TrashIcon } from "lucide-react";
import { NotebookPenIcon, PlusIcon } from "lucide-react";
import {
AllCommunityModule,
ColDef,
ModuleRegistry,
themeQuartz,
} from "ag-grid-community";
@ -28,6 +29,15 @@ import {
import { CalendarDate, DateValue } from "@internationalized/date";
import { Form } from "@ui/ui-library/form";
import { DateField, DateInput } from "@ui/ui-library/date-field";
import { RowActionMenu } from "@ui/components/RowActionMenu";
import { CustomLoadingOverlay } from "@ui/components/CustomLoadingOverlay";
import {
EmptyState,
EmptyStateActions,
EmptyStateDescription,
EmptyStateHeading,
EmptyStateIcon,
} from "@ui/ui-library/empty-state";
ModuleRegistry.registerModules([AllCommunityModule]);
@ -55,6 +65,9 @@ export const DevisPage = () => {
const [dueDateError, setDueDateError] = useState("");
const [selectedDevis, setSelectedDevis] = useState<Devis | null>(null);
const [formData, setFormData] = useState(defaultFormData);
const [devisToDelete, setDevisToDelete] = useState<Devis | null>(null);
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
useState(false);
const validateDueDate = (date: DateValue, dueDate: DateValue) => {
if (dueDate.compare(date) < 0) {
@ -123,26 +136,89 @@ export const DevisPage = () => {
setFormData(defaultFormData);
};
const confirmDelete = (devisId: string) => {
deleteDevis.mutate(devisId);
const handleEdit = (devis: Devis) => {
console.log("Edit devis:", devis);
};
if (isLoading) {
return <div>Loading...</div>;
}
const handleDelete = (devis: Devis) => {
setDevisToDelete(devis);
setIsConfirmDeleteModalOpen(true);
};
const confirmDeleteAction = () => {
if (devisToDelete) {
deleteDevis.mutate(devisToDelete.id);
setDevisToDelete(null);
setIsConfirmDeleteModalOpen(false);
}
};
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" },
{
headerName: "Actions",
pinned: "right",
width: 80,
cellStyle: { padding: 0 },
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}
onEdit={handleEdit}
onDelete={handleDelete}
/>
</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">
Devis
Vos devis
</h1>
<DialogTrigger>
<Button
variant="solid"
color="accent"
className="px-4"
className="px-4 bg-sky-900 hover:bg-accent-200 dark:bg-accent-900 dark:hover:bg-accent-800"
aria-label="Créer un nouveau devis"
>
<PlusIcon />
@ -301,100 +377,62 @@ export const DevisPage = () => {
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">
<div className="container mx-auto">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div
className="ag-theme-alpine dark:ag-theme-alpine-dark w-full"
style={{ height: 700 }}
>
<AgGridReact<Devis>
rowData={devisData}
gridOptions={{
theme: themeQuartz,
onRowDoubleClicked: (event) => {
if (event.data) {
setSelectedDevis(event.data);
}
},
domLayout: "autoHeight",
suppressHorizontalScroll: true,
defaultColDef: {
resizable: false,
sortable: true,
filter: false,
flex: 1,
minWidth: 100,
},
}}
columnDefs={[
{
field: "date",
headerName: "Date",
valueFormatter: (params) => {
if (!params.value) return "";
return new Date(params.value).toLocaleDateString("fr-FR");
{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>
<DialogTrigger>
<Button>Créer un nouveau devis</Button>
</DialogTrigger>
</EmptyStateActions>
</EmptyState>
) : (
<div className="ag-theme-alpine dark:ag-theme-alpine-dark w-full h-[400px]">
<AgGridReact<Devis>
rowData={devisData}
loading={isLoading}
gridOptions={{
theme: themeQuartz,
onRowDoubleClicked: (event) => {
if (event.data) {
setSelectedDevis(event.data);
}
},
},
{ field: "client_email", headerName: "Client" },
{
field: "total",
headerName: "Montant",
valueFormatter: (params) => {
return params.value.toFixed(2) + " €";
suppressHorizontalScroll: true,
domLayout: "autoHeight",
loadingOverlayComponent: CustomLoadingOverlay,
loadingOverlayComponentParams: {
loadingMessage: "Chargement des devis...",
},
},
{ field: "status", headerName: "Status" },
{
headerName: "Actions",
cellRenderer: (params: { data: Devis }) => (
<DialogTrigger>
<Button
variant="plain"
color="destructive"
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-200 rounded-full"
aria-label="Supprimer le devis"
name="Supprimer le devis"
>
<TrashIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
</Button>
<Modal size="sm" isDismissable>
<Dialog>
<DialogHeader>
<h2 className="text-xl font-semibold text-red-600">
Supprimer le devis
</h2>
<DialogCloseButton />
</DialogHeader>
<DialogBody>
<p className="text-gray-600 dark:text-gray-300">
Êtes-vous sûr de vouloir supprimer ce devis ?
Cette action est irréversible.
</p>
</DialogBody>
<DialogFooter>
<DialogCloseButton variant="outline">
Annuler
</DialogCloseButton>
<Button
variant="solid"
color="destructive"
onPress={() => confirmDelete(params.data.id)}
>
Supprimer
</Button>
</DialogFooter>
</Dialog>
</Modal>
</DialogTrigger>
),
},
]}
/>
</div>
defaultColDef: {
sortable: true,
filter: false,
flex: 1,
minWidth: 100,
resizable: false,
},
}}
columnDefs={columnDefs}
/>
</div>
)}
</div>
</div>
</main>
<Modal size="lg" isOpen={!!selectedDevis} isDismissable>
<Modal
size="lg"
isOpen={!!selectedDevis}
onOpenChange={(isOpen) => !isOpen && setSelectedDevis(null)}
isDismissable
>
<Dialog>
<DialogHeader>
<h2 className="text-xl font-semibold">
@ -490,6 +528,44 @@ export const DevisPage = () => {
</DialogFooter>
</Dialog>
</Modal>
<Modal
size="sm"
isOpen={isConfirmDeleteModalOpen}
onOpenChange={setIsConfirmDeleteModalOpen}
isDismissable
>
<Dialog>
<DialogHeader>
<h2 className="text-xl font-semibold text-red-600">
Supprimer le devis
</h2>
<DialogCloseButton
onPress={() => setIsConfirmDeleteModalOpen(false)}
/>
</DialogHeader>
<DialogBody>
<p className="text-gray-600 dark:text-gray-300">
Êtes-vous sûr de vouloir supprimer ce devis ? Cette action est
irréversible.
</p>
</DialogBody>
<DialogFooter>
<Button
variant="outline"
onPress={() => setIsConfirmDeleteModalOpen(false)}
>
Annuler
</Button>
<Button
variant="solid"
color="destructive"
onPress={confirmDeleteAction}
>
Supprimer
</Button>
</DialogFooter>
</Dialog>
</Modal>
</div>
);
};