feat: enhance proforma status management and UI components
- Added new StatusBadge and StatusOptionCard components for better status representation. - Integrated status selection and change functionality in ChangeProformaStatusDialog. - Improved ProformasGrid to support metadata and selection actions. - Implemented bulk actions for proforma selection, including status change and deletion. - Updated DataTable components to support custom selection actions. - Enhanced ProformaStatusBadge to include specific styles for each status.
This commit is contained in:
parent
67ef358866
commit
c0ed1ca92c
@ -8,16 +8,16 @@ import {
|
||||
DialogTitle,
|
||||
Spinner,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ArrowRightIcon, CheckIcon, LockIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useTranslation } from "../../../i18n";
|
||||
import { PROFORMA_STATUS, type ProformaStatus } from "../../shared";
|
||||
import type { ChangeProformaStatusTarget } from "../entities";
|
||||
import { getProformaStatusIcon } from "../helpers";
|
||||
import { getProformaStatusColor, getProformaStatusIcon } from "../helpers";
|
||||
import { getCommonAvailableProformaStatuses } from "../utils";
|
||||
|
||||
import { StatusLegend, StatusNode, TimelineConnector } from "./components";
|
||||
|
||||
interface ChangeProformaStatusDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@ -26,43 +26,76 @@ interface ChangeProformaStatusDialogProps {
|
||||
onConfirm: (status: ProformaStatus) => void;
|
||||
}
|
||||
|
||||
const STATUS_FLOW = [
|
||||
{
|
||||
status: PROFORMA_STATUS.DRAFT,
|
||||
activeColor: "text-gray-700",
|
||||
activeBg: "bg-gray-100",
|
||||
activeBorder: "border-gray-300",
|
||||
activeRing: "ring-gray-200",
|
||||
},
|
||||
{
|
||||
status: PROFORMA_STATUS.SENT,
|
||||
activeColor: "text-yellow-700",
|
||||
activeBg: "bg-yellow-100",
|
||||
activeBorder: "border-yellow-300",
|
||||
activeRing: "ring-yellow-200",
|
||||
},
|
||||
{
|
||||
status: PROFORMA_STATUS.REJECTED,
|
||||
activeColor: "text-red-700",
|
||||
activeBg: "bg-red-100",
|
||||
activeBorder: "border-red-300",
|
||||
activeRing: "ring-red-200",
|
||||
},
|
||||
{
|
||||
status: PROFORMA_STATUS.APPROVED,
|
||||
activeColor: "text-green-700",
|
||||
activeBg: "bg-green-100",
|
||||
activeBorder: "border-green-300",
|
||||
activeRing: "ring-green-200",
|
||||
},
|
||||
{
|
||||
status: PROFORMA_STATUS.ISSUED,
|
||||
activeColor: "text-blue-700",
|
||||
activeBg: "bg-blue-100",
|
||||
activeBorder: "border-blue-300",
|
||||
activeRing: "ring-blue-200",
|
||||
},
|
||||
] as const;
|
||||
const STATUS_ORDER: ProformaStatus[] = [
|
||||
PROFORMA_STATUS.DRAFT,
|
||||
PROFORMA_STATUS.SENT,
|
||||
PROFORMA_STATUS.APPROVED,
|
||||
PROFORMA_STATUS.REJECTED,
|
||||
PROFORMA_STATUS.ISSUED,
|
||||
];
|
||||
|
||||
const StatusBadge = ({ status }: { status: ProformaStatus }) => {
|
||||
const { t } = useTranslation();
|
||||
const Icon = getProformaStatusIcon(status);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full border px-3 py-1 text-sm font-medium",
|
||||
getProformaStatusColor(status)
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
{t(`catalog.proformas.status.${status}.label`)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface StatusOptionCardProps {
|
||||
status: ProformaStatus;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const StatusOptionCard = ({ status, selected, onClick }: StatusOptionCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const Icon = getProformaStatusIcon(status);
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-checked={selected}
|
||||
className={cn(
|
||||
"flex w-full items-start gap-3 rounded-lg border p-4 text-left transition-colors",
|
||||
selected
|
||||
? "border-primary bg-primary/5 ring-1 ring-primary"
|
||||
: "hover:border-foreground/20 hover:bg-muted/50"
|
||||
)}
|
||||
onClick={onClick}
|
||||
role="radio"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-full",
|
||||
selected ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 font-medium text-foreground">
|
||||
<span>{t(`catalog.proformas.status.${status}.label`)}</span>
|
||||
{selected ? <CheckIcon aria-hidden="true" className="size-4 text-primary" /> : null}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(`catalog.proformas.status.${status}.description`)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangeProformaStatusDialog = ({
|
||||
open,
|
||||
@ -81,33 +114,123 @@ export const ChangeProformaStatusDialog = ({
|
||||
}, [open]);
|
||||
|
||||
const availableStatuses = useMemo(() => getCommonAvailableProformaStatuses(targets), [targets]);
|
||||
const orderedAvailableStatuses = useMemo(
|
||||
() => STATUS_ORDER.filter((status) => availableStatuses.includes(status)),
|
||||
[availableStatuses]
|
||||
);
|
||||
|
||||
const currentStatus = targets.length === 1 ? targets[0].status : null;
|
||||
const isSingleTarget = targets.length === 1;
|
||||
const currentTarget = isSingleTarget ? targets[0] : null;
|
||||
const currentStatus = currentTarget?.status ?? null;
|
||||
|
||||
const getStatusType = (
|
||||
status: ProformaStatus
|
||||
): "past" | "current" | "available" | "unavailable" => {
|
||||
if (currentStatus === status) {
|
||||
return "current";
|
||||
}
|
||||
const emptyState = (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4 text-sm">
|
||||
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-muted">
|
||||
<LockIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{t("proformas.change_proforma_status_dialog.empty_title", "No hay cambios disponibles")}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{t("proformas.change_proforma_status_dialog.empty_available_statuses")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!currentStatus) {
|
||||
return availableStatuses.includes(status) ? "available" : "unavailable";
|
||||
}
|
||||
const singleTargetContent = currentTarget ? (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-3 rounded-lg border bg-muted/40 p-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("proformas.change_proforma_status_dialog.current_status", "Estado actual")}
|
||||
</span>
|
||||
<StatusBadge status={currentTarget.status} />
|
||||
</div>
|
||||
|
||||
const currentIndex = STATUS_FLOW.findIndex((item) => item.status === currentStatus);
|
||||
const statusIndex = STATUS_FLOW.findIndex((item) => item.status === status);
|
||||
{orderedAvailableStatuses.length === 0 ? (
|
||||
emptyState
|
||||
) : (
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="text-sm font-medium">
|
||||
{t(
|
||||
"proformas.change_proforma_status_dialog.select_new_status",
|
||||
"Selecciona el nuevo estado"
|
||||
)}
|
||||
</legend>
|
||||
|
||||
if (statusIndex < currentIndex) {
|
||||
return "past";
|
||||
}
|
||||
<div
|
||||
aria-label={t(
|
||||
"proformas.change_proforma_status_dialog.select_new_status",
|
||||
"Selecciona el nuevo estado"
|
||||
)}
|
||||
role="radiogroup"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{orderedAvailableStatuses.map((status) => (
|
||||
<StatusOptionCard
|
||||
key={status}
|
||||
onClick={() => setSelectedStatus(status)}
|
||||
selected={selectedStatus === status}
|
||||
status={status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if (availableStatuses.includes(status)) {
|
||||
return "available";
|
||||
}
|
||||
{selectedStatus ? (
|
||||
<div className="flex items-center justify-center gap-3 rounded-lg bg-muted/40 p-3">
|
||||
<StatusBadge status={currentTarget.status} />
|
||||
<ArrowRightIcon aria-hidden="true" className="size-4 text-muted-foreground" />
|
||||
<StatusBadge status={selectedStatus} />
|
||||
</div>
|
||||
) : null}
|
||||
</fieldset>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return "unavailable";
|
||||
};
|
||||
const bulkContent =
|
||||
orderedAvailableStatuses.length === 0 ? (
|
||||
emptyState
|
||||
) : (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="rounded-lg border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||
{t(
|
||||
"proformas.change_proforma_status_dialog.bulk_help",
|
||||
"Selecciona el estado que quieres aplicar a todas las proformas seleccionadas."
|
||||
)}
|
||||
</div>
|
||||
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="text-sm font-medium">
|
||||
{t(
|
||||
"proformas.change_proforma_status_dialog.select_new_status",
|
||||
"Selecciona el nuevo estado"
|
||||
)}
|
||||
</legend>
|
||||
|
||||
<div
|
||||
aria-label={t(
|
||||
"proformas.change_proforma_status_dialog.select_new_status",
|
||||
"Selecciona el nuevo estado"
|
||||
)}
|
||||
role="radiogroup"
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{orderedAvailableStatuses.map((status) => (
|
||||
<StatusOptionCard
|
||||
key={status}
|
||||
onClick={() => setSelectedStatus(status)}
|
||||
selected={selectedStatus === status}
|
||||
status={status}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -120,14 +243,14 @@ export const ChangeProformaStatusDialog = ({
|
||||
}}
|
||||
open={open}
|
||||
>
|
||||
<DialogContent className="sm:max-w-4xl">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("proformas.change_proforma_status_dialog.title")}</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
{targets.length === 1
|
||||
{isSingleTarget
|
||||
? t("proformas.change_proforma_status_dialog.single_description", {
|
||||
reference: targets[0].reference,
|
||||
reference: currentTarget?.reference,
|
||||
})
|
||||
: t("proformas.change_proforma_status_dialog.multiple_description", {
|
||||
count: targets.length,
|
||||
@ -135,62 +258,15 @@ export const ChangeProformaStatusDialog = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{availableStatuses.length === 0 ? (
|
||||
<div className="py-8 text-center text-sm text-muted-foreground">
|
||||
{t("proformas.change_proforma_status_dialog.empty_available_statuses")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6">
|
||||
<div className="relative">
|
||||
<div className="relative mb-8 grid grid-cols-5 gap-2">
|
||||
{STATUS_FLOW.map((statusConfig) => {
|
||||
const statusType = getStatusType(statusConfig.status);
|
||||
const isSelected = selectedStatus === statusConfig.status;
|
||||
const isClickable = statusType === "available";
|
||||
{isSingleTarget ? singleTargetContent : bulkContent}
|
||||
|
||||
return (
|
||||
<StatusNode
|
||||
activeBg={statusConfig.activeBg}
|
||||
activeBorder={statusConfig.activeBorder}
|
||||
activeColor={statusConfig.activeColor}
|
||||
activeRing={statusConfig.activeRing}
|
||||
description={t(`catalog.proformas.status.${statusConfig.status}.description`)}
|
||||
icon={getProformaStatusIcon(statusConfig.status)}
|
||||
isSelected={isSelected}
|
||||
key={statusConfig.status}
|
||||
label={t(`catalog.proformas.status.${statusConfig.status}.label`)}
|
||||
onClick={() => {
|
||||
if (isClickable) {
|
||||
setSelectedStatus(statusConfig.status);
|
||||
}
|
||||
}}
|
||||
status={statusConfig.status}
|
||||
statusType={statusType}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<TimelineConnector
|
||||
nodeCount={STATUS_FLOW.length}
|
||||
statusColors={STATUS_FLOW.map((statusConfig) => ({
|
||||
bgClass: statusConfig.activeBg,
|
||||
statusType: getStatusType(statusConfig.status),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatusLegend hasCurrentStatus={currentStatus !== null} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="sm:justify-between">
|
||||
<DialogFooter>
|
||||
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline">
|
||||
{t("proformas.change_proforma_status_dialog.cancel")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!selectedStatus || availableStatuses.length === 0 || isSubmitting}
|
||||
disabled={!selectedStatus || orderedAvailableStatuses.length === 0 || isSubmitting}
|
||||
onClick={() => {
|
||||
if (!selectedStatus) {
|
||||
return;
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { DataTable, type DataTableSort, SkeletonDataTable } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
DataTable,
|
||||
type DataTableMeta,
|
||||
type DataTableSort,
|
||||
SkeletonDataTable,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import type { ColumnDef, OnChangeFn, VisibilityState } from "@tanstack/react-table";
|
||||
import type * as React from "react";
|
||||
|
||||
@ -12,6 +17,7 @@ interface ProformasGridProps {
|
||||
searching?: boolean;
|
||||
|
||||
columns: ColumnDef<ProformaListRow, unknown>[];
|
||||
meta?: DataTableMeta<ProformaListRow>;
|
||||
topContent?: React.ReactNode;
|
||||
|
||||
pageIndex: number;
|
||||
@ -31,6 +37,7 @@ interface ProformasGridProps {
|
||||
export const ProformasGrid = ({
|
||||
data,
|
||||
columns,
|
||||
meta,
|
||||
topContent,
|
||||
|
||||
loading,
|
||||
@ -75,6 +82,7 @@ export const ProformasGrid = ({
|
||||
isSearching={searching}
|
||||
manualPagination
|
||||
manualSorting
|
||||
meta={meta}
|
||||
onColumnVisibilityChange={onColumnVisibilityChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
|
||||
@ -3,8 +3,14 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import { DateHelper } from "@repo/rdx-utils";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -18,6 +24,7 @@ import {
|
||||
FileDownIcon,
|
||||
FileTextIcon,
|
||||
FolderArchiveIcon,
|
||||
MoreHorizontalIcon,
|
||||
PencilIcon,
|
||||
RefreshCwIcon,
|
||||
Trash2Icon,
|
||||
@ -103,7 +110,7 @@ export function useProformasGridColumns(
|
||||
/>
|
||||
),
|
||||
meta: {
|
||||
cellClassName: "font-medium",
|
||||
cellClassName: "font-mono text-sm font-medium",
|
||||
},
|
||||
},
|
||||
|
||||
@ -119,6 +126,9 @@ export function useProformasGridColumns(
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => DateHelper.format(row.original.proformaDate),
|
||||
meta: {
|
||||
cellClassName: "text-muted-foreground",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@ -132,34 +142,9 @@ export function useProformasGridColumns(
|
||||
cell: ({ row }) => {
|
||||
const proforma = row.original;
|
||||
|
||||
const isIssued = proforma.status === "issued";
|
||||
const invoiceId = proforma.linkedInvoiceId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ProformaStatusBadge status={proforma.status} />
|
||||
{/* Enlace discreto a factura real */}
|
||||
{isIssued && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-6 text-foreground hover:text-primary"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<a href={`/facturas/${invoiceId}`}>
|
||||
<ExternalLinkIcon />
|
||||
<span className="sr-only">Ver factura {invoiceId}</span>
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Ver factura {invoiceId}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -216,7 +201,7 @@ export function useProformasGridColumns(
|
||||
<EmptyCellValue />
|
||||
),
|
||||
meta: {
|
||||
cellClassName: "hidden lg:table-cell max-w-16",
|
||||
cellClassName: "hidden lg:table-cell max-w-16 text-muted-foreground",
|
||||
headerClassName: "hidden lg:table-cell",
|
||||
},
|
||||
},
|
||||
@ -243,7 +228,8 @@ export function useProformasGridColumns(
|
||||
header: "Dtos",
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
cellClassName: "hidden 2xl:table-cell text-right tabular-nums font-medium",
|
||||
cellClassName:
|
||||
"hidden 2xl:table-cell text-right tabular-nums font-medium text-muted-foreground",
|
||||
headerClassName: "hidden 2xl:table-cell text-right",
|
||||
},
|
||||
},
|
||||
@ -319,224 +305,273 @@ export function useProformasGridColumns(
|
||||
const isPDFLoading =
|
||||
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id;
|
||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||
const nextTransition = availableTransitions[0] ?? null;
|
||||
|
||||
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
||||
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(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 primaryAction = (() => {
|
||||
if (isIssued && proforma.linkedInvoiceId) {
|
||||
return {
|
||||
href: `/issed-invoices/${proforma.linkedInvoiceId}`,
|
||||
icon: ExternalLinkIcon,
|
||||
label: "Ver factura",
|
||||
};
|
||||
}
|
||||
|
||||
if (canIssue) {
|
||||
return {
|
||||
icon: FileTextIcon,
|
||||
label: "Emitir factura",
|
||||
onClick: handleIssue,
|
||||
};
|
||||
}
|
||||
|
||||
if (isUnarchivable) {
|
||||
return {
|
||||
icon: Undo2Icon,
|
||||
label: "Desarchivar",
|
||||
onClick: handleUnarchive,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDraft && nextTransition && actionHandlers.onChangeStatusClick) {
|
||||
return {
|
||||
icon: RefreshCwIcon,
|
||||
label: "Cambiar estado",
|
||||
onClick: handleChangeStatus,
|
||||
};
|
||||
}
|
||||
|
||||
if (canEdit) {
|
||||
return {
|
||||
icon: PencilIcon,
|
||||
label: "Editar",
|
||||
onClick: handleEdit,
|
||||
};
|
||||
}
|
||||
|
||||
if (nextTransition && actionHandlers.onChangeStatusClick) {
|
||||
return {
|
||||
icon: RefreshCwIcon,
|
||||
label: "Cambiar estado",
|
||||
onClick: handleChangeStatus,
|
||||
};
|
||||
}
|
||||
|
||||
if (isArchivable && actionHandlers.onArchiveClick) {
|
||||
return {
|
||||
icon: FolderArchiveIcon,
|
||||
label: "Archivar",
|
||||
onClick: handleArchive,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
const quickAction = (() => {
|
||||
if (primaryAction?.label !== "Editar" && canEdit) {
|
||||
return {
|
||||
icon: PencilIcon,
|
||||
label: "Editar",
|
||||
loading: false,
|
||||
onClick: handleEdit,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
const menuActions = [
|
||||
!isIssued &&
|
||||
nextTransition &&
|
||||
actionHandlers.onChangeStatusClick &&
|
||||
primaryAction?.label !== "Cambiar estado"
|
||||
? {
|
||||
icon: RefreshCwIcon,
|
||||
label: "Cambiar estado",
|
||||
onClick: handleChangeStatus,
|
||||
}
|
||||
: null,
|
||||
canIssue && primaryAction?.label !== "Emitir factura"
|
||||
? {
|
||||
icon: FileTextIcon,
|
||||
label: "Emitir factura",
|
||||
onClick: handleIssue,
|
||||
}
|
||||
: null,
|
||||
canEdit && primaryAction?.label !== "Editar" && quickAction?.label !== "Editar"
|
||||
? {
|
||||
icon: PencilIcon,
|
||||
label: "Editar",
|
||||
onClick: handleEdit,
|
||||
}
|
||||
: null,
|
||||
canDuplicate
|
||||
? {
|
||||
icon: CopyIcon,
|
||||
label: "Duplicar",
|
||||
onClick: handleDuplicate,
|
||||
}
|
||||
: null,
|
||||
isArchivable && actionHandlers.onArchiveClick && primaryAction?.label !== "Archivar"
|
||||
? {
|
||||
icon: FolderArchiveIcon,
|
||||
label: "Archivar",
|
||||
onClick: handleArchive,
|
||||
}
|
||||
: null,
|
||||
isUnarchivable &&
|
||||
actionHandlers.onUnarchiveClick &&
|
||||
primaryAction?.label !== "Desarchivar"
|
||||
? {
|
||||
icon: Undo2Icon,
|
||||
label: "Desarchivar",
|
||||
onClick: handleUnarchive,
|
||||
}
|
||||
: null,
|
||||
canDownloadPdf
|
||||
? {
|
||||
icon: FileDownIcon,
|
||||
label: "Descargar PDF",
|
||||
onClick: handleDownloadPdf,
|
||||
loading: isPDFLoading,
|
||||
}
|
||||
: null,
|
||||
canDelete
|
||||
? {
|
||||
destructive: true,
|
||||
icon: Trash2Icon,
|
||||
label: "Eliminar",
|
||||
onClick: handleDelete,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
{canEdit && (
|
||||
<div className="flex items-center justify-end gap-1 [@media(hover:hover)_and_(pointer:fine)]:pointer-events-none [@media(hover:hover)_and_(pointer:fine)]:opacity-0 [@media(hover:hover)_and_(pointer:fine)]:transition-opacity [@media(hover:hover)_and_(pointer:fine)]:duration-150 [@media(hover:hover)_and_(pointer:fine)]:group-hover:pointer-events-auto [@media(hover:hover)_and_(pointer:fine)]:group-hover:opacity-100 [@media(hover:hover)_and_(pointer:fine)]:group-focus-within:pointer-events-auto [@media(hover:hover)_and_(pointer:fine)]:group-focus-within:opacity-100">
|
||||
{primaryAction && "href" in primaryAction ? (
|
||||
<Button className="h-8 gap-2 px-3" size="sm" variant="outline">
|
||||
<a
|
||||
href={primaryAction.href}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<primaryAction.icon className="size-4" />
|
||||
<span>{primaryAction.label}</span>
|
||||
</a>
|
||||
</Button>
|
||||
) : primaryAction ? (
|
||||
<Button
|
||||
className="h-8 gap-2 px-3"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
primaryAction.onClick?.();
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<primaryAction.icon className="size-4" />
|
||||
<span>{primaryAction.label}</span>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{quickAction ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary "
|
||||
className="size-8 text-muted-foreground hover:text-primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
actionHandlers.onEditClick?.(proforma);
|
||||
quickAction.onClick();
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<PencilIcon className="size-4" />
|
||||
<span className="sr-only">Editar</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Editar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{canDuplicate && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
actionHandlers.onDuplicateClick?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
<span className="sr-only">Duplicar</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Duplicar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{isArchivable && actionHandlers.onArchiveClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
actionHandlers.onArchiveClick?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<FolderArchiveIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Archivar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{isUnarchivable && actionHandlers.onUnarchiveClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
actionHandlers.onUnarchiveClick?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Undo2Icon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Desarchivar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Cambiar estado */}
|
||||
{!(isDeletedScope || isIssued) &&
|
||||
availableTransitions.length > 0 &&
|
||||
actionHandlers.onChangeStatusClick && (
|
||||
<TooltipProvider key={availableTransitions[0]}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
actionHandlers.onChangeStatusClick?.(
|
||||
proforma,
|
||||
availableTransitions[0]
|
||||
);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
<span className="sr-only">Cambiar estado</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Emitir factura: solo si approved */}
|
||||
{canIssue && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
actionHandlers.onIssueClick?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<FileTextIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Emitir a factura</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Descargar en PDF */}
|
||||
{canDownloadPdf && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className={"size-8"}
|
||||
disabled={isPDFLoading}
|
||||
onClick={(e) => {
|
||||
stop(e);
|
||||
actionHandlers.onDownloadPdf?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
{isPDFLoading ? (
|
||||
{quickAction.loading ? (
|
||||
<Spinner className="size-4 cursor-progress" />
|
||||
) : (
|
||||
<FileDownIcon className="size-4 cursor-pointer" />
|
||||
<quickAction.icon className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Descargar PDF</span>
|
||||
<span className="sr-only">{quickAction.label}</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Descargar PDF</TooltipContent>
|
||||
<TooltipContent>{quickAction.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Eliminar */}
|
||||
{canDelete && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
{menuActions.length > 0 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
className="size-8 text-destructive/75 hover:text-destructive cursor-pointer"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
actionHandlers.onDeleteClick?.(proforma);
|
||||
}}
|
||||
className="size-8 text-muted-foreground hover:text-primary"
|
||||
onClick={stop}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">Más acciones</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Eliminar</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenuContent align="end" className="w-52" onClick={stop}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{menuActions.length &&
|
||||
menuActions.map((action, index) =>
|
||||
action ? (
|
||||
<React.Fragment key={action.label}>
|
||||
{action.destructive && index > 0 ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem
|
||||
className={
|
||||
action.destructive
|
||||
? "text-destructive focus:text-destructive"
|
||||
: undefined
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
action.onClick();
|
||||
}}
|
||||
variant={action.destructive ? "destructive" : "default"}
|
||||
>
|
||||
{"loading" in action && action.loading ? (
|
||||
<Spinner className="mr-2 size-4" />
|
||||
) : (
|
||||
<action.icon className="mr-2 size-4" />
|
||||
)}
|
||||
</ButtonGroup>
|
||||
<span>{action.label}</span>
|
||||
</DropdownMenuItem>
|
||||
</React.Fragment>
|
||||
) : null
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { ReceiptTextIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import {
|
||||
getProformaStatusButtonVariant,
|
||||
getProformaStatusColor,
|
||||
getProformaStatusIcon,
|
||||
} from "../../../change-status/helpers";
|
||||
import { getProformaStatusIcon } from "../../../change-status/helpers";
|
||||
import type { ProformaStatus } from "../../../shared";
|
||||
|
||||
export type ProformaStatusBadgeProps = {
|
||||
@ -17,19 +14,36 @@ export type ProformaStatusBadgeProps = {
|
||||
export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgeProps) => {
|
||||
const { t } = useTranslation();
|
||||
const normalizedStatus = status.toLowerCase() as ProformaStatus;
|
||||
const Icon = getProformaStatusIcon(normalizedStatus);
|
||||
const Icon = normalizedStatus === "issued" ? ReceiptTextIcon : getProformaStatusIcon(normalizedStatus);
|
||||
|
||||
const stylesByStatus: Record<ProformaStatus, string> = {
|
||||
draft:
|
||||
"border-gray-200 bg-gray-50 text-gray-600 ring-1 ring-inset ring-gray-200/90",
|
||||
sent:
|
||||
"border-amber-300 bg-amber-50 text-amber-800 ring-1 ring-inset ring-amber-300/90",
|
||||
approved:
|
||||
"border-emerald-300 bg-emerald-100/70 text-emerald-700 ring-1 ring-inset ring-emerald-300/90",
|
||||
rejected:
|
||||
"border-red-300 bg-red-50 text-red-600 ring-1 ring-inset ring-red-300/90",
|
||||
issued:
|
||||
"border-sky-300 bg-sky-100/70 text-sky-700 ring-1 ring-inset ring-sky-300/90",
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Badge
|
||||
className={cn(getProformaStatusColor(normalizedStatus), "gap-1", className)}
|
||||
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium leading-none whitespace-nowrap",
|
||||
"[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:size-3.5",
|
||||
stylesByStatus[normalizedStatus],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
<Icon aria-hidden="true" />
|
||||
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||
</Badge>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>
|
||||
|
||||
@ -1,30 +1,42 @@
|
||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { useDataSource, useReturnToNavigation } from "@erp/core/hooks";
|
||||
import { type DataTableMeta, AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
Spinner,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
CircleDollarSignIcon,
|
||||
ClockIcon,
|
||||
FileDownIcon,
|
||||
FileTextIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { createSearchParams, useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { ChangeProformaStatusDialog } from "../../../change-status";
|
||||
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
|
||||
import { DeleteProformaDialog } from "../../../delete";
|
||||
import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
||||
import { downloadProformaPDFApi } from "../../../download-pdf/api";
|
||||
import { IssueProformaDialog } from "../../../issue-proforma";
|
||||
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
||||
import type { ProformaListRow } from "../../../shared";
|
||||
@ -42,6 +54,13 @@ interface ScopedListProformasPageProps {
|
||||
scope: ProformaListScope;
|
||||
}
|
||||
|
||||
interface ProformasSelectionActionsProps {
|
||||
scope: ProformaListScope;
|
||||
selectedProformas: ProformaListRow[];
|
||||
onChangeStatus: (selected: ProformaListRow[]) => void;
|
||||
onDelete: (selected: ProformaListRow[]) => void;
|
||||
}
|
||||
|
||||
const SCOPE_CONFIG: Record<
|
||||
ProformaListScope,
|
||||
{
|
||||
@ -79,6 +98,116 @@ const SCOPE_CONFIG: Record<
|
||||
},
|
||||
};
|
||||
|
||||
const ProformasSelectionActions = ({
|
||||
scope,
|
||||
selectedProformas,
|
||||
onChangeStatus,
|
||||
onDelete,
|
||||
}: ProformasSelectionActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const dataSource = useDataSource();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const canDeleteSelection =
|
||||
scope !== "deleted" &&
|
||||
selectedProformas.length > 0 &&
|
||||
selectedProformas.every((proforma) => proforma.status === "draft");
|
||||
|
||||
const handleDownloadSelection = useCallback(async () => {
|
||||
if (selectedProformas.length === 0 || isDownloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloading(true);
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
try {
|
||||
for (const proforma of selectedProformas) {
|
||||
const blob = await downloadProformaPDFApi(dataSource, proforma.id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `proforma-${proforma.proformaReference}.pdf`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
successCount += 1;
|
||||
}
|
||||
|
||||
showSuccessToast(
|
||||
"Descarga iniciada",
|
||||
successCount === 1
|
||||
? `La proforma ${selectedProformas[0]?.proformaReference ?? ""} se está descargando.`
|
||||
: `Se han iniciado ${successCount} descargas de proformas.`
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
"Error al descargar",
|
||||
error instanceof Error ? error.message : "No se pudieron descargar las proformas seleccionadas."
|
||||
);
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
}, [dataSource, isDownloading, selectedProformas]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
onClick={() => onChangeStatus(selectedProformas)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCwIcon aria-hidden="true" className="mr-2 size-4" />
|
||||
<span>{t("proformas.change_proforma_status_dialog.title")}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="cursor-pointer"
|
||||
disabled={isDownloading}
|
||||
onClick={() => {
|
||||
void handleDownloadSelection();
|
||||
}}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<Spinner aria-hidden="true" className="mr-2 size-4" />
|
||||
) : (
|
||||
<FileDownIcon aria-hidden="true" className="mr-2 size-4" />
|
||||
)}
|
||||
<span>Descargar PDF</span>
|
||||
</Button>
|
||||
|
||||
{canDeleteSelection ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button className="cursor-pointer" size="sm" type="button" variant="outline">
|
||||
<MoreHorizontalIcon aria-hidden="true" className="mr-2 size-4" />
|
||||
<span>Más acciones</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="start" className="w-52">
|
||||
<DropdownMenuLabel>Selección</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(selectedProformas)}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2Icon aria-hidden="true" className="mr-2 size-4" />
|
||||
<span>Eliminar borradores</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@ -155,6 +284,32 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
isPdfDownloading,
|
||||
});
|
||||
|
||||
const tableMeta = useMemo<DataTableMeta<ProformaListRow>>(
|
||||
() => ({
|
||||
renderSelectionActions: (table) => {
|
||||
const selectedProformas = table.getSelectedRowModel().rows.map((row) => row.original);
|
||||
|
||||
if (selectedProformas.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProformasSelectionActions
|
||||
onChangeStatus={(selected) =>
|
||||
changeStatusDialogCtrl.openDialog(prepareChangeProformaStatusTargets(selected))
|
||||
}
|
||||
onDelete={(selected) =>
|
||||
deleteDialogCtrl.openDialog(prepareDeleteProformaTargets(selected))
|
||||
}
|
||||
scope={scope}
|
||||
selectedProformas={selectedProformas}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[changeStatusDialogCtrl, deleteDialogCtrl, scope]
|
||||
);
|
||||
|
||||
const isPanelOpen = scopeConfig.enablePanel && panelCtrl.panelState.isOpen;
|
||||
|
||||
const listContent = (
|
||||
@ -166,6 +321,7 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||
data={listCtrl.data}
|
||||
fetching={listCtrl.isFetching}
|
||||
loading={listCtrl.isLoading}
|
||||
meta={tableMeta}
|
||||
onColumnVisibilityChange={listCtrl.tablePreferences.setColumnVisibility}
|
||||
onPageChange={listCtrl.setPageIndex}
|
||||
onPageSizeChange={listCtrl.setPageSize}
|
||||
|
||||
@ -72,6 +72,14 @@ export function DataTableToolbar<TData>({
|
||||
table.resetRowSelection();
|
||||
}, [table]);
|
||||
|
||||
const customSelectionActions = React.useMemo(() => {
|
||||
if (!hasSelection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return meta?.renderSelectionActions?.(table) ?? null;
|
||||
}, [hasSelection, meta, table]);
|
||||
|
||||
// Render principal
|
||||
return (
|
||||
<div
|
||||
@ -100,7 +108,7 @@ export function DataTableToolbar<TData>({
|
||||
{/* Acciones sobre selección */}
|
||||
{hasSelection && (
|
||||
<>
|
||||
<Separator className="h-5 mx-1" orientation="vertical" />
|
||||
{customSelectionActions}
|
||||
|
||||
{!readOnly && meta?.bulkOps?.duplicateSelected && (
|
||||
<Button
|
||||
|
||||
@ -64,6 +64,7 @@ export type DataTableMeta<TData> = TableMeta<TData> & {
|
||||
tableOps?: DataTableOps<TData>;
|
||||
rowOps?: DataTableRowOps<TData>;
|
||||
bulkOps?: DataTableBulkRowOps<TData>;
|
||||
renderSelectionActions?: (table: Table<TData>) => React.ReactNode;
|
||||
};
|
||||
|
||||
export type DataTableSortDirection = "asc" | "desc";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user