diff --git a/ui/src/components/devis/CreateDevisModal.tsx b/ui/src/components/devis/CreateDevisModal.tsx index c7dbed5..0aa138f 100644 --- a/ui/src/components/devis/CreateDevisModal.tsx +++ b/ui/src/components/devis/CreateDevisModal.tsx @@ -55,8 +55,8 @@ export const CreateDevisModal = ({ Nouveau Devis - - + +

Créer un nouveau devis

diff --git a/ui/src/components/devis/DeleteDevisModal.tsx b/ui/src/components/devis/DeleteDevisModal.tsx index b07837b..092f850 100644 --- a/ui/src/components/devis/DeleteDevisModal.tsx +++ b/ui/src/components/devis/DeleteDevisModal.tsx @@ -30,7 +30,7 @@ export const DeleteDevisModalButton = ({ - +

Supprimer le devis diff --git a/ui/src/components/devis/ViewDevisModal.tsx b/ui/src/components/devis/ViewDevisModal.tsx index f3aa00c..2a71e0a 100644 --- a/ui/src/components/devis/ViewDevisModal.tsx +++ b/ui/src/components/devis/ViewDevisModal.tsx @@ -47,7 +47,7 @@ export const ViewDevisModal = ({ }) => { return ( - +

Devis {selectedDevis?.number} diff --git a/ui/src/pages/devis.test.tsx b/ui/src/pages/devis.test.tsx index c5438b2..f1d730b 100644 --- a/ui/src/pages/devis.test.tsx +++ b/ui/src/pages/devis.test.tsx @@ -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; const mockUseCreateDevis = useCreateDevis as ReturnType; const mockUseDeleteDevis = useDeleteDevis as ReturnType; +const mockUseUpdateDevis = useUpdateDevis as ReturnType; // 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(); + 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(); + 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(); + 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 + }); + }); + }); }); diff --git a/ui/src/pages/devis.tsx b/ui/src/pages/devis.tsx index 2284d61..ba8e0e5 100644 --- a/ui/src/pages/devis.tsx +++ b/ui/src/pages/devis.tsx @@ -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 = { + 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(null); @@ -137,15 +156,43 @@ export const DevisPage = () => { { field: "status", headerName: "Status", + cellStyle: { padding: 4 }, cellRenderer: (params: { data: Devis }) => { - return {params.data.status}; + const currentStatus = params.data.status; + + return ( + + ); }, }, { 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) { diff --git a/ui/src/ui-library/badge.tsx b/ui/src/ui-library/badge.tsx index ad9dd4a..e1a44ec 100644 --- a/ui/src/ui-library/badge.tsx +++ b/ui/src/ui-library/badge.tsx @@ -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; diff --git a/ui/src/utils/helpers.ts b/ui/src/utils/helpers.ts index d339f9b..c487fee 100644 --- a/ui/src/utils/helpers.ts +++ b/ui/src/utils/helpers.ts @@ -9,6 +9,14 @@ export const calculateTotal = (amount: number, tax: number) => { return amount + tax; }; +export const statusToText: Record = { + draft: "Brouillon", + sent: "Envoyé", + accepted: "Accepté", + rejected: "Rejeté", + expired: "Expiré", +}; + type Devis = Database["public"]["Tables"]["devis"]["Row"]; export const exportDevisToPdf = (devis: Devis) => {