Agregar acciones y metadatos a la página de detalle de proformas recurrentes, incluyendo pruebas y utilidades de formato de fecha
This commit is contained in:
parent
5775378ca6
commit
e670ff4b33
@ -106,6 +106,7 @@
|
||||
"no_customer": "No customer",
|
||||
"created_at": "Created",
|
||||
"updated_at": "Updated",
|
||||
"metadata": "Created on {{createdAt}} · Last updated {{updatedAt}}",
|
||||
"inherited": "Inherited",
|
||||
"general": {
|
||||
"title": "General information"
|
||||
|
||||
@ -106,6 +106,7 @@
|
||||
"no_customer": "Sin cliente",
|
||||
"created_at": "Creada",
|
||||
"updated_at": "Actualizada",
|
||||
"metadata": "Creada el {{createdAt}} · Última actualización {{updatedAt}}",
|
||||
"inherited": "Heredado",
|
||||
"general": {
|
||||
"title": "Informacion general"
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { getRecurringProformaDetailActions } from "./recurring-proforma-detail-actions";
|
||||
|
||||
test("prioriza la activación de un borrador y conserva las acciones terminales en el menú", () => {
|
||||
assert.deepEqual(getRecurringProformaDetailActions("draft"), [
|
||||
{ key: "activate", placement: "primary" },
|
||||
{ key: "duplicate", placement: "secondary" },
|
||||
{ key: "complete", placement: "menu", terminal: true },
|
||||
{ key: "delete", placement: "menu", terminal: true, destructive: true },
|
||||
]);
|
||||
});
|
||||
|
||||
test("no ofrece acciones de ciclo de vida para recurrencias archivadas", () => {
|
||||
assert.deepEqual(getRecurringProformaDetailActions("archived"), [
|
||||
{ key: "duplicate", placement: "primary" },
|
||||
]);
|
||||
});
|
||||
@ -0,0 +1,44 @@
|
||||
import type { RecurringProformaAction } from "../shared/hooks/use-recurring-proforma-action-mutation";
|
||||
import type { RecurringProformaStatus } from "../shared/recurring-proforma-list";
|
||||
|
||||
export type RecurringProformaDetailActionPlacement = "primary" | "secondary" | "menu";
|
||||
|
||||
export type RecurringProformaDetailAction = {
|
||||
key: RecurringProformaAction;
|
||||
placement: RecurringProformaDetailActionPlacement;
|
||||
terminal?: boolean;
|
||||
destructive?: boolean;
|
||||
};
|
||||
|
||||
export const getRecurringProformaDetailActions = (
|
||||
status: RecurringProformaStatus
|
||||
): RecurringProformaDetailAction[] => {
|
||||
switch (status) {
|
||||
case "draft":
|
||||
return [
|
||||
{ key: "activate", placement: "primary" },
|
||||
{ key: "duplicate", placement: "secondary" },
|
||||
{ key: "complete", placement: "menu", terminal: true },
|
||||
{ key: "delete", placement: "menu", terminal: true, destructive: true },
|
||||
];
|
||||
case "active":
|
||||
return [
|
||||
{ key: "pause", placement: "primary" },
|
||||
{ key: "duplicate", placement: "secondary" },
|
||||
{ key: "complete", placement: "menu", terminal: true },
|
||||
];
|
||||
case "paused":
|
||||
return [
|
||||
{ key: "resume", placement: "primary" },
|
||||
{ key: "duplicate", placement: "secondary" },
|
||||
{ key: "complete", placement: "menu", terminal: true },
|
||||
];
|
||||
case "completed":
|
||||
return [
|
||||
{ key: "duplicate", placement: "primary" },
|
||||
{ key: "archive", placement: "menu", terminal: true },
|
||||
];
|
||||
case "archived":
|
||||
return [{ key: "duplicate", placement: "primary" }];
|
||||
}
|
||||
};
|
||||
@ -11,9 +11,15 @@ import type {
|
||||
|
||||
type Translate = (key: string) => string;
|
||||
|
||||
export const formatRecurringProformaDateOnly = (value: string, locale: string) =>
|
||||
export const formatRecurringProformaDateOnly = (value: string, locale?: string) =>
|
||||
DateHelper.format(value, locale);
|
||||
|
||||
export const formatRecurringProformaDateTime = (value: string, locale: string) =>
|
||||
new Intl.DateTimeFormat(locale, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(new Date(value));
|
||||
|
||||
export const formatRecurringProformaOverride = (value: string | null, t: Translate) =>
|
||||
value ?? t("recurring_proformas.detail.inherited");
|
||||
|
||||
|
||||
@ -0,0 +1,133 @@
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
CheckCircle2Icon,
|
||||
CirclePauseIcon,
|
||||
CopyIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlayCircleIcon,
|
||||
RefreshCwIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { RecurringProformaAction } from "../../../shared/hooks/use-recurring-proforma-action-mutation";
|
||||
import type { RecurringProformaDetailAction } from "../../recurring-proforma-detail-actions";
|
||||
|
||||
type RecurringProformaDetailHeaderActionsProps = {
|
||||
actions: RecurringProformaDetailAction[];
|
||||
isPending: boolean;
|
||||
onExecuteAction: (action: RecurringProformaAction) => void;
|
||||
};
|
||||
|
||||
const ActionIcon = ({ action }: { action: RecurringProformaAction }) => {
|
||||
switch (action) {
|
||||
case "activate":
|
||||
case "resume":
|
||||
return <PlayCircleIcon aria-hidden="true" className="mr-2 size-4" />;
|
||||
case "pause":
|
||||
return <CirclePauseIcon aria-hidden="true" className="mr-2 size-4" />;
|
||||
case "complete":
|
||||
return <CheckCircle2Icon aria-hidden="true" className="mr-2 size-4" />;
|
||||
case "archive":
|
||||
return <ArchiveIcon aria-hidden="true" className="mr-2 size-4" />;
|
||||
case "duplicate":
|
||||
return <CopyIcon aria-hidden="true" className="mr-2 size-4" />;
|
||||
case "delete":
|
||||
return <Trash2Icon aria-hidden="true" className="mr-2 size-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
export const RecurringProformaDetailHeaderActions = ({
|
||||
actions,
|
||||
isPending,
|
||||
onExecuteAction,
|
||||
}: RecurringProformaDetailHeaderActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const primaryAction = actions.find((action) => action.placement === "primary");
|
||||
const secondaryActions = actions.filter((action) => action.placement === "secondary");
|
||||
const menuActions = actions.filter((action) => action.placement === "menu");
|
||||
const hasFunctionalMenuAction = menuActions.some((action) => !action.terminal);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
|
||||
{primaryAction ? (
|
||||
<Button
|
||||
disabled={isPending}
|
||||
onClick={() => onExecuteAction(primaryAction.key)}
|
||||
type="button"
|
||||
>
|
||||
{isPending ? (
|
||||
<RefreshCwIcon aria-hidden="true" className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
<ActionIcon action={primaryAction.key} />
|
||||
)}
|
||||
{t(`recurring_proformas.actions.${primaryAction.key}`)}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{secondaryActions.map((action) => {
|
||||
return (
|
||||
<Button
|
||||
disabled={isPending}
|
||||
key={action.key}
|
||||
onClick={() => onExecuteAction(action.key)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<ActionIcon action={action.key} />
|
||||
{t(`recurring_proformas.actions.${action.key}`)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
{menuActions.length > 0 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("common.more_actions")}
|
||||
disabled={isPending}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<MoreHorizontalIcon aria-hidden="true" className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
{menuActions.map((action, index) => {
|
||||
const showSeparator =
|
||||
action.terminal &&
|
||||
hasFunctionalMenuAction &&
|
||||
index > 0 &&
|
||||
!menuActions[index - 1]?.terminal;
|
||||
|
||||
return (
|
||||
<div key={action.key}>
|
||||
{showSeparator ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem
|
||||
disabled={isPending}
|
||||
onClick={() => onExecuteAction(action.key)}
|
||||
variant={action.destructive ? "destructive" : "default"}
|
||||
>
|
||||
<ActionIcon action={action.key} />
|
||||
{t(`recurring_proformas.actions.${action.key}`)}
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -14,14 +14,18 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { ProformaPageHeader } from "../../../../proformas/shared";
|
||||
import { useRecurringProforma } from "../../../shared/hooks/use-recurring-proforma";
|
||||
import { useRecurringProformaActionMutation } from "../../../shared/hooks/use-recurring-proforma-action-mutation";
|
||||
import { getRecurringProformaDetailActions } from "../../recurring-proforma-detail-actions";
|
||||
import {
|
||||
buildRecurringProformaDetailMainView,
|
||||
formatRecurringProformaDateOnly,
|
||||
formatRecurringProformaDateTime,
|
||||
formatRecurringProformaOverride,
|
||||
formatRecurringProformaPercentage,
|
||||
getRecurringProformaDetailFeedbackState,
|
||||
} from "../../recurring-proforma-detail-utils";
|
||||
import { RecurringProformaOccurrencesSection } from "../../recurring-proforma-occurrences-section";
|
||||
import { RecurringProformaDetailHeaderActions } from "../blocks/recurring-proforma-detail-header-actions";
|
||||
|
||||
type DetailFieldProps = {
|
||||
label: string;
|
||||
@ -40,6 +44,7 @@ export const RecurringProformaDetailPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { recurring_proforma_id: recurringProformaId } = useParams();
|
||||
const query = useRecurringProforma(recurringProformaId);
|
||||
const actionMutation = useRecurringProformaActionMutation();
|
||||
const feedbackState = getRecurringProformaDetailFeedbackState({
|
||||
isLoading: query.isLoading,
|
||||
isError: query.isError,
|
||||
@ -63,7 +68,9 @@ export const RecurringProformaDetailPage = () => {
|
||||
description={t("recurring_proformas.detail.error.description")}
|
||||
primaryAction={{
|
||||
label: t("recurring_proformas.detail.error.retry"),
|
||||
onClick: query.refetch,
|
||||
onClick: async () => {
|
||||
await query.refetch();
|
||||
},
|
||||
}}
|
||||
title={t("recurring_proformas.detail.error.title")}
|
||||
variant="unknown"
|
||||
@ -76,25 +83,46 @@ export const RecurringProformaDetailPage = () => {
|
||||
if (!recurringProforma) {
|
||||
return null;
|
||||
}
|
||||
const dateLocale = i18n.language === "en" ? "en-US" : "es-ES";
|
||||
const dateLocale = i18n.language.startsWith("en") ? "en-US" : "es-ES";
|
||||
const schedule = recurringProforma.schedule;
|
||||
const mainView = buildRecurringProformaDetailMainView(recurringProforma, (key, options) =>
|
||||
String(t(key, options))
|
||||
);
|
||||
const discount = formatRecurringProformaPercentage(recurringProforma.global_discount_percentage);
|
||||
const headerActions = getRecurringProformaDetailActions(recurringProforma.status);
|
||||
const metadata = t("recurring_proformas.detail.metadata", {
|
||||
createdAt: formatRecurringProformaDateTime(recurringProforma.created_at, dateLocale),
|
||||
updatedAt: formatRecurringProformaDateTime(recurringProforma.updated_at, dateLocale),
|
||||
});
|
||||
|
||||
return (
|
||||
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
|
||||
<ProformaPageHeader
|
||||
actions={
|
||||
<RecurringProformaDetailHeaderActions
|
||||
actions={headerActions}
|
||||
isPending={actionMutation.isPending}
|
||||
onExecuteAction={(action) =>
|
||||
actionMutation.mutate({ id: recurringProforma.id, action })
|
||||
}
|
||||
/>
|
||||
}
|
||||
backLabel={t("recurring_proformas.detail.back")}
|
||||
onBack={() => navigate("/recurring-proformas")}
|
||||
title={mainView.name}
|
||||
title={
|
||||
<span className="inline-flex max-w-full items-center gap-3">
|
||||
<span className="truncate">{mainView.name}</span>
|
||||
<Badge className="shrink-0 px-4 py-3 rounded-md" variant="secondary">
|
||||
{t(mainView.statusKey)}
|
||||
</Badge>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-4 py-3 sm:px-6">
|
||||
<Badge variant="secondary">{t(mainView.statusKey)}</Badge>
|
||||
<div className="space-y-3 border-b bg-background px-4 py-4 sm:px-6">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("recurring_proformas.detail.context")}
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">{metadata}</p>
|
||||
</div>
|
||||
</ProformaPageHeader>
|
||||
|
||||
@ -114,14 +142,6 @@ export const RecurringProformaDetailPage = () => {
|
||||
label={t("recurring_proformas.detail.customer")}
|
||||
value={recurringProforma.customer_id ?? t("recurring_proformas.detail.no_customer")}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.created_at")}
|
||||
value={formatRecurringProformaDateOnly(recurringProforma.created_at, dateLocale)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.updated_at")}
|
||||
value={formatRecurringProformaDateOnly(recurringProforma.updated_at, dateLocale)}
|
||||
/>
|
||||
</dl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -139,13 +159,13 @@ export const RecurringProformaDetailPage = () => {
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.start_date")}
|
||||
value={formatRecurringProformaDateOnly(schedule.start_date, dateLocale)}
|
||||
value={formatRecurringProformaDateOnly(schedule.start_date)}
|
||||
/>
|
||||
<DetailField
|
||||
label={t("recurring_proformas.detail.schedule.end_date")}
|
||||
value={
|
||||
schedule.end_date
|
||||
? formatRecurringProformaDateOnly(schedule.end_date, dateLocale)
|
||||
? formatRecurringProformaDateOnly(schedule.end_date)
|
||||
: t("recurring_proformas.detail.schedule.no_end_date")
|
||||
}
|
||||
/>
|
||||
|
||||
@ -25,7 +25,7 @@ export const useRecurringProformaActionMutation = () => {
|
||||
data: undefined,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: recurringProformaQueryKeys.lists() });
|
||||
await queryClient.invalidateQueries({ queryKey: recurringProformaQueryKeys.all });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user