feat: implementar acciones de fila para proformas y refactorizar la lógica de acciones en columnas
This commit is contained in:
parent
021c19fc40
commit
3bf08fccec
@ -1,6 +1,6 @@
|
|||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@repo/shadcn-ui/components";
|
import { type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
|
||||||
import { Button, Spinner } from "@repo/shadcn-ui/components";
|
import { Spinner } from "@repo/shadcn-ui/components";
|
||||||
import { MoreHorizontalIcon } from "lucide-react";
|
import { PencilIcon, PowerIcon, PowerOffIcon, Trash2Icon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { PaymentMethodListRow } from "../../../../payment-methods/shared";
|
import type { PaymentMethodListRow } from "../../../../payment-methods/shared";
|
||||||
@ -30,67 +30,52 @@ export const PaymentMethodRowActions = ({
|
|||||||
const toggleDisabled = paymentMethod.isSystem || paymentMethod.isDefault || isTogglingActive;
|
const toggleDisabled = paymentMethod.isSystem || paymentMethod.isDefault || isTogglingActive;
|
||||||
const deleteDisabled = paymentMethod.isSystem || paymentMethod.isDefault;
|
const deleteDisabled = paymentMethod.isSystem || paymentMethod.isDefault;
|
||||||
|
|
||||||
|
const primaryActions: RdxRowAction[] = [
|
||||||
|
{
|
||||||
|
id: "edit",
|
||||||
|
label: t("settings.paymentMethods.actions.edit"),
|
||||||
|
icon: <PencilIcon aria-hidden className="size-4" />,
|
||||||
|
disabled: editDisabled,
|
||||||
|
disabledReason: editDisabled
|
||||||
|
? t("settings.paymentMethods.protections.systemEdit")
|
||||||
|
: undefined,
|
||||||
|
onSelect: () => onEdit(paymentMethod),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "toggle-active",
|
||||||
|
label: paymentMethod.isActive
|
||||||
|
? t("settings.paymentMethods.actions.disable")
|
||||||
|
: t("settings.paymentMethods.actions.enable"),
|
||||||
|
icon: isTogglingActive ? (
|
||||||
|
<Spinner aria-hidden className="size-4" />
|
||||||
|
) : paymentMethod.isActive ? (
|
||||||
|
<PowerOffIcon aria-hidden className="size-4" />
|
||||||
|
) : (
|
||||||
|
<PowerIcon aria-hidden className="size-4" />
|
||||||
|
),
|
||||||
|
disabled: toggleDisabled,
|
||||||
|
disabledReason: toggleDisabled ? activeDisabledReason : undefined,
|
||||||
|
onSelect: () => onToggleActive(paymentMethod),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const overflowActions: RdxRowAction[] = [
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
label: t("settings.paymentMethods.actions.delete"),
|
||||||
|
icon: <Trash2Icon aria-hidden className="size-4" />,
|
||||||
|
destructive: true,
|
||||||
|
disabled: deleteDisabled,
|
||||||
|
disabledReason: deleteDisabled ? deleteDisabledReason : undefined,
|
||||||
|
onSelect: () => onDelete(paymentMethod),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<RdxRowActions
|
||||||
<Button
|
overflowActions={overflowActions}
|
||||||
aria-label={t("settings.paymentMethods.actions.edit")}
|
overflowLabel={t("settings.paymentMethods.actions.more")}
|
||||||
disabled={editDisabled}
|
primaryActions={primaryActions}
|
||||||
onClick={() => onEdit(paymentMethod)}
|
/>
|
||||||
size="sm"
|
|
||||||
title={
|
|
||||||
editDisabled ? t("settings.paymentMethods.protections.systemEdit") : undefined
|
|
||||||
}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{t("settings.paymentMethods.actions.edit")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
aria-label={
|
|
||||||
paymentMethod.isActive
|
|
||||||
? t("settings.paymentMethods.actions.disable")
|
|
||||||
: t("settings.paymentMethods.actions.enable")
|
|
||||||
}
|
|
||||||
aria-busy={isTogglingActive}
|
|
||||||
disabled={toggleDisabled}
|
|
||||||
onClick={() => onToggleActive(paymentMethod)}
|
|
||||||
size="sm"
|
|
||||||
title={toggleDisabled ? activeDisabledReason : undefined}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{isTogglingActive ? <Spinner aria-hidden className="mr-2 size-4" /> : null}
|
|
||||||
{paymentMethod.isActive
|
|
||||||
? t("settings.paymentMethods.actions.disable")
|
|
||||||
: t("settings.paymentMethods.actions.enable")}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
aria-label={t("settings.paymentMethods.actions.more")}
|
|
||||||
size="icon"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<MoreHorizontalIcon className="size-4" />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
|
||||||
className="w-48"
|
|
||||||
render={
|
|
||||||
<DropdownMenuItem
|
|
||||||
disabled={deleteDisabled}
|
|
||||||
onClick={() => onDelete(paymentMethod)}
|
|
||||||
title={deleteDisabled ? deleteDisabledReason : undefined}
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
{t("settings.paymentMethods.actions.delete")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export const PaymentMethodsSettingsPage = () => {
|
|||||||
id={`payment-method-default-${paymentMethod.id}`}
|
id={`payment-method-default-${paymentMethod.id}`}
|
||||||
value={paymentMethod.id}
|
value={paymentMethod.id}
|
||||||
/>
|
/>
|
||||||
<div className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
<div className="hidden flex size-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||||
<CreditCardIcon aria-hidden className="size-5" />
|
<CreditCardIcon aria-hidden className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 space-y-1">
|
<div className="min-w-0 flex-1 space-y-1">
|
||||||
|
|||||||
@ -0,0 +1,254 @@
|
|||||||
|
import { type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
|
||||||
|
import { Spinner } from "@repo/shadcn-ui/components";
|
||||||
|
import {
|
||||||
|
CalendarSyncIcon,
|
||||||
|
CopyIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
EyeIcon,
|
||||||
|
FileDownIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
FolderArchiveIcon,
|
||||||
|
PencilIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
|
Trash2Icon,
|
||||||
|
Undo2Icon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { ComponentType } from "react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../../i18n";
|
||||||
|
import {
|
||||||
|
PROFORMA_STATUS,
|
||||||
|
PROFORMA_STATUS_TRANSITIONS,
|
||||||
|
type ProformaListRow,
|
||||||
|
type ProformaStatus,
|
||||||
|
} from "../../../../shared";
|
||||||
|
import type { ProformaListScope } from "../../../types/proforma-list-filters";
|
||||||
|
|
||||||
|
export interface ProformaRowActionHandlers {
|
||||||
|
onViewClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onEditClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onArchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
||||||
|
onLinkedInvoiceClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onDownloadPdf?: (proforma: ProformaListRow) => void;
|
||||||
|
pdfDownloadingId?: string | null;
|
||||||
|
isPdfDownloading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProformaRowActionsProps {
|
||||||
|
proforma: ProformaListRow;
|
||||||
|
scope: ProformaListScope;
|
||||||
|
actionHandlers?: ProformaRowActionHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
|
||||||
|
loading ? <Spinner aria-hidden className="size-4" /> : <Icon aria-hidden className="size-4" />;
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <Mantiene juntas las reglas ya existentes de disponibilidad de las acciones de proformas.>
|
||||||
|
export function ProformaRowActions({
|
||||||
|
proforma,
|
||||||
|
scope,
|
||||||
|
actionHandlers = {},
|
||||||
|
}: ProformaRowActionsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isDraft = proforma.status === PROFORMA_STATUS.DRAFT;
|
||||||
|
const isIssued = proforma.status === PROFORMA_STATUS.ISSUED;
|
||||||
|
const isApproved = proforma.status === PROFORMA_STATUS.APPROVED;
|
||||||
|
const isNormalScope = scope === "normal";
|
||||||
|
const isArchivedScope = scope === "archived";
|
||||||
|
const isDeletedScope = scope === "deleted";
|
||||||
|
const isArchivable =
|
||||||
|
isNormalScope &&
|
||||||
|
!proforma.isArchived &&
|
||||||
|
(proforma.status === PROFORMA_STATUS.DRAFT || proforma.status === PROFORMA_STATUS.REJECTED);
|
||||||
|
const isUnarchivable =
|
||||||
|
isArchivedScope &&
|
||||||
|
proforma.isArchived &&
|
||||||
|
(proforma.status === PROFORMA_STATUS.DRAFT || proforma.status === PROFORMA_STATUS.REJECTED);
|
||||||
|
const availableTransitions = isNormalScope
|
||||||
|
? (PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const isPDFLoading =
|
||||||
|
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id;
|
||||||
|
const nextTransition = availableTransitions[0] ?? null;
|
||||||
|
|
||||||
|
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
||||||
|
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
||||||
|
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
||||||
|
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
|
||||||
|
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
||||||
|
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
||||||
|
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
||||||
|
const handleDownloadPdf = () => actionHandlers.onDownloadPdf?.(proforma);
|
||||||
|
const handleDelete = () => actionHandlers.onDeleteClick?.(proforma);
|
||||||
|
const handleChangeStatus = () => {
|
||||||
|
if (!nextTransition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionHandlers.onChangeStatusClick?.(proforma, nextTransition);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScopeReason = () => {
|
||||||
|
if (isArchivedScope) {
|
||||||
|
return t("pages.proformas.list.actions.disabled_reasons.archived_scope");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeletedScope) {
|
||||||
|
return t("pages.proformas.list.actions.disabled_reasons.deleted_scope");
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleActions: RdxRowAction[] = [
|
||||||
|
{
|
||||||
|
id: "view",
|
||||||
|
label: t("pages.proformas.list.actions.view"),
|
||||||
|
icon: renderActionIcon(EyeIcon),
|
||||||
|
disabled: !actionHandlers.onViewClick || isDeletedScope,
|
||||||
|
disabledReason: isDeletedScope
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
||||||
|
: undefined,
|
||||||
|
onSelect: handleView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "edit",
|
||||||
|
label: t("pages.proformas.list.actions.edit"),
|
||||||
|
icon: renderActionIcon(PencilIcon),
|
||||||
|
disabled: !(actionHandlers.onEditClick && isNormalScope) || isIssued,
|
||||||
|
disabledReason: isNormalScope
|
||||||
|
? isIssued
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.edit_issued")
|
||||||
|
: undefined
|
||||||
|
: getScopeReason(),
|
||||||
|
onSelect: handleEdit,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const menuActions: RdxRowAction[] = [
|
||||||
|
{
|
||||||
|
id: "linked-invoice",
|
||||||
|
label: t("pages.proformas.list.actions.view_linked_invoice"),
|
||||||
|
icon: renderActionIcon(ExternalLinkIcon),
|
||||||
|
disabled: !(isIssued && proforma.linkedInvoiceId),
|
||||||
|
disabledReason:
|
||||||
|
isIssued && proforma.linkedInvoiceId
|
||||||
|
? undefined
|
||||||
|
: t("pages.proformas.list.actions.disabled_reasons.linked_invoice_unavailable"),
|
||||||
|
onSelect: () => {
|
||||||
|
if (proforma.linkedInvoiceId) {
|
||||||
|
window.location.href = "/customer-invoices";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "change-status",
|
||||||
|
label: t("pages.proformas.list.actions.change_status"),
|
||||||
|
icon: renderActionIcon(RefreshCwIcon),
|
||||||
|
disabled:
|
||||||
|
!(actionHandlers.onChangeStatusClick && isNormalScope && nextTransition) || isIssued,
|
||||||
|
disabledReason: isNormalScope
|
||||||
|
? isIssued
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.status_issued")
|
||||||
|
: nextTransition
|
||||||
|
? undefined
|
||||||
|
: t("proformas.change_proforma_status_dialog.empty_available_statuses")
|
||||||
|
: getScopeReason(),
|
||||||
|
onSelect: handleChangeStatus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "issue",
|
||||||
|
label: t("pages.proformas.list.actions.issue"),
|
||||||
|
icon: renderActionIcon(FileTextIcon),
|
||||||
|
disabled: !(actionHandlers.onIssueClick && isNormalScope && isApproved) || isIssued,
|
||||||
|
disabledReason: isNormalScope
|
||||||
|
? isApproved
|
||||||
|
? undefined
|
||||||
|
: t("proformas.issue_proforma_dialog.not_allowed_description")
|
||||||
|
: getScopeReason(),
|
||||||
|
onSelect: handleIssue,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "duplicate",
|
||||||
|
label: t("pages.proformas.list.actions.duplicate"),
|
||||||
|
icon: renderActionIcon(CopyIcon),
|
||||||
|
disabled: !actionHandlers.onDuplicateClick || isDeletedScope,
|
||||||
|
disabledReason: isDeletedScope
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
||||||
|
: undefined,
|
||||||
|
onSelect: handleDuplicate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "create-recurring",
|
||||||
|
label: t("pages.proformas.list.actions.create_recurring"),
|
||||||
|
icon: renderActionIcon(CalendarSyncIcon),
|
||||||
|
disabled: !(actionHandlers.onCreateRecurringClick && isNormalScope),
|
||||||
|
disabledReason: isNormalScope ? undefined : getScopeReason(),
|
||||||
|
onSelect: handleCreateRecurring,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "archive",
|
||||||
|
label: t("pages.proformas.list.actions.archive"),
|
||||||
|
icon: renderActionIcon(FolderArchiveIcon),
|
||||||
|
disabled: !(actionHandlers.onArchiveClick && isArchivable),
|
||||||
|
disabledReason: actionHandlers.onArchiveClick
|
||||||
|
? isNormalScope
|
||||||
|
? t("proformas.archive.error_conflict_message")
|
||||||
|
: getScopeReason()
|
||||||
|
: undefined,
|
||||||
|
onSelect: handleArchive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "unarchive",
|
||||||
|
label: t("pages.proformas.list.actions.unarchive"),
|
||||||
|
icon: renderActionIcon(Undo2Icon),
|
||||||
|
disabled: !(actionHandlers.onUnarchiveClick && isUnarchivable),
|
||||||
|
disabledReason: actionHandlers.onUnarchiveClick
|
||||||
|
? isArchivedScope
|
||||||
|
? t("proformas.unarchive.error_conflict_message")
|
||||||
|
: t("pages.proformas.list.actions.disabled_reasons.archived_only")
|
||||||
|
: undefined,
|
||||||
|
onSelect: handleUnarchive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "download-pdf",
|
||||||
|
label: t("pages.proformas.list.actions.download_pdf"),
|
||||||
|
icon: renderActionIcon(FileDownIcon, isPDFLoading),
|
||||||
|
disabled: Boolean(!actionHandlers.onDownloadPdf || isDeletedScope || isPDFLoading),
|
||||||
|
disabledReason: isDeletedScope
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
||||||
|
: isPDFLoading
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.operation_in_progress")
|
||||||
|
: undefined,
|
||||||
|
onSelect: handleDownloadPdf,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
label: t("pages.proformas.list.actions.delete"),
|
||||||
|
icon: renderActionIcon(Trash2Icon),
|
||||||
|
destructive: true,
|
||||||
|
disabled: !actionHandlers.onDeleteClick || isDeletedScope || !isDraft,
|
||||||
|
disabledReason: isDeletedScope
|
||||||
|
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
||||||
|
: isDraft
|
||||||
|
? undefined
|
||||||
|
: t("proformas.delete_proforma_dialog.error_conflict_message"),
|
||||||
|
onSelect: handleDelete,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RdxRowActions
|
||||||
|
overflowActions={menuActions}
|
||||||
|
overflowLabel={t("common.more_actions")}
|
||||||
|
primaryActions={visibleActions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import { RdxRowActions, type RdxRowActionsProps } from "@repo/rdx-ui/components";
|
|
||||||
import type { CellContext } from "@tanstack/react-table";
|
import type { CellContext } from "@tanstack/react-table";
|
||||||
|
import type { ComponentProps, ReactElement } from "react";
|
||||||
import { isValidElement } from "react";
|
import { isValidElement } from "react";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
import type { ProformaListRow } from "../../../../shared";
|
import type { ProformaListRow } from "../../../../shared";
|
||||||
import { PROFORMA_STATUS } from "../../../../shared";
|
import { PROFORMA_STATUS } from "../../../../shared";
|
||||||
|
|
||||||
|
import { ProformaRowActions } from "./proforma-row-actions";
|
||||||
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
||||||
|
|
||||||
const proformaFixture: ProformaListRow = {
|
const proformaFixture: ProformaListRow = {
|
||||||
@ -50,17 +51,21 @@ const proformaFixture: ProformaListRow = {
|
|||||||
linkedInvoiceId: null,
|
linkedInvoiceId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRowActionsProps = (
|
const getRowActionsNode = (
|
||||||
scope: Parameters<typeof useProformasGridColumns>[0],
|
scope: Parameters<typeof useProformasGridColumns>[0],
|
||||||
actionHandlers: Parameters<typeof useProformasGridColumns>[1] = {}
|
actionHandlers: Parameters<typeof useProformasGridColumns>[1] = {}
|
||||||
): RdxRowActionsProps => {
|
): ReactElement<ComponentProps<typeof ProformaRowActions>> => {
|
||||||
let cellNode: unknown = null;
|
let cellNode: unknown = null;
|
||||||
|
|
||||||
const Harness = () => {
|
const Harness = () => {
|
||||||
const columns = useProformasGridColumns(scope, actionHandlers);
|
const columns = useProformasGridColumns(scope, actionHandlers);
|
||||||
const actionsColumn = columns.find((column) => column.id === "actions");
|
const actionsColumn = columns.find((column) => column.id === "actions");
|
||||||
|
|
||||||
cellNode = actionsColumn?.cell?.({
|
const cell = actionsColumn?.cell as
|
||||||
|
| ((context: CellContext<ProformaListRow, unknown>) => ReactElement)
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
cellNode = cell?.({
|
||||||
row: { original: proformaFixture },
|
row: { original: proformaFixture },
|
||||||
} as unknown as CellContext<ProformaListRow, unknown>);
|
} as unknown as CellContext<ProformaListRow, unknown>);
|
||||||
|
|
||||||
@ -70,87 +75,32 @@ const getRowActionsProps = (
|
|||||||
renderToStaticMarkup(<Harness />);
|
renderToStaticMarkup(<Harness />);
|
||||||
|
|
||||||
if (!isValidElement(cellNode)) {
|
if (!isValidElement(cellNode)) {
|
||||||
throw new Error("Expected RdxRowActions cell");
|
throw new Error("Expected ProformaRowActions cell");
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.equal(cellNode.type, RdxRowActions);
|
assert.equal(cellNode.type, ProformaRowActions);
|
||||||
|
|
||||||
return cellNode.props as RdxRowActionsProps;
|
return cellNode as ReactElement<ComponentProps<typeof ProformaRowActions>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
test("mantiene ver y editar como acciones visibles de referencia", () => {
|
test("delegates the action cell to ProformaRowActions", () => {
|
||||||
const props = getRowActionsProps("normal", {
|
const handlers = {
|
||||||
onEditClick: () => undefined,
|
onEditClick: () => undefined,
|
||||||
onViewClick: () => undefined,
|
onViewClick: () => undefined,
|
||||||
});
|
};
|
||||||
|
const node = getRowActionsNode("normal", handlers);
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.equal(node.props.proforma, proformaFixture);
|
||||||
props.primaryActions?.map((action) => action.id),
|
assert.equal(node.props.scope, "normal");
|
||||||
["view", "edit"]
|
assert.equal(node.props.actionHandlers, handlers);
|
||||||
);
|
|
||||||
assert.ok(["Más acciones", "common.more_actions"].includes(props.overflowLabel));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mantiene las destructivas en overflow y conserva disabled", () => {
|
test("preserves the supplied handlers for archived and normal scopes", () => {
|
||||||
const props = getRowActionsProps("normal", {
|
const handlers = {
|
||||||
|
onCreateRecurringClick: () => undefined,
|
||||||
onDeleteClick: () => undefined,
|
onDeleteClick: () => undefined,
|
||||||
});
|
};
|
||||||
|
|
||||||
const deleteAction = props.overflowActions?.find((action) => action.id === "delete");
|
assert.equal(getRowActionsNode("normal", handlers).props.actionHandlers, handlers);
|
||||||
const viewAction = props.primaryActions?.find((action) => action.id === "view");
|
assert.equal(getRowActionsNode("archived", handlers).props.actionHandlers, handlers);
|
||||||
|
|
||||||
assert.ok(deleteAction);
|
|
||||||
assert.equal(deleteAction?.destructive, true);
|
|
||||||
assert.equal(deleteAction?.disabled, false);
|
|
||||||
assert.equal(viewAction?.disabled, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserva callbacks de view, edit, crear recurrencia y delete", () => {
|
|
||||||
let didView = false;
|
|
||||||
let didEdit = false;
|
|
||||||
let didCreateRecurring = false;
|
|
||||||
let didDelete = false;
|
|
||||||
|
|
||||||
const props = getRowActionsProps("normal", {
|
|
||||||
onCreateRecurringClick: () => {
|
|
||||||
didCreateRecurring = true;
|
|
||||||
},
|
|
||||||
onDeleteClick: () => {
|
|
||||||
didDelete = true;
|
|
||||||
},
|
|
||||||
onEditClick: () => {
|
|
||||||
didEdit = true;
|
|
||||||
},
|
|
||||||
onViewClick: () => {
|
|
||||||
didView = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
props.primaryActions?.find((action) => action.id === "view")?.onSelect();
|
|
||||||
props.primaryActions?.find((action) => action.id === "edit")?.onSelect();
|
|
||||||
props.overflowActions?.find((action) => action.id === "create-recurring")?.onSelect();
|
|
||||||
props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
|
|
||||||
|
|
||||||
assert.equal(didView, true);
|
|
||||||
assert.equal(didEdit, true);
|
|
||||||
assert.equal(didCreateRecurring, true);
|
|
||||||
assert.equal(didDelete, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("solo permite crear recurrencia desde el listado normal", () => {
|
|
||||||
const normalProps = getRowActionsProps("normal", {
|
|
||||||
onCreateRecurringClick: () => undefined,
|
|
||||||
});
|
|
||||||
const archivedProps = getRowActionsProps("archived", {
|
|
||||||
onCreateRecurringClick: () => undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
normalProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
archivedProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,62 +1,20 @@
|
|||||||
import { EmptyCellValue } from "@erp/core/components";
|
import { EmptyCellValue } from "@erp/core/components";
|
||||||
import { DataTableColumnHeader, type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
|
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||||
import { DateHelper } from "@repo/rdx-utils";
|
import { DateHelper } from "@repo/rdx-utils";
|
||||||
import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
|
import { Checkbox } from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
|
||||||
CalendarSyncIcon,
|
|
||||||
CopyIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
EyeIcon,
|
|
||||||
FileDownIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
FolderArchiveIcon,
|
|
||||||
PencilIcon,
|
|
||||||
RefreshCwIcon,
|
|
||||||
Trash2Icon,
|
|
||||||
Undo2Icon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type { ComponentType } from "react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../../i18n";
|
import { useTranslation } from "../../../../../i18n";
|
||||||
import {
|
import type { ProformaListRow } from "../../../../shared";
|
||||||
PROFORMA_STATUS,
|
|
||||||
PROFORMA_STATUS_TRANSITIONS,
|
|
||||||
type ProformaListRow,
|
|
||||||
type ProformaStatus,
|
|
||||||
} from "../../../../shared";
|
|
||||||
import type { ProformaListScope } from "../../../types/proforma-list-filters";
|
import type { ProformaListScope } from "../../../types/proforma-list-filters";
|
||||||
import { ProformaStatusBadge } from "../../components";
|
import { ProformaStatusBadge } from "../../components";
|
||||||
|
|
||||||
type GridActionHandlers = {
|
import { type ProformaRowActionHandlers, ProformaRowActions } from "./proforma-row-actions";
|
||||||
onViewClick?: (proforma: ProformaListRow) => void;
|
|
||||||
onEditClick?: (proforma: ProformaListRow) => void;
|
|
||||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
|
||||||
onArchiveClick?: (proforma: ProformaListRow) => void;
|
|
||||||
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
|
||||||
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
|
|
||||||
onUnarchiveClick?: (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;
|
|
||||||
|
|
||||||
onDownloadPdf?: (proforma: ProformaListRow) => void;
|
|
||||||
pdfDownloadingId?: string | null;
|
|
||||||
isPdfDownloading?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
|
|
||||||
loading ? <Spinner className="size-4" /> : <Icon className="size-4" />;
|
|
||||||
|
|
||||||
export function useProformasGridColumns(
|
export function useProformasGridColumns(
|
||||||
scope: ProformaListScope,
|
scope: ProformaListScope,
|
||||||
actionHandlers: GridActionHandlers = {}
|
actionHandlers: ProformaRowActionHandlers = {}
|
||||||
): ColumnDef<ProformaListRow, unknown>[] {
|
): ColumnDef<ProformaListRow, unknown>[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -280,207 +238,13 @@ export function useProformasGridColumns(
|
|||||||
header: "Acciones",
|
header: "Acciones",
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => (
|
||||||
const proforma = row.original;
|
<ProformaRowActions
|
||||||
const isDraft = proforma.status === PROFORMA_STATUS.DRAFT;
|
actionHandlers={actionHandlers}
|
||||||
const isIssued = proforma.status === PROFORMA_STATUS.ISSUED;
|
proforma={row.original}
|
||||||
const isApproved = proforma.status === PROFORMA_STATUS.APPROVED;
|
scope={scope}
|
||||||
const isNormalScope = scope === "normal";
|
/>
|
||||||
const isArchivedScope = scope === "archived";
|
),
|
||||||
const isDeletedScope = scope === "deleted";
|
|
||||||
const isArchivable =
|
|
||||||
isNormalScope &&
|
|
||||||
!proforma.isArchived &&
|
|
||||||
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
|
||||||
proforma.status === PROFORMA_STATUS.REJECTED);
|
|
||||||
const isUnarchivable =
|
|
||||||
isArchivedScope &&
|
|
||||||
proforma.isArchived &&
|
|
||||||
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
|
||||||
proforma.status === PROFORMA_STATUS.REJECTED);
|
|
||||||
const availableTransitions = isNormalScope
|
|
||||||
? (PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [])
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const isPDFLoading =
|
|
||||||
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id;
|
|
||||||
const nextTransition = availableTransitions[0] ?? null;
|
|
||||||
|
|
||||||
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
|
||||||
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
|
||||||
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
|
||||||
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
|
|
||||||
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
|
||||||
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
|
||||||
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
|
||||||
const handleDownloadPdf = () => actionHandlers.onDownloadPdf?.(proforma);
|
|
||||||
const handleDelete = () => actionHandlers.onDeleteClick?.(proforma);
|
|
||||||
const handleChangeStatus = () => {
|
|
||||||
if (!nextTransition) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
actionHandlers.onChangeStatusClick?.(proforma, nextTransition);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getScopeReason = () => {
|
|
||||||
if (isArchivedScope) {
|
|
||||||
return t("pages.proformas.list.actions.disabled_reasons.archived_scope");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDeletedScope) {
|
|
||||||
return t("pages.proformas.list.actions.disabled_reasons.deleted_scope");
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const visibleActions: RdxRowAction[] = [
|
|
||||||
{
|
|
||||||
id: "view",
|
|
||||||
label: t("pages.proformas.list.actions.view"),
|
|
||||||
icon: renderActionIcon(EyeIcon),
|
|
||||||
disabled: !actionHandlers.onViewClick || isDeletedScope,
|
|
||||||
disabledReason: isDeletedScope
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
|
||||||
: undefined,
|
|
||||||
onSelect: handleView,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "edit",
|
|
||||||
label: t("pages.proformas.list.actions.edit"),
|
|
||||||
icon: renderActionIcon(PencilIcon),
|
|
||||||
disabled: !(actionHandlers.onEditClick && isNormalScope) || isIssued,
|
|
||||||
disabledReason: isNormalScope
|
|
||||||
? isIssued
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.edit_issued")
|
|
||||||
: undefined
|
|
||||||
: getScopeReason(),
|
|
||||||
onSelect: handleEdit,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const menuActions: RdxRowAction[] = [
|
|
||||||
{
|
|
||||||
id: "linked-invoice",
|
|
||||||
label: t("pages.proformas.list.actions.view_linked_invoice"),
|
|
||||||
icon: renderActionIcon(ExternalLinkIcon),
|
|
||||||
disabled: !(isIssued && proforma.linkedInvoiceId),
|
|
||||||
disabledReason:
|
|
||||||
isIssued && proforma.linkedInvoiceId
|
|
||||||
? undefined
|
|
||||||
: t("pages.proformas.list.actions.disabled_reasons.linked_invoice_unavailable"),
|
|
||||||
onSelect: () => {
|
|
||||||
if (proforma.linkedInvoiceId) {
|
|
||||||
window.location.href = "/customer-invoices";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "change-status",
|
|
||||||
label: t("pages.proformas.list.actions.change_status"),
|
|
||||||
icon: renderActionIcon(RefreshCwIcon),
|
|
||||||
disabled:
|
|
||||||
!(actionHandlers.onChangeStatusClick && isNormalScope && nextTransition) ||
|
|
||||||
isIssued,
|
|
||||||
disabledReason: isNormalScope
|
|
||||||
? isIssued
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.status_issued")
|
|
||||||
: nextTransition
|
|
||||||
? undefined
|
|
||||||
: t("proformas.change_proforma_status_dialog.empty_available_statuses")
|
|
||||||
: getScopeReason(),
|
|
||||||
onSelect: handleChangeStatus,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "issue",
|
|
||||||
label: t("pages.proformas.list.actions.issue"),
|
|
||||||
icon: renderActionIcon(FileTextIcon),
|
|
||||||
disabled: !(actionHandlers.onIssueClick && isNormalScope && isApproved) || isIssued,
|
|
||||||
disabledReason: isNormalScope
|
|
||||||
? isApproved
|
|
||||||
? undefined
|
|
||||||
: t("proformas.issue_proforma_dialog.not_allowed_description")
|
|
||||||
: getScopeReason(),
|
|
||||||
onSelect: handleIssue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "duplicate",
|
|
||||||
label: t("pages.proformas.list.actions.duplicate"),
|
|
||||||
icon: renderActionIcon(CopyIcon),
|
|
||||||
disabled: !actionHandlers.onDuplicateClick || isDeletedScope,
|
|
||||||
disabledReason: isDeletedScope
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
|
||||||
: undefined,
|
|
||||||
onSelect: handleDuplicate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "create-recurring",
|
|
||||||
label: t("pages.proformas.list.actions.create_recurring"),
|
|
||||||
icon: renderActionIcon(CalendarSyncIcon),
|
|
||||||
disabled: !(actionHandlers.onCreateRecurringClick && isNormalScope),
|
|
||||||
disabledReason: isNormalScope ? undefined : getScopeReason(),
|
|
||||||
onSelect: handleCreateRecurring,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "archive",
|
|
||||||
label: t("pages.proformas.list.actions.archive"),
|
|
||||||
icon: renderActionIcon(FolderArchiveIcon),
|
|
||||||
disabled: !(actionHandlers.onArchiveClick && isArchivable),
|
|
||||||
disabledReason: actionHandlers.onArchiveClick
|
|
||||||
? isNormalScope
|
|
||||||
? t("proformas.archive.error_conflict_message")
|
|
||||||
: getScopeReason()
|
|
||||||
: undefined,
|
|
||||||
onSelect: handleArchive,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "unarchive",
|
|
||||||
label: t("pages.proformas.list.actions.unarchive"),
|
|
||||||
icon: renderActionIcon(Undo2Icon),
|
|
||||||
disabled: !(actionHandlers.onUnarchiveClick && isUnarchivable),
|
|
||||||
disabledReason: actionHandlers.onUnarchiveClick
|
|
||||||
? isArchivedScope
|
|
||||||
? t("proformas.unarchive.error_conflict_message")
|
|
||||||
: t("pages.proformas.list.actions.disabled_reasons.archived_only")
|
|
||||||
: undefined,
|
|
||||||
onSelect: handleUnarchive,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "download-pdf",
|
|
||||||
label: t("pages.proformas.list.actions.download_pdf"),
|
|
||||||
icon: renderActionIcon(FileDownIcon, isPDFLoading),
|
|
||||||
disabled: Boolean(!actionHandlers.onDownloadPdf || isDeletedScope || isPDFLoading),
|
|
||||||
disabledReason: isDeletedScope
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
|
||||||
: isPDFLoading
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.operation_in_progress")
|
|
||||||
: undefined,
|
|
||||||
onSelect: handleDownloadPdf,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "delete",
|
|
||||||
label: t("pages.proformas.list.actions.delete"),
|
|
||||||
icon: renderActionIcon(Trash2Icon),
|
|
||||||
destructive: true,
|
|
||||||
disabled: !actionHandlers.onDeleteClick || isDeletedScope || !isDraft,
|
|
||||||
disabledReason: isDeletedScope
|
|
||||||
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
|
|
||||||
: isDraft
|
|
||||||
? undefined
|
|
||||||
: t("proformas.delete_proforma_dialog.error_conflict_message"),
|
|
||||||
onSelect: handleDelete,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RdxRowActions
|
|
||||||
overflowActions={menuActions}
|
|
||||||
overflowLabel={t("common.more_actions")}
|
|
||||||
primaryActions={visibleActions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t, actionHandlers, scope]
|
[t, actionHandlers, scope]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user