Repaso de UI de PROFORMAS
This commit is contained in:
parent
772cf2a253
commit
bd1266a6a2
@ -166,7 +166,7 @@
|
|||||||
"noUnsafeOptionalChaining": "error",
|
"noUnsafeOptionalChaining": "error",
|
||||||
"noUnusedLabels": "error",
|
"noUnusedLabels": "error",
|
||||||
"noUnusedVariables": "warn",
|
"noUnusedVariables": "warn",
|
||||||
"useExhaustiveDependencies": "error",
|
"useExhaustiveDependencies": "info",
|
||||||
"useHookAtTopLevel": "error",
|
"useHookAtTopLevel": "error",
|
||||||
"useIsNan": "error",
|
"useIsNan": "error",
|
||||||
"useJsxKeyInIterable": "error",
|
"useJsxKeyInIterable": "error",
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { lazy } from "react";
|
|||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
const ProformaLayout = lazy(() =>
|
const ProformaLayout = lazy(() =>
|
||||||
import("./proformas/shared2").then((m) => ({ default: m.ProformaLayout }))
|
import("./proformas/shared").then((m) => ({ default: m.ProformaLayout }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProformasListPage = lazy(() =>
|
const ProformasListPage = lazy(() =>
|
||||||
import("./proformas/list").then((m) => ({ default: m.ProformaListPage }))
|
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
/*const ProformasCreatePage = lazy(() =>
|
/*const ProformasCreatePage = lazy(() =>
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import type { ProformaListRow } from "../../shared2";
|
import type { ProformaListRow } from "../../shared";
|
||||||
import type { PROFORMA_STATUS } from "../../shared2/entities";
|
import type { PROFORMA_STATUS } from "../../shared/entities";
|
||||||
import { useChangeProformaStatusMutation } from "../../shared2/hooks";
|
import { useChangeProformaStatusMutation } from "../../shared/hooks";
|
||||||
|
|
||||||
interface ChangeStatusDialogState {
|
interface ChangeStatusDialogState {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import type { ProformaStatus } from "../../shared2";
|
import type { ProformaStatus } from "../../shared";
|
||||||
|
|
||||||
export const getProformaStatusButtonVariant = (
|
export const getProformaStatusButtonVariant = (
|
||||||
status: ProformaStatus
|
status: ProformaStatus
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { PROFORMA_STATUS, PROFORMA_STATUS_TRANSITIONS, type ProformaListRow } from "../../shared2";
|
import { PROFORMA_STATUS, PROFORMA_STATUS_TRANSITIONS, type ProformaListRow } from "../../shared";
|
||||||
import { getProformaStatusIcon } from "../helpers";
|
import { getProformaStatusIcon } from "../helpers";
|
||||||
|
|
||||||
import { StatusNode, TimelineConnector } from "./components";
|
import { StatusNode, TimelineConnector } from "./components";
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
|
|||||||
import { CheckCircle2, type LucideIcon } from "lucide-react";
|
import { CheckCircle2, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { PROFORMA_STATUS } from "../../../shared2";
|
import type { PROFORMA_STATUS } from "../../../shared";
|
||||||
|
|
||||||
interface StatusNodeProps {
|
interface StatusNodeProps {
|
||||||
status: PROFORMA_STATUS;
|
status: PROFORMA_STATUS;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { type ProformaListRow, useDeleteProformaMutation } from "../../shared2";
|
import { type ProformaListRow, useDeleteProformaMutation } from "../../shared";
|
||||||
|
|
||||||
interface DeleteProformaDialogState {
|
interface DeleteProformaDialogState {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { ProformaListRow } from "../../../shared2";
|
import type { ProformaListRow } from "../../../shared";
|
||||||
|
|
||||||
interface DeleteProformaDialogProps {
|
interface DeleteProformaDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./use-list-proformas.controller.ts";
|
export * from "./use-list-proformas.controller";
|
||||||
export * from "./use-list-proformas-page.controller.ts";
|
export * from "./use-list-proformas-page.controller";
|
||||||
|
export * from "./use-proforma-summary-panel.controller";
|
||||||
|
|||||||
@ -1,62 +1,24 @@
|
|||||||
import React from "react";
|
import type { RightPanelMode } from "@repo/rdx-ui/hooks";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { useChangeProformaStatusDialogController } from "../../change-status";
|
|
||||||
import { useDeleteProformaDialogController } from "../../delete";
|
|
||||||
import { useProformaIssueDialogController } from "../../issue-proforma";
|
|
||||||
import type { ProformaListRow } from "../../shared2";
|
|
||||||
import {
|
|
||||||
type PROFORMA_STATUS,
|
|
||||||
PROFORMA_STATUS_TRANSITIONS,
|
|
||||||
} from "../../shared2/entities/proforma-status.entity";
|
|
||||||
|
|
||||||
import { useListProformasController } from "./use-list-proformas.controller";
|
import { useListProformasController } from "./use-list-proformas.controller";
|
||||||
|
import { useProformaSummaryPanelController } from "./use-proforma-summary-panel.controller";
|
||||||
|
|
||||||
export function useListProformasPageController() {
|
export const useListProformasPageController = () => {
|
||||||
const listCtrl = useListProformasController();
|
const listCtrl = useListProformasController();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
// Controlador de diálogos
|
const proformaId = searchParams.get("proformaId") ?? "";
|
||||||
const issueDialogCtrl = useProformaIssueDialogController();
|
const panelMode = (searchParams.get("panel") as RightPanelMode | null) ?? "view";
|
||||||
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
|
|
||||||
const deleteDialogCtrl = useDeleteProformaDialogController();
|
|
||||||
|
|
||||||
const handleOpenIssueProformaDialog = React.useCallback(
|
const panelCtrl = useProformaSummaryPanelController({
|
||||||
(proforma: ProformaListRow) => {
|
initialProformaId: proformaId,
|
||||||
issueDialogCtrl.openDialog(proforma);
|
initialMode: panelMode,
|
||||||
},
|
initialOpen: proformaId !== "",
|
||||||
[issueDialogCtrl]
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenChangeProformaStatusDialog = React.useCallback(
|
|
||||||
(proforma: ProformaListRow, nextStatus: string) => {
|
|
||||||
const proforma_status = proforma.status as PROFORMA_STATUS;
|
|
||||||
const transitions = PROFORMA_STATUS_TRANSITIONS[proforma_status] ?? [];
|
|
||||||
|
|
||||||
if (!transitions.includes(nextStatus as PROFORMA_STATUS)) {
|
|
||||||
console.warn(`Transición inválida: ${proforma.status} → ${nextStatus}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeStatusDialogCtrl.openDialog([proforma], nextStatus as PROFORMA_STATUS);
|
|
||||||
},
|
|
||||||
[changeStatusDialogCtrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenDeleteProformaDialog = React.useCallback(
|
|
||||||
(proforma: ProformaListRow) => {
|
|
||||||
deleteDialogCtrl.openDialog([proforma]);
|
|
||||||
},
|
|
||||||
[deleteDialogCtrl]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listCtrl,
|
listCtrl,
|
||||||
|
panelCtrl,
|
||||||
issueDialogCtrl,
|
|
||||||
changeStatusDialogCtrl,
|
|
||||||
deleteDialogCtrl,
|
|
||||||
|
|
||||||
handleIssueProforma: handleOpenIssueProformaDialog,
|
|
||||||
handleChangeStatusProforma: handleOpenChangeProformaStatusDialog,
|
|
||||||
handleDeleteProforma: handleOpenDeleteProformaDialog,
|
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,50 +1,85 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
|
||||||
import { useDebounce } from "@repo/rdx-ui/components";
|
import { useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { useProformasListQuery } from "../../shared2";
|
import {
|
||||||
|
type ListProformasByCriteriaParams,
|
||||||
|
type ProformaList,
|
||||||
|
type ProformaStatus,
|
||||||
|
useProformasListQuery,
|
||||||
|
} from "../../shared";
|
||||||
|
|
||||||
|
type ProformaListStatusFilter = "all" | ProformaStatus;
|
||||||
|
|
||||||
|
const EMPTY_PROFORMAS_LIST: ProformaList = {
|
||||||
|
items: [],
|
||||||
|
page: 0,
|
||||||
|
perPage: 5,
|
||||||
|
totalPages: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export const useListProformasController = () => {
|
export const useListProformasController = () => {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(5);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [status, setStatus] = useState("all");
|
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>("all");
|
||||||
|
|
||||||
const debouncedQ = useDebounce(search, 300);
|
const debouncedSearch = useDebounce(search, 300);
|
||||||
|
|
||||||
const criteria = useMemo<CriteriaDTO>(() => {
|
const criteria = useMemo<NonNullable<ListProformasByCriteriaParams["criteria"]>>(
|
||||||
const baseFilters =
|
() => ({
|
||||||
status === "all" ? [] : [{ field: "status", operator: "EQUALS", value: status }];
|
q: debouncedSearch || "",
|
||||||
|
|
||||||
return {
|
|
||||||
q: debouncedQ || "",
|
|
||||||
pageSize,
|
|
||||||
pageNumber: pageIndex,
|
pageNumber: pageIndex,
|
||||||
|
pageSize,
|
||||||
order: "desc",
|
order: "desc",
|
||||||
orderBy: "invoice_date",
|
orderBy: "invoiceDate",
|
||||||
filters: baseFilters,
|
filters:
|
||||||
};
|
statusFilter === "all" ? [] : [{ field: "status", operator: "eq", value: statusFilter }],
|
||||||
}, [pageSize, pageIndex, debouncedQ, status]);
|
}),
|
||||||
|
[debouncedSearch, pageIndex, pageSize, statusFilter]
|
||||||
|
);
|
||||||
|
|
||||||
const query = useProformasListQuery({ criteria });
|
const query = useProformasListQuery({ criteria });
|
||||||
|
|
||||||
|
const setStatusFilterValue = (value: string) => {
|
||||||
|
const nextValue = (value || "all") as ProformaListStatusFilter;
|
||||||
|
|
||||||
|
setStatusFilter((prev) => {
|
||||||
|
if (prev === nextValue) return prev;
|
||||||
|
|
||||||
|
// Sólo si la búsqueda realmente cambia,
|
||||||
|
// reseteamos la página a 0 para evitar inconsistencias
|
||||||
|
setPageIndex(0);
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setSearchValue = (value: string) => {
|
const setSearchValue = (value: string) => {
|
||||||
setSearch(value.trim().replace(/\s+/g, " "));
|
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||||
setPageIndex(0);
|
|
||||||
|
setSearch((prev) => {
|
||||||
|
if (prev === nextValue) return prev;
|
||||||
|
|
||||||
|
// Sólo si la búsqueda realmente cambia,
|
||||||
|
// reseteamos la página a 0 para evitar inconsistencias
|
||||||
|
setPageIndex(0);
|
||||||
|
return nextValue;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPageSizeValue = (value: number) => {
|
const setPageSizeValue = (value: number) => {
|
||||||
setPageSize(value);
|
setPageSize((prev) => {
|
||||||
setPageIndex(0);
|
if (prev === value) return prev;
|
||||||
};
|
|
||||||
|
|
||||||
const setStatusFilter = (newStatus: string) => {
|
// Sólo si el tamaño de página realmente cambia,
|
||||||
setStatus(newStatus);
|
// reseteamos la página a 0 para evitar inconsistencias
|
||||||
setPageIndex(0);
|
setPageIndex(0);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: query.data,
|
data: query.data ?? EMPTY_PROFORMAS_LIST,
|
||||||
isLoading: query.isLoading,
|
isLoading: query.isLoading,
|
||||||
isFetching: query.isFetching,
|
isFetching: query.isFetching,
|
||||||
|
|
||||||
@ -61,7 +96,7 @@ export const useListProformasController = () => {
|
|||||||
search,
|
search,
|
||||||
setSearchValue,
|
setSearchValue,
|
||||||
|
|
||||||
status,
|
statusFilter,
|
||||||
setStatusFilter,
|
setStatusFilter: setStatusFilterValue,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { type RightPanelMode, useRightPanelState } from "@repo/rdx-ui/hooks";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { useProformaGetQuery } from "../../shared";
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
initialProformaId?: string;
|
||||||
|
initialMode?: RightPanelMode;
|
||||||
|
initialOpen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProformaSummaryPanelController = ({
|
||||||
|
initialProformaId = "",
|
||||||
|
initialMode = "view",
|
||||||
|
initialOpen = false,
|
||||||
|
}: Options = {}) => {
|
||||||
|
const [proformaId, setProformaId] = useState(initialProformaId);
|
||||||
|
|
||||||
|
const panelState = useRightPanelState({
|
||||||
|
defaultMode: initialMode,
|
||||||
|
defaultVisibility: initialOpen ? "visible" : "hidden",
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = useProformaGetQuery({
|
||||||
|
id: proformaId,
|
||||||
|
enabled: Boolean(proformaId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const openProformaPanel = useCallback(
|
||||||
|
(id: string, mode: RightPanelMode = "view") => {
|
||||||
|
setProformaId(id);
|
||||||
|
panelState.open(mode);
|
||||||
|
},
|
||||||
|
[panelState.open]
|
||||||
|
);
|
||||||
|
|
||||||
|
const closePanel = useCallback(() => {
|
||||||
|
panelState.close();
|
||||||
|
setProformaId("");
|
||||||
|
}, [panelState.close]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
proforma: query.data,
|
||||||
|
proformaId,
|
||||||
|
openProformaPanel,
|
||||||
|
closePanel,
|
||||||
|
panelState,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1 +1,3 @@
|
|||||||
export * from "./proformas-grid";
|
export * from "./proforma-summary-panel/proforma-summary-panel";
|
||||||
|
export * from "./proformas-grid/proformas-grid";
|
||||||
|
export * from "./proformas-grid/use-proforma-grid-columns";
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./proforma-summary-panel";
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
|
import type { Proforma } from "../../../../shared";
|
||||||
|
|
||||||
|
export const ProformaFooterActions = ({
|
||||||
|
proforma,
|
||||||
|
onChangeStatus,
|
||||||
|
onEdit,
|
||||||
|
}: {
|
||||||
|
proforma: Proforma;
|
||||||
|
onChangeStatus?: (proforma: Proforma) => void;
|
||||||
|
onEdit?: (proforma: Proforma) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="border-t p-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button className="flex-1" onClick={() => onChangeStatus?.(proforma)} variant="outline">
|
||||||
|
Cambiar de estado
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button className="flex-1" onClick={() => onEdit?.(proforma)}>
|
||||||
|
Editar proforma
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { Avatar, AvatarFallback, Badge } from "@repo/shadcn-ui/components";
|
||||||
|
import { Building2Icon, CopyIcon, UserIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Proforma } from "../../../../shared";
|
||||||
|
import { Initials, ProformaStatusBadge } from "../../components";
|
||||||
|
|
||||||
|
export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
||||||
|
const handleCopyTin = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(proforma.recipient.tin);
|
||||||
|
} catch {
|
||||||
|
// Silencio o toast fuera de este componente
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<Avatar className="size-10 border-2 border-background shadow-sm">
|
||||||
|
<AvatarFallback
|
||||||
|
className={
|
||||||
|
proforma.status !== "draft"
|
||||||
|
? "bg-blue-100 text-blue-700"
|
||||||
|
: "bg-muted text-muted-foreground"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Initials name={proforma.recipient.name} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h2 className="truncate text-lg font-semibold">{proforma.recipient.name}</h2>
|
||||||
|
<ProformaStatusBadge status={proforma.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{proforma.description && (
|
||||||
|
<p className="truncate text-sm text-muted-foreground">{proforma.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<Badge className="gap-1" variant="outline">
|
||||||
|
{proforma.isProforma ? (
|
||||||
|
<>
|
||||||
|
<Building2Icon className="size-3" />
|
||||||
|
Empresa
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserIcon className="size-3" />
|
||||||
|
Particular
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 text-xs font-mono text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={handleCopyTin}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{proforma.recipient.tin}
|
||||||
|
<CopyIcon className="size-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { Separator } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
|
import type { Proforma } from "../../../../shared";
|
||||||
|
|
||||||
|
import { ProformaFooterActions } from "./proforma-footer-actions";
|
||||||
|
import { ProformaHeader } from "./proforma-header";
|
||||||
|
|
||||||
|
interface ProformaSummaryContentProps {
|
||||||
|
proforma: Proforma;
|
||||||
|
onEdit?: (proforma: Proforma) => void;
|
||||||
|
onChangeStatus?: (proforma: Proforma) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProformaSummaryContent = ({
|
||||||
|
proforma,
|
||||||
|
onEdit,
|
||||||
|
onChangeStatus,
|
||||||
|
}: ProformaSummaryContentProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<ProformaHeader proforma={proforma} />
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<ProformaFooterActions onChangeStatus={onChangeStatus} onEdit={onEdit} proforma={proforma} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
import { RightPanel } from "@repo/rdx-ui/components";
|
||||||
|
import type { RightPanelMode, RightPanelVisibility } from "@repo/rdx-ui/hooks";
|
||||||
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { PencilIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import type { Proforma } from "../../../../shared";
|
||||||
|
|
||||||
|
import { ProformaSummaryContent } from "./proforma-summary-content";
|
||||||
|
|
||||||
|
const mockERPData = {
|
||||||
|
totalPurchases: 45750.8,
|
||||||
|
purchasesThisYear: 12350.25,
|
||||||
|
lastInvoices: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
number: "FAC-2024-0156",
|
||||||
|
date: "2024-01-15",
|
||||||
|
amount: 1250.0,
|
||||||
|
status: "paid" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
number: "FAC-2024-0142",
|
||||||
|
date: "2024-01-08",
|
||||||
|
amount: 890.5,
|
||||||
|
status: "paid" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
number: "FAC-2024-0128",
|
||||||
|
date: "2023-12-22",
|
||||||
|
amount: 2100.0,
|
||||||
|
status: "pending" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
number: "FAC-2023-0098",
|
||||||
|
date: "2023-11-30",
|
||||||
|
amount: 750.25,
|
||||||
|
status: "overdue" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProformaSummaryPanelProps {
|
||||||
|
proforma?: Proforma;
|
||||||
|
|
||||||
|
open: boolean;
|
||||||
|
visibility: RightPanelVisibility;
|
||||||
|
mode: RightPanelMode;
|
||||||
|
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
|
||||||
|
onEdit?: (proforma: Proforma) => void;
|
||||||
|
onChangeStatus?: (proforma: Proforma) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProformaSummaryPanel = ({
|
||||||
|
proforma,
|
||||||
|
|
||||||
|
open,
|
||||||
|
visibility,
|
||||||
|
mode,
|
||||||
|
|
||||||
|
onOpenChange,
|
||||||
|
|
||||||
|
onEdit,
|
||||||
|
onChangeStatus,
|
||||||
|
|
||||||
|
className,
|
||||||
|
}: ProformaSummaryPanelProps) => {
|
||||||
|
const titleMap: Record<RightPanelMode, string> = {
|
||||||
|
view: "Vista previa de proforma",
|
||||||
|
edit: "Editar proforma",
|
||||||
|
create: "Nuevo proforma",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightPanel
|
||||||
|
className={cn("bg-transparent", className)}
|
||||||
|
headerActions={
|
||||||
|
<>
|
||||||
|
{proforma ? (
|
||||||
|
<Button
|
||||||
|
aria-label="Editar cliente"
|
||||||
|
onClick={() => onEdit?.(proforma)}
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<PencilIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
open={open}
|
||||||
|
title={titleMap[mode]}
|
||||||
|
>
|
||||||
|
{proforma ? (
|
||||||
|
<ProformaSummaryContent
|
||||||
|
onChangeStatus={onChangeStatus}
|
||||||
|
onEdit={onEdit}
|
||||||
|
proforma={proforma}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center p-4">
|
||||||
|
<p className="text-sm text-muted-foreground">Selecciona una proforma</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</RightPanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
import { DataTable, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import type { ProformaList, ProformaListRow } from "../../../../shared2";
|
import type { ProformaList, ProformaListRow } from "../../../../shared";
|
||||||
|
|
||||||
interface ProformasGridProps {
|
interface ProformasGridProps {
|
||||||
data?: ProformaList;
|
data?: ProformaList;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
fetching?: boolean;
|
||||||
|
|
||||||
columns: ColumnDef<ProformaListRow, unknown>[];
|
columns: ColumnDef<ProformaListRow, unknown>[];
|
||||||
|
|
||||||
@ -29,15 +29,14 @@ export const ProformasGrid = ({
|
|||||||
onPageSizeChange,
|
onPageSizeChange,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
}: ProformasGridProps) => {
|
}: ProformasGridProps) => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { items, total_items } = data || { items: [], total_items: 0 };
|
const { items, totalItems } = data || { items: [], totalItems: 0 };
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
<SkeletonDataTable
|
<SkeletonDataTable
|
||||||
columns={columns.length}
|
columns={columns.length}
|
||||||
footerProps={{ pageIndex, pageSize, totalItems: total_items ?? 0 }}
|
footerProps={{ pageIndex, pageSize, totalItems: totalItems ?? 0 }}
|
||||||
rows={Math.max(6, pageSize)}
|
rows={Math.max(6, pageSize)}
|
||||||
showFooter
|
showFooter
|
||||||
/>
|
/>
|
||||||
@ -55,7 +54,7 @@ export const ProformasGrid = ({
|
|||||||
onRowClick={(row, _index) => onRowClick?.(row.id)}
|
onRowClick={(row, _index) => onRowClick?.(row.id)}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
totalItems={total_items}
|
totalItems={totalItems}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,14 +21,20 @@ import {
|
|||||||
PROFORMA_STATUS_TRANSITIONS,
|
PROFORMA_STATUS_TRANSITIONS,
|
||||||
type ProformaListRow,
|
type ProformaListRow,
|
||||||
type ProformaStatus,
|
type ProformaStatus,
|
||||||
} from "../../../../shared2";
|
} from "../../../../shared";
|
||||||
import { ProformaStatusBadge } from "../../components";
|
import { ProformaStatusBadge } from "../../components";
|
||||||
|
|
||||||
type GridActionHandlers = {
|
type GridActionHandlers = {
|
||||||
|
onPreviewClick?: (proforma: ProformaListRow) => void;
|
||||||
onEditClick?: (proforma: ProformaListRow) => void;
|
onEditClick?: (proforma: ProformaListRow) => void;
|
||||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||||
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: string) => void;
|
|
||||||
onDeleteClick?: (proforma: ProformaListRow) => void;
|
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
||||||
|
onLinkedInvoiceClick?: (proforma: ProformaListRow) => void;
|
||||||
|
getNextStatus?: (proforma: ProformaListRow) => ProformaStatus | null;
|
||||||
|
canIssue?: (proforma: ProformaListRow) => boolean;
|
||||||
|
canEdit?: (proforma: ProformaListRow) => boolean;
|
||||||
|
canDelete?: (proforma: ProformaListRow) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useProformasGridColumns(
|
export function useProformasGridColumns(
|
||||||
@ -61,7 +67,7 @@ export function useProformasGridColumns(
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
},*/
|
},*/
|
||||||
{
|
{
|
||||||
accessorKey: "invoice_number",
|
accessorKey: "invoiceNumber",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -74,7 +80,7 @@ export function useProformasGridColumns(
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => <div className="font-medium">{row.getValue("invoice_number")}</div>,
|
cell: ({ row }) => <div className="font-medium">{row.getValue("invoiceNumber")}</div>,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Estado
|
// Estado
|
||||||
@ -85,7 +91,7 @@ export function useProformasGridColumns(
|
|||||||
const proforma = row.original;
|
const proforma = row.original;
|
||||||
|
|
||||||
const isIssued = proforma.status === "issued";
|
const isIssued = proforma.status === "issued";
|
||||||
const invoiceId = proforma.linked_invoice_id;
|
const invoiceId = proforma.linkedInvoiceId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -99,13 +105,12 @@ export function useProformasGridColumns(
|
|||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
className="size-6 text-foreground hover:text-primary"
|
className="size-6 text-foreground hover:text-primary"
|
||||||
|
onClick={() => actionHandlers.onLinkedInvoiceClick?.(proforma)}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<a href={`/facturas/${invoiceId}`}>
|
<ExternalLinkIcon />
|
||||||
<ExternalLinkIcon />
|
<span className="sr-only">Ver factura {invoiceId}</span>
|
||||||
<span className="sr-only">Ver factura {invoiceId}</span>
|
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Ver factura {invoiceId}</TooltipContent>
|
<TooltipContent>Ver factura {invoiceId}</TooltipContent>
|
||||||
@ -119,7 +124,7 @@ export function useProformasGridColumns(
|
|||||||
|
|
||||||
// Cliente
|
// Cliente
|
||||||
{
|
{
|
||||||
accessorKey: "client_name",
|
accessorKey: "recipientName",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -136,12 +141,15 @@ export function useProformasGridColumns(
|
|||||||
const proforma = row.original;
|
const proforma = row.original;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a
|
<button
|
||||||
className="text-primary hover:underline font-semibold"
|
className="text-primary hover:underline font-semibold"
|
||||||
href={`/customers/${proforma.customer_id}`}
|
onClick={() => actionHandlers.onPreviewClick?.(proforma)}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
{proforma.recipient.name}
|
{proforma.recipient.name}
|
||||||
</a>
|
</button>
|
||||||
|
|
||||||
|
<br />
|
||||||
<div className="text-xs text-muted-foreground">{proforma.recipient.tin}</div>
|
<div className="text-xs text-muted-foreground">{proforma.recipient.tin}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -156,7 +164,7 @@ export function useProformasGridColumns(
|
|||||||
header: "Reference",
|
header: "Reference",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "invoice_date",
|
accessorKey: "invoiceDate",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -171,7 +179,7 @@ export function useProformasGridColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "operation_date",
|
accessorKey: "operationDate",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -186,32 +194,32 @@ export function useProformasGridColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "subtotal_amount_fmt",
|
accessorKey: "subtotalAmountFmt",
|
||||||
header: () => <div className="text-right">Subtotal</div>,
|
header: () => <div className="text-right">Subtotal</div>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-right tabular-nums">{row.getValue("subtotal_amount_fmt")}</div>
|
<div className="text-right tabular-nums">{row.getValue("subtotalAmountFmt")}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "discount_amount_fmt",
|
accessorKey: "discountAmountFmt",
|
||||||
header: () => <div className="text-right">Descuentos</div>,
|
header: () => <div className="text-right">Descuentos</div>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-right tabular-nums">{row.getValue("discount_amount_fmt")}</div>
|
<div className="text-right tabular-nums">{row.getValue("discountAmountFmt")}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "taxes_amount_fmt",
|
accessorKey: "taxesAmountFmt",
|
||||||
header: () => <div className="text-right">Impuestos</div>,
|
header: () => <div className="text-right">Impuestos</div>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-right tabular-nums">{row.getValue("taxes_amount_fmt")}</div>
|
<div className="text-right tabular-nums">{row.getValue("taxesAmountFmt")}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "total_amount_fmt",
|
accessorKey: "totalAmountFmt",
|
||||||
header: () => <div className="text-right">Importe total</div>,
|
header: () => <div className="text-right">Importe total</div>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="text-right tabular-nums font-medium">
|
<div className="text-right tabular-nums font-medium">
|
||||||
{row.getValue("total_amount_fmt")}
|
{row.getValue("totalAmountFmt")}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./initials";
|
||||||
export * from "./proforma-status-badge";
|
export * from "./proforma-status-badge";
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export const Initials = ({ name }: { name: string }) => {
|
||||||
|
const parts = name.trim().split(/\s+/).slice(0, 2);
|
||||||
|
return <> {parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?"} </>;
|
||||||
|
};
|
||||||
@ -7,10 +7,10 @@ import {
|
|||||||
getProformaStatusColor,
|
getProformaStatusColor,
|
||||||
getProformaStatusIcon,
|
getProformaStatusIcon,
|
||||||
} from "../../../change-status/helpers";
|
} from "../../../change-status/helpers";
|
||||||
import type { ProformaStatus } from "../../../shared2";
|
import type { ProformaStatus } from "../../../shared";
|
||||||
|
|
||||||
export type ProformaStatusBadgeProps = {
|
export type ProformaStatusBadgeProps = {
|
||||||
status: string | ProformaStatus; // permitir cualquier valor
|
status: ProformaStatus;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export * from "./proforma-list-page";
|
export * from "./list-proformas-page";
|
||||||
|
|||||||
@ -0,0 +1,177 @@
|
|||||||
|
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { FilterIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import { ChangeStatusDialog } from "../../../change-status";
|
||||||
|
import { ProformaIssueDialog } from "../../../issue-proforma";
|
||||||
|
import { useListProformasPageController } from "../../controllers";
|
||||||
|
import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks";
|
||||||
|
|
||||||
|
export const ListProformasPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { listCtrl, panelCtrl } = useListProformasPageController();
|
||||||
|
|
||||||
|
const columns = useProformasGridColumns({
|
||||||
|
onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
||||||
|
onIssueClick: handleIssueProforma,
|
||||||
|
onDeleteClick: handleDeleteProforma,
|
||||||
|
onChangeStatusClick: handleChangeStatusProforma,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPanelOpen = panelCtrl.panelState.isOpen;
|
||||||
|
|
||||||
|
const listContent = (
|
||||||
|
<div className="flex h-full min-w-0 flex-col gap-4 overflow-hidden">
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<SimpleSearchInput
|
||||||
|
loading={listCtrl.isLoading}
|
||||||
|
onSearchChange={listCtrl.setSearchValue}
|
||||||
|
value={listCtrl.search}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select onValueChange={listCtrl.setStatusFilter} value={listCtrl.statusFilter}>
|
||||||
|
<SelectTrigger className="w-full sm:w-48">
|
||||||
|
<FilterIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
<SelectValue placeholder={t("filters.status")} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{t("catalog.proformas.status.all.label")}</SelectItem>
|
||||||
|
<SelectItem value="draft">{t("catalog.proformas.status.draft.label")}</SelectItem>
|
||||||
|
<SelectItem value="sent">{t("catalog.proformas.status.sent.label")}</SelectItem>
|
||||||
|
<SelectItem value="approved">{t("catalog.proformas.status.approved.label")}</SelectItem>
|
||||||
|
<SelectItem value="rejected">{t("catalog.proformas.status.rejected.label")}</SelectItem>
|
||||||
|
<SelectItem value="issued">{t("catalog.proformas.status.issued.label")}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
|
<ProformasGrid
|
||||||
|
columns={columns}
|
||||||
|
data={listCtrl.data}
|
||||||
|
fetching={listCtrl.isFetching}
|
||||||
|
loading={listCtrl.isLoading}
|
||||||
|
onPageChange={listCtrl.setPageIndex}
|
||||||
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
|
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
||||||
|
pageIndex={listCtrl.pageIndex}
|
||||||
|
pageSize={listCtrl.pageSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (listCtrl.isError) {
|
||||||
|
return (
|
||||||
|
<AppContent>
|
||||||
|
<ErrorAlert
|
||||||
|
message={(listCtrl.error as Error)?.message || "Error al cargar el listado"}
|
||||||
|
title={t("pages.proformas.list.loadErrorTitle")}
|
||||||
|
/>
|
||||||
|
<BackHistoryButton />
|
||||||
|
</AppContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
description={t("pages.proformas.list.description")}
|
||||||
|
rightSlot={
|
||||||
|
<Button
|
||||||
|
aria-label={t("pages.proformas.create.title")}
|
||||||
|
onClick={() => navigate("/proformas/create")}
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
|
{t("pages.proformas.create.title")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
title={t("pages.proformas.list.title")}
|
||||||
|
/>
|
||||||
|
</AppHeader>
|
||||||
|
|
||||||
|
<AppContent>
|
||||||
|
{isPanelOpen ? (
|
||||||
|
<ResizablePanelGroup
|
||||||
|
autoSave="list-proformas-page"
|
||||||
|
className="h-full"
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<ResizablePanel defaultSize="70%" maxSize="75%" minSize="70%">
|
||||||
|
{listContent}
|
||||||
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<ResizableHandle className="mx-4" withHandle />
|
||||||
|
|
||||||
|
<ResizablePanel defaultSize="30%" maxSize="30%" minSize="25%">
|
||||||
|
<div className="h-full">
|
||||||
|
<ProformaSummaryPanel
|
||||||
|
className="border bg-background"
|
||||||
|
mode={panelCtrl.panelState.mode}
|
||||||
|
onEdit={(proforma) => navigate(`/proformas/${proforma.id}/edit`)}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
panelCtrl.closePanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panelCtrl.panelState.onOpenChange(true);
|
||||||
|
}}
|
||||||
|
open={panelCtrl.panelState.isOpen}
|
||||||
|
proforma={panelCtrl.proforma}
|
||||||
|
visibility={panelCtrl.panelState.visibility}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
) : (
|
||||||
|
<div className="flex min-h-0 flex-1 overflow-hidden">{listContent}</div>
|
||||||
|
)}
|
||||||
|
<>
|
||||||
|
{/* Emitir factura */}
|
||||||
|
<ProformaIssueDialog
|
||||||
|
isSubmitting={issueDialogCtrl.isSubmitting}
|
||||||
|
onConfirm={issueDialogCtrl.confirmIssue}
|
||||||
|
onOpenChange={(open) => !open && issueDialogCtrl.closeDialog()}
|
||||||
|
open={issueDialogCtrl.open}
|
||||||
|
proforma={issueDialogCtrl.proforma}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Cambiar estado */}
|
||||||
|
<ChangeStatusDialog
|
||||||
|
isSubmitting={changeStatusDialogCtrl.isSubmitting}
|
||||||
|
onConfirm={changeStatusDialogCtrl.confirmChangeStatus}
|
||||||
|
onOpenChange={(open) => !open && changeStatusDialogCtrl.closeDialog()}
|
||||||
|
open={changeStatusDialogCtrl.open}
|
||||||
|
proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Eliminar */}
|
||||||
|
<DeleteProformaDialog
|
||||||
|
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||||
|
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||||
|
onOpenChange={(open) => !open && deleteDialogCtrl.closeDialog()}
|
||||||
|
open={deleteDialogCtrl.open}
|
||||||
|
proformas={deleteDialogCtrl.proformas}
|
||||||
|
requireSecondConfirm={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
|
||||||
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import { FilterIcon, PlusIcon } from "lucide-react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
|
||||||
import { ChangeStatusDialog } from "../../../change-status";
|
|
||||||
import { DeleteProformaDialog } from "../../../delete/ui/components";
|
|
||||||
import { ProformaIssueDialog } from "../../../issue-proforma";
|
|
||||||
import { useListProformasPageController } from "../../controllers";
|
|
||||||
import { ProformasGrid, useProformasGridColumns } from "../blocks";
|
|
||||||
|
|
||||||
export const ProformaListPage = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {
|
|
||||||
listCtrl,
|
|
||||||
|
|
||||||
issueDialogCtrl,
|
|
||||||
changeStatusDialogCtrl,
|
|
||||||
deleteDialogCtrl,
|
|
||||||
|
|
||||||
handleChangeStatusProforma,
|
|
||||||
handleDeleteProforma,
|
|
||||||
handleIssueProforma,
|
|
||||||
} = useListProformasPageController();
|
|
||||||
|
|
||||||
const columns = useProformasGridColumns({
|
|
||||||
onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
|
|
||||||
onIssueClick: handleIssueProforma,
|
|
||||||
onDeleteClick: handleDeleteProforma,
|
|
||||||
onChangeStatusClick: handleChangeStatusProforma,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (listCtrl.isError) {
|
|
||||||
return (
|
|
||||||
<AppContent>
|
|
||||||
<ErrorAlert
|
|
||||||
message={(listCtrl.error as Error)?.message || "Error al cargar el listado"}
|
|
||||||
title={t("pages.proformas.list.loadErrorTitle")}
|
|
||||||
/>
|
|
||||||
<BackHistoryButton />
|
|
||||||
</AppContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AppHeader>
|
|
||||||
<PageHeader
|
|
||||||
description={t("pages.proformas.list.description")}
|
|
||||||
rightSlot={
|
|
||||||
<Button
|
|
||||||
aria-label={t("pages.proformas.create.title")}
|
|
||||||
onClick={() => navigate("/proformas/create")}
|
|
||||||
>
|
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
|
||||||
{t("pages.proformas.create.title")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
title={t("pages.proformas.list.title")}
|
|
||||||
/>
|
|
||||||
</AppHeader>
|
|
||||||
|
|
||||||
<AppContent>
|
|
||||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
||||||
{/* Search and filters */}
|
|
||||||
<div className="h-full min-w-0 overflow-auto w-full">
|
|
||||||
<div className="flex items-center justify-between gap-16">
|
|
||||||
<SimpleSearchInput
|
|
||||||
loading={listCtrl.isLoading}
|
|
||||||
onSearchChange={listCtrl.setSearchValue}
|
|
||||||
value={listCtrl.search}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Select defaultValue="all" onValueChange={listCtrl.setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-full sm:w-48">
|
|
||||||
<FilterIcon aria-hidden className="mr-2 size-4" />
|
|
||||||
<SelectValue placeholder={t("filters.status")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">{t("catalog.proformas.status.all.label")}</SelectItem>
|
|
||||||
<SelectItem value="draft">{t("catalog.proformas.status.draft.label")}</SelectItem>
|
|
||||||
<SelectItem value="sent">{t("catalog.proformas.status.sent.label")}</SelectItem>
|
|
||||||
<SelectItem value="approved">
|
|
||||||
{t("catalog.proformas.status.approved.label")}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="rejected">
|
|
||||||
{t("catalog.proformas.status.rejected.label")}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="issued">
|
|
||||||
{t("catalog.proformas.status.issued.label")}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ProformasGrid
|
|
||||||
columns={columns}
|
|
||||||
data={listCtrl.data}
|
|
||||||
loading={listCtrl.isLoading}
|
|
||||||
onPageChange={listCtrl.setPageIndex}
|
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
|
||||||
pageIndex={listCtrl.pageIndex}
|
|
||||||
pageSize={listCtrl.pageSize}
|
|
||||||
// acciones rápidas del grid → page controller
|
|
||||||
//onRowClick={(id) => navigate(`/proformas/${id}`)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Emitir factura */}
|
|
||||||
<ProformaIssueDialog
|
|
||||||
isSubmitting={issueDialogCtrl.isSubmitting}
|
|
||||||
onConfirm={issueDialogCtrl.confirmIssue}
|
|
||||||
onOpenChange={(open) => !open && issueDialogCtrl.closeDialog()}
|
|
||||||
open={issueDialogCtrl.open}
|
|
||||||
proforma={issueDialogCtrl.proforma}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Cambiar estado */}
|
|
||||||
<ChangeStatusDialog
|
|
||||||
isSubmitting={changeStatusDialogCtrl.isSubmitting}
|
|
||||||
onConfirm={changeStatusDialogCtrl.confirmChangeStatus}
|
|
||||||
onOpenChange={(open) => !open && changeStatusDialogCtrl.closeDialog()}
|
|
||||||
open={changeStatusDialogCtrl.open}
|
|
||||||
proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Eliminar */}
|
|
||||||
<DeleteProformaDialog
|
|
||||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
|
||||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
|
||||||
onOpenChange={(open) => !open && deleteDialogCtrl.closeDialog()}
|
|
||||||
open={deleteDialogCtrl.open}
|
|
||||||
proformas={deleteDialogCtrl.proformas}
|
|
||||||
requireSecondConfirm={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AppContent>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { ProformaDtoAdapter } from "../../adapters";
|
import { ProformaDtoAdapter } from "../../adapters";
|
||||||
import { useUpdateProforma } from "../../shared2/hooks/use-proforma-update-mutation";
|
import { useUpdateProforma } from "../../shared/hooks/use-proforma-update-mutation";
|
||||||
import {
|
import {
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaFormData,
|
type ProformaFormData,
|
||||||
|
|||||||
@ -11,4 +11,13 @@ export enum PROFORMA_STATUS {
|
|||||||
ISSUED = "issued",
|
ISSUED = "issued",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transiciones válidas según reglas del dominio
|
||||||
|
export const PROFORMA_STATUS_TRANSITIONS: Record<PROFORMA_STATUS, PROFORMA_STATUS[]> = {
|
||||||
|
[PROFORMA_STATUS.DRAFT]: [PROFORMA_STATUS.SENT],
|
||||||
|
[PROFORMA_STATUS.SENT]: [PROFORMA_STATUS.APPROVED, PROFORMA_STATUS.REJECTED],
|
||||||
|
[PROFORMA_STATUS.APPROVED]: [PROFORMA_STATUS.ISSUED, PROFORMA_STATUS.DRAFT],
|
||||||
|
[PROFORMA_STATUS.REJECTED]: [PROFORMA_STATUS.DRAFT],
|
||||||
|
[PROFORMA_STATUS.ISSUED]: [],
|
||||||
|
};
|
||||||
|
|
||||||
export type ProformaStatus = `${PROFORMA_STATUS}`;
|
export type ProformaStatus = `${PROFORMA_STATUS}`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user