Add status selection
This commit is contained in:
parent
d1b97ee909
commit
ca41b24d83
7 changed files with 157 additions and 14 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue