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 { Button, Spinner } from "@repo/shadcn-ui/components";
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
import { type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
|
||||
import { Spinner } from "@repo/shadcn-ui/components";
|
||||
import { PencilIcon, PowerIcon, PowerOffIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { PaymentMethodListRow } from "../../../../payment-methods/shared";
|
||||
@ -30,67 +30,52 @@ export const PaymentMethodRowActions = ({
|
||||
const toggleDisabled = paymentMethod.isSystem || paymentMethod.isDefault || isTogglingActive;
|
||||
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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
aria-label={t("settings.paymentMethods.actions.edit")}
|
||||
disabled={editDisabled}
|
||||
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>
|
||||
}
|
||||
<RdxRowActions
|
||||
overflowActions={overflowActions}
|
||||
overflowLabel={t("settings.paymentMethods.actions.more")}
|
||||
primaryActions={primaryActions}
|
||||
/>
|
||||
<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}`}
|
||||
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" />
|
||||
</div>
|
||||
<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 test from "node:test";
|
||||
|
||||
import { RdxRowActions, type RdxRowActionsProps } from "@repo/rdx-ui/components";
|
||||
import type { CellContext } from "@tanstack/react-table";
|
||||
import type { ComponentProps, ReactElement } from "react";
|
||||
import { isValidElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import type { ProformaListRow } from "../../../../shared";
|
||||
import { PROFORMA_STATUS } from "../../../../shared";
|
||||
|
||||
import { ProformaRowActions } from "./proforma-row-actions";
|
||||
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
||||
|
||||
const proformaFixture: ProformaListRow = {
|
||||
@ -50,17 +51,21 @@ const proformaFixture: ProformaListRow = {
|
||||
linkedInvoiceId: null,
|
||||
};
|
||||
|
||||
const getRowActionsProps = (
|
||||
const getRowActionsNode = (
|
||||
scope: Parameters<typeof useProformasGridColumns>[0],
|
||||
actionHandlers: Parameters<typeof useProformasGridColumns>[1] = {}
|
||||
): RdxRowActionsProps => {
|
||||
): ReactElement<ComponentProps<typeof ProformaRowActions>> => {
|
||||
let cellNode: unknown = null;
|
||||
|
||||
const Harness = () => {
|
||||
const columns = useProformasGridColumns(scope, actionHandlers);
|
||||
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 },
|
||||
} as unknown as CellContext<ProformaListRow, unknown>);
|
||||
|
||||
@ -70,87 +75,32 @@ const getRowActionsProps = (
|
||||
renderToStaticMarkup(<Harness />);
|
||||
|
||||
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", () => {
|
||||
const props = getRowActionsProps("normal", {
|
||||
test("delegates the action cell to ProformaRowActions", () => {
|
||||
const handlers = {
|
||||
onEditClick: () => undefined,
|
||||
onViewClick: () => undefined,
|
||||
});
|
||||
};
|
||||
const node = getRowActionsNode("normal", handlers);
|
||||
|
||||
assert.deepEqual(
|
||||
props.primaryActions?.map((action) => action.id),
|
||||
["view", "edit"]
|
||||
);
|
||||
assert.ok(["Más acciones", "common.more_actions"].includes(props.overflowLabel));
|
||||
assert.equal(node.props.proforma, proformaFixture);
|
||||
assert.equal(node.props.scope, "normal");
|
||||
assert.equal(node.props.actionHandlers, handlers);
|
||||
});
|
||||
|
||||
test("mantiene las destructivas en overflow y conserva disabled", () => {
|
||||
const props = getRowActionsProps("normal", {
|
||||
test("preserves the supplied handlers for archived and normal scopes", () => {
|
||||
const handlers = {
|
||||
onCreateRecurringClick: () => undefined,
|
||||
onDeleteClick: () => undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAction = props.overflowActions?.find((action) => action.id === "delete");
|
||||
const viewAction = props.primaryActions?.find((action) => action.id === "view");
|
||||
|
||||
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
|
||||
);
|
||||
assert.equal(getRowActionsNode("normal", handlers).props.actionHandlers, handlers);
|
||||
assert.equal(getRowActionsNode("archived", handlers).props.actionHandlers, handlers);
|
||||
});
|
||||
|
||||
@ -1,62 +1,20 @@
|
||||
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 { Checkbox, Spinner } from "@repo/shadcn-ui/components";
|
||||
import { Checkbox } from "@repo/shadcn-ui/components";
|
||||
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 { useTranslation } from "../../../../../i18n";
|
||||
import {
|
||||
PROFORMA_STATUS,
|
||||
PROFORMA_STATUS_TRANSITIONS,
|
||||
type ProformaListRow,
|
||||
type ProformaStatus,
|
||||
} from "../../../../shared";
|
||||
import type { ProformaListRow } from "../../../../shared";
|
||||
import type { ProformaListScope } from "../../../types/proforma-list-filters";
|
||||
import { ProformaStatusBadge } from "../../components";
|
||||
|
||||
type GridActionHandlers = {
|
||||
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" />;
|
||||
import { type ProformaRowActionHandlers, ProformaRowActions } from "./proforma-row-actions";
|
||||
|
||||
export function useProformasGridColumns(
|
||||
scope: ProformaListScope,
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
actionHandlers: ProformaRowActionHandlers = {}
|
||||
): ColumnDef<ProformaListRow, unknown>[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -280,207 +238,13 @@ export function useProformasGridColumns(
|
||||
header: "Acciones",
|
||||
enableSorting: false,
|
||||
|
||||
cell: ({ row }) => {
|
||||
const proforma = row.original;
|
||||
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}
|
||||
cell: ({ row }) => (
|
||||
<ProformaRowActions
|
||||
actionHandlers={actionHandlers}
|
||||
proforma={row.original}
|
||||
scope={scope}
|
||||
/>
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, actionHandlers, scope]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user