Add empty state + fix logout issue
This commit is contained in:
parent
bbe02bff13
commit
e5c0cab7e9
4 changed files with 258 additions and 97 deletions
35
ui/src/components/CustomLoadingOverlay.tsx
Normal file
35
ui/src/components/CustomLoadingOverlay.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
48
ui/src/components/RowActionMenu.tsx
Normal file
48
ui/src/components/RowActionMenu.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue