feat: implementar acciones de fila para proformas y refactorizar la lógica de acciones en columnas

This commit is contained in:
David Arranz 2026-09-05 11:22:43 +02:00
parent 021c19fc40
commit 3bf08fccec
5 changed files with 341 additions and 388 deletions

View File

@ -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>
); );
}; };

View File

@ -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">

View File

@ -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}
/>
);
}

View File

@ -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
);
}); });

View File

@ -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]