Add status selection

This commit is contained in:
Arthur Belleville 2025-04-14 08:32:45 +02:00
parent d1b97ee909
commit ca41b24d83
No known key found for this signature in database
7 changed files with 157 additions and 14 deletions

View file

@ -55,8 +55,8 @@ export const CreateDevisModal = ({
Nouveau Devis
</Button>
<Modal size="lg" isDismissable isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog>
<DialogHeader>
<Dialog aria-label="Créer un nouveau devis">
<DialogHeader slot="title">
<h2 className="text-xl font-semibold">Créer un nouveau devis</h2>
<DialogCloseButton />
</DialogHeader>

View file

@ -30,7 +30,7 @@ export const DeleteDevisModalButton = ({
<TrashIcon className="w-4 h-4" />
</Button>
<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog>
<Dialog aria-label="Supprimer le devis">
<DialogHeader slot="title">
<h2 className="text-xl font-semibold text-red-600">
Supprimer le devis

View file

@ -47,7 +47,7 @@ export const ViewDevisModal = ({
}) => {
return (
<Modal size="lg" isOpen={isOpen} onOpenChange={setIsOpen} isDismissable>
<Dialog>
<Dialog aria-label="Voir le devis">
<DialogHeader slot="title">
<h2 className="text-xl font-semibold">
Devis {selectedDevis?.number}

View file

@ -1,7 +1,12 @@
import { screen, waitFor, within } from "@testing-library/react";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { DevisPage } from "@ui/pages/devis";
import { useDevisList, useCreateDevis, useDeleteDevis } from "@ui/hooks/devis";
import {
useDevisList,
useCreateDevis,
useDeleteDevis,
useUpdateDevis,
} from "@ui/hooks/devis";
import userEvent from "@testing-library/user-event";
import {
renderWithProviders,
@ -13,6 +18,7 @@ vi.mock("@ui/hooks/devis");
const mockUseDevisList = useDevisList as ReturnType<typeof vi.fn>;
const mockUseCreateDevis = useCreateDevis as ReturnType<typeof vi.fn>;
const mockUseDeleteDevis = useDeleteDevis as ReturnType<typeof vi.fn>;
const mockUseUpdateDevis = useUpdateDevis as ReturnType<typeof vi.fn>;
// Mock data
const mockDevis = {
@ -58,6 +64,11 @@ describe("DevisPage", () => {
mockUseDeleteDevis.mockReturnValue({
mutate: vi.fn(),
});
// Setup mock for updateDevis
mockUseUpdateDevis.mockReturnValue({
mutate: vi.fn(),
});
});
it("renders the devis page", async () => {
@ -164,4 +175,80 @@ describe("DevisPage", () => {
expect(dialog.getByText("20.00 €")).toBeInTheDocument();
expect(dialog.getByText("120.00 €")).toBeInTheDocument();
});
describe("Status Column", () => {
it("renders the initial status badge correctly", async () => {
renderWithProviders(<DevisPage />);
await waitForGridToBeInTheDOM();
const grid = await screen.findByRole("grid");
// Find the button by its accessible name (rendered text)
const selectButton = within(grid).getByRole("button", {
name: "Brouillon Status", // Use text from statusToText for 'draft'
});
expect(selectButton).toBeInTheDocument();
// Check the badge is inside the button
const badge = within(selectButton).getByText("Brouillon");
expect(badge).toBeInTheDocument();
});
it("opens the status select popover with correct options on click", async () => {
renderWithProviders(<DevisPage />);
await waitForGridToBeInTheDOM();
const grid = await screen.findByRole("grid");
// Find the button by its accessible name
const selectButton = within(grid).getByRole("button", {
name: "Brouillon Status",
});
await userEvent.click(selectButton);
// Popover is usually in a portal, search in the document body
const listBox = await screen.findByRole("listbox");
expect(listBox).toBeInTheDocument();
// Check for expected status options (use statusToText values)
expect(within(listBox).getByText("Brouillon")).toBeInTheDocument();
expect(within(listBox).getByText("Envoyé")).toBeInTheDocument(); // Assuming 'sent' maps to 'Envoyé'
expect(within(listBox).getByText("Accepté")).toBeInTheDocument(); // Assuming 'accepted' maps to 'Accepté'
expect(within(listBox).getByText("Rejeté")).toBeInTheDocument(); // Assuming 'rejected' maps to 'Rejeté'
expect(within(listBox).getByText("Expiré")).toBeInTheDocument(); // Assuming 'expired' maps to 'Expiré'
// Add check for 'in-progress' if needed
// expect(within(listBox).getByText("En cours")).toBeInTheDocument();
});
it("calls update mutation when a new status is selected", async () => {
const mockUpdateMutate = vi.fn();
mockUseUpdateDevis.mockReturnValue({ mutate: mockUpdateMutate });
renderWithProviders(<DevisPage />);
await waitForGridToBeInTheDOM();
const grid = await screen.findByRole("grid");
// Find the button by its accessible name
const selectButton = within(grid).getByRole("button", {
name: "Brouillon Status",
});
// Open popover
await userEvent.click(selectButton);
// Select a new status (e.g., 'Sent')
const listBox = await screen.findByRole("listbox");
// Find the option by its text
const sentOption = within(listBox).getByRole("option", {
name: "Envoyé",
});
await userEvent.click(sentOption);
// Check if mutation was called correctly
expect(mockUpdateMutate).toHaveBeenCalledTimes(1);
expect(mockUpdateMutate).toHaveBeenCalledWith({
id: mockDevis.id, // Make sure mockDevis has the correct id
status: "sent", // The key/value selected
});
});
});
});

View file

@ -6,7 +6,12 @@ import {
themeQuartz,
} from "ag-grid-community";
import { AgGridReact } from "ag-grid-react";
import { useDevisList, useCreateDevis, useDeleteDevis } from "@ui/hooks/devis";
import {
useDevisList,
useCreateDevis,
useDeleteDevis,
useUpdateDevis,
} from "@ui/hooks/devis";
import { useState } from "react";
import { Database } from "@ui/types/db";
import { CalendarDate, DateValue } from "@internationalized/date";
@ -24,17 +29,31 @@ import {
calculateTotal,
calculateTax,
exportDevisToPdf,
statusToText,
} from "@ui/utils/helpers";
import { ViewDevisModal } from "@ui/components/devis/ViewDevisModal";
import { Badge } from "@ui/ui-library/badge";
import { Badge, BadgeVariant } 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"], BadgeVariant> = {
draft: "neutral",
sent: "in-review",
accepted: "done",
rejected: "danger",
expired: "notice",
};
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);
@ -137,15 +156,43 @@ export const DevisPage = () => {
{
field: "status",
headerName: "Status",
cellStyle: { padding: 4 },
cellRenderer: (params: { data: Devis }) => {
return <Badge variant="neutral">{params.data.status}</Badge>;
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 variant={statusToVariant[status as Devis["status"]]}>
{statusToText[status as Devis["status"]]}
</Badge>
</SelectListItem>
))}
</SelectListBox>
</SelectPopover>
</Select>
);
},
},
{
headerName: "Actions",
pinned: "right",
width: 120,
cellStyle: { padding: 0 },
width: 130,
cellStyle: { padding: 2 },
colId: "actions-column",
cellRenderer: (params: { data: Devis; node: { id: string | null } }) => {
if (!params.data) return null;
@ -209,6 +256,7 @@ export const DevisPage = () => {
rowData={devisData}
loading={isLoading}
gridOptions={{
rowHeight: 44,
theme: themeQuartz,
onRowDoubleClicked: (event) => {
if (event.data) {

View file

@ -6,13 +6,13 @@ const baseStyles =
const variantStyles = {
neutral:
"border-transparent bg-gray-100 text-gray-800 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
positive:
"border-transparent bg-green-100 text-green-800 hover:bg-green-100/80 dark:bg-green-900 dark:text-green-50 dark:hover:bg-green-900/80",
negative:
done: "border-transparent bg-green-100 text-green-800 hover:bg-green-100/80 dark:bg-green-900 dark:text-green-50 dark:hover:bg-green-900/80",
danger:
"border-transparent bg-red-100 text-red-800 hover:bg-red-100/80 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/80",
notice:
"border-transparent bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80 dark:bg-yellow-900 dark:text-yellow-50 dark:hover:bg-yellow-900/80",
info: "border-transparent bg-blue-100 text-blue-800 hover:bg-blue-100/80 dark:bg-blue-900 dark:text-blue-50 dark:hover:bg-blue-900/80",
"in-review":
"border-transparent bg-blue-100 text-blue-800 hover:bg-blue-100/80 dark:bg-blue-900 dark:text-blue-50 dark:hover:bg-blue-900/80",
};
export type BadgeVariant = keyof typeof variantStyles;

View file

@ -9,6 +9,14 @@ export const calculateTotal = (amount: number, tax: number) => {
return amount + tax;
};
export const statusToText: Record<Devis["status"], string> = {
draft: "Brouillon",
sent: "Envoyé",
accepted: "Accepté",
rejected: "Rejeté",
expired: "Expiré",
};
type Devis = Database["public"]["Tables"]["devis"]["Row"];
export const exportDevisToPdf = (devis: Devis) => {