Facturas de cliente
This commit is contained in:
parent
a062732e5e
commit
323860b5b8
@ -11,29 +11,43 @@
|
|||||||
"remove_selected_rows_tooltip": "Remove selected row(s)",
|
"remove_selected_rows_tooltip": "Remove selected row(s)",
|
||||||
"download_pdf": "Download PDF",
|
"download_pdf": "Download PDF",
|
||||||
"send_email": "Send email",
|
"send_email": "Send email",
|
||||||
|
|
||||||
"insert_row_above": "Insert row above",
|
"insert_row_above": "Insert row above",
|
||||||
"insert_row_below": "Insert row below",
|
"insert_row_below": "Insert row below",
|
||||||
"delete_row": "Delete",
|
"delete_row": "Delete",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
|
|
||||||
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
||||||
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.",
|
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.",
|
||||||
|
|
||||||
"search_placeholder": "Type for search...",
|
"search_placeholder": "Type for search...",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"clear": "Clear"
|
"clear": "Clear"
|
||||||
},
|
},
|
||||||
|
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"proformas": {
|
"proformas": {
|
||||||
"status": {
|
"status": {
|
||||||
"all": "All",
|
"all": {
|
||||||
"draft": "Draft",
|
"label": "All",
|
||||||
"sent": "Sent",
|
"description": ""
|
||||||
"approved": "Approved",
|
},
|
||||||
"rejected": "Rejected",
|
"draft": {
|
||||||
"issued": "Issued"
|
"label": "Draft",
|
||||||
|
"description": "Proforma in preparation"
|
||||||
|
},
|
||||||
|
"sent": {
|
||||||
|
"label": "Sent",
|
||||||
|
"description": "Sent to the client"
|
||||||
|
},
|
||||||
|
"approved": {
|
||||||
|
"label": "Approved",
|
||||||
|
"description": "Approved by the client"
|
||||||
|
},
|
||||||
|
"rejected": {
|
||||||
|
"label": "Rejected",
|
||||||
|
"description": "Rejected by the client"
|
||||||
|
},
|
||||||
|
"issued": {
|
||||||
|
"label": "Issued",
|
||||||
|
"description": "Converted to invoice"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issued_invoices": {
|
"issued_invoices": {
|
||||||
@ -294,4 +308,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,15 +11,12 @@
|
|||||||
"remove_selected_rows_tooltip": "Eliminar fila(s) seleccionada(s)",
|
"remove_selected_rows_tooltip": "Eliminar fila(s) seleccionada(s)",
|
||||||
"download_pdf": "Descargar en PDF",
|
"download_pdf": "Descargar en PDF",
|
||||||
"send_email": "Enviar por email",
|
"send_email": "Enviar por email",
|
||||||
|
|
||||||
"insert_row_above": "Insertar fila arriba",
|
"insert_row_above": "Insertar fila arriba",
|
||||||
"insert_row_below": "Insertar fila abajo",
|
"insert_row_below": "Insertar fila abajo",
|
||||||
"delete_row": "Eliminar",
|
"delete_row": "Eliminar",
|
||||||
"actions": "Acciones",
|
"actions": "Acciones",
|
||||||
|
|
||||||
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
"rows_selected": "{{count}} fila(s) seleccionadas.",
|
||||||
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.",
|
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.",
|
||||||
|
|
||||||
"search_placeholder": "Escribe aquí para buscar...",
|
"search_placeholder": "Escribe aquí para buscar...",
|
||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"clear": "Limpiar"
|
"clear": "Limpiar"
|
||||||
@ -27,12 +24,30 @@
|
|||||||
"catalog": {
|
"catalog": {
|
||||||
"proformas": {
|
"proformas": {
|
||||||
"status": {
|
"status": {
|
||||||
"all": "Todas",
|
"all": {
|
||||||
"draft": "Borrador",
|
"label": "Todas",
|
||||||
"sent": "Enviada",
|
"description": ""
|
||||||
"approved": "Aprobada",
|
},
|
||||||
"rejected": "Rechazada",
|
"draft": {
|
||||||
"issued": "Emitida"
|
"label": "Borrador",
|
||||||
|
"description": "Proforma en preparación"
|
||||||
|
},
|
||||||
|
"sent": {
|
||||||
|
"label": "Enviada",
|
||||||
|
"description": "Enviada al cliente"
|
||||||
|
},
|
||||||
|
"approved": {
|
||||||
|
"label": "Aprobada",
|
||||||
|
"description": "Aprobada por el cliente"
|
||||||
|
},
|
||||||
|
"rejected": {
|
||||||
|
"label": "Rechazada",
|
||||||
|
"description": "Rechazada por el cliente"
|
||||||
|
},
|
||||||
|
"issued": {
|
||||||
|
"label": "Emitida",
|
||||||
|
"description": "Convertida a factura"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"issued_invoices": {
|
"issued_invoices": {
|
||||||
@ -138,7 +153,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"form_groups": {
|
"form_groups": {
|
||||||
"customer": {
|
"customer": {
|
||||||
"title": "Cliente",
|
"title": "Cliente",
|
||||||
@ -187,7 +201,6 @@
|
|||||||
"placeholder": "Referencia de la proforma",
|
"placeholder": "Referencia de la proforma",
|
||||||
"description": "Referencia de la proforma"
|
"description": "Referencia de la proforma"
|
||||||
},
|
},
|
||||||
|
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Descripción",
|
"label": "Descripción",
|
||||||
"placeholder": "Descripción de la proforma",
|
"placeholder": "Descripción de la proforma",
|
||||||
@ -286,4 +299,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import { showErrorToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import type { ProformaSummaryData } from "../../types";
|
import type { PROFORMA_STATUS, ProformaSummaryData } from "../../types";
|
||||||
import { useChangeProformaStatus } from "../hooks/use-change-status";
|
import { useChangeProformaStatus } from "../hooks/use-change-status";
|
||||||
|
|
||||||
interface ChangeStatusDialogState {
|
interface ChangeStatusDialogState {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
targetStatus: string | null;
|
|
||||||
proformas: ProformaSummaryData[];
|
proformas: ProformaSummaryData[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
}
|
}
|
||||||
@ -16,15 +15,13 @@ export function useChangeStatusDialogController() {
|
|||||||
|
|
||||||
const [state, setState] = React.useState<ChangeStatusDialogState>({
|
const [state, setState] = React.useState<ChangeStatusDialogState>({
|
||||||
open: false,
|
open: false,
|
||||||
targetStatus: null,
|
proformas: [] as ProformaSummaryData[],
|
||||||
proformas: [],
|
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const openDialog = (proformas: ProformaSummaryData[]) => {
|
const openDialog = (proformas: ProformaSummaryData[]) => {
|
||||||
setState({
|
setState({
|
||||||
open: true,
|
open: true,
|
||||||
targetStatus: null,
|
|
||||||
proformas,
|
proformas,
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
@ -38,13 +35,16 @@ export function useChangeStatusDialogController() {
|
|||||||
setState((s) => ({ ...s, targetStatus: status }));
|
setState((s) => ({ ...s, targetStatus: status }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmChangeStatus = async () => {
|
const confirmChangeStatus = async (status: PROFORMA_STATUS) => {
|
||||||
if (!state.targetStatus || state.proformas.length === 0) return;
|
if (!state.proformas.length) return;
|
||||||
|
|
||||||
setState((s) => ({ ...s, loading: true }));
|
setState((s) => ({ ...s, loading: true }));
|
||||||
|
|
||||||
for (const proforma of state.proformas) {
|
for (const proforma of state.proformas) {
|
||||||
await changeStatus(proforma.id, state.targetStatus, {
|
await changeStatus(proforma.id, status, {
|
||||||
|
onSuccess: () => {
|
||||||
|
showSuccessToast("Estado cambiado");
|
||||||
|
},
|
||||||
onError: (err: unknown) => {
|
onError: (err: unknown) => {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
showErrorToast("Error cambiando estado", error.message);
|
showErrorToast("Error cambiando estado", error.message);
|
||||||
@ -59,12 +59,10 @@ export function useChangeStatusDialogController() {
|
|||||||
return {
|
return {
|
||||||
open: state.open,
|
open: state.open,
|
||||||
proformas: state.proformas,
|
proformas: state.proformas,
|
||||||
targetStatus: state.targetStatus,
|
|
||||||
isSubmitting: state.loading,
|
isSubmitting: state.loading,
|
||||||
|
|
||||||
openDialog,
|
openDialog,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
setTargetStatus,
|
|
||||||
confirmChangeStatus,
|
confirmChangeStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,64 +8,62 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
import {
|
|
||||||
CheckCircle2Icon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
FileCheckIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
SendIcon,
|
|
||||||
XCircleIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { PROFORMA_STATUS, PROFORMA_STATUS_TRANSITIONS } from "../../types";
|
import {
|
||||||
|
PROFORMA_STATUS,
|
||||||
|
PROFORMA_STATUS_TRANSITIONS,
|
||||||
|
type ProformaSummaryData,
|
||||||
|
getProformaStatusIcon,
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
|
import { StatusNode, TimelineConnector } from "./components";
|
||||||
|
|
||||||
interface ChangeStatusDialogProps {
|
interface ChangeStatusDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
proformas: { id: string; status: string }[];
|
proformas: ProformaSummaryData[];
|
||||||
targetStatus?: string;
|
targetStatus?: string;
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: (status: PROFORMA_STATUS) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_FLOW = [
|
const STATUS_FLOW = [
|
||||||
{
|
{
|
||||||
status: PROFORMA_STATUS.DRAFT,
|
status: PROFORMA_STATUS.DRAFT,
|
||||||
icon: FileTextIcon,
|
activeColor: "text-gray-700",
|
||||||
color: "text-gray-500",
|
activeBg: "bg-gray-100",
|
||||||
bgColor: "bg-gray-100",
|
activeBorder: "border-gray-300",
|
||||||
borderColor: "border-gray-300",
|
activeRing: "ring-gray-200",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: PROFORMA_STATUS.SENT,
|
status: PROFORMA_STATUS.SENT,
|
||||||
icon: SendIcon,
|
activeColor: "text-yellow-700",
|
||||||
color: "text-blue-600",
|
activeBg: "bg-yellow-100",
|
||||||
bgColor: "bg-blue-100",
|
activeBorder: "border-yellow-300",
|
||||||
borderColor: "border-blue-300",
|
activeRing: "ring-yellow-200",
|
||||||
},
|
|
||||||
{
|
|
||||||
status: PROFORMA_STATUS.APPROVED,
|
|
||||||
icon: CheckCircle2Icon,
|
|
||||||
color: "text-green-600",
|
|
||||||
bgColor: "bg-green-100",
|
|
||||||
borderColor: "border-green-300",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: PROFORMA_STATUS.REJECTED,
|
status: PROFORMA_STATUS.REJECTED,
|
||||||
icon: XCircleIcon,
|
activeColor: "text-red-700",
|
||||||
color: "text-red-600",
|
activeBg: "bg-red-100",
|
||||||
bgColor: "bg-red-100",
|
activeBorder: "border-red-300",
|
||||||
borderColor: "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,
|
status: PROFORMA_STATUS.ISSUED,
|
||||||
icon: FileCheckIcon,
|
activeColor: "text-blue-700",
|
||||||
color: "text-purple-600",
|
activeBg: "bg-blue-100",
|
||||||
bgColor: "bg-purple-100",
|
activeBorder: "border-blue-300",
|
||||||
borderColor: "border-purple-300",
|
activeRing: "ring-blue-200",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@ -122,12 +120,12 @@ export function ChangeStatusDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||||
<DialogContent className="max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Cambiar estado de la proforma</DialogTitle>
|
<DialogTitle>Cambiar estado de la proforma</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{proformas.length === 1
|
{proformas.length === 1
|
||||||
? `Selecciona el nuevo estado para la proforma #${proformas[0].id}`
|
? `Selecciona el nuevo estado para la proforma #${proformas[0].reference}`
|
||||||
: `Selecciona el nuevo estado para ${proformas.length} proformas seleccionadas`}
|
: `Selecciona el nuevo estado para ${proformas.length} proformas seleccionadas`}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -139,150 +137,56 @@ export function ChangeStatusDialog({
|
|||||||
) : (
|
) : (
|
||||||
<div className="py-6">
|
<div className="py-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Línea de conexión */}
|
<div className="relative grid grid-cols-5 gap-2 mb-8">
|
||||||
<div className="absolute top-12 left-0 right-0 h-0.5 bg-gray-200" />
|
|
||||||
|
|
||||||
{/* Estados */}
|
|
||||||
<div className="relative grid grid-cols-5 gap-2">
|
|
||||||
{STATUS_FLOW.map((statusConfig, index) => {
|
{STATUS_FLOW.map((statusConfig, index) => {
|
||||||
const statusType = getStatusType(statusConfig.status);
|
const statusType = getStatusType(statusConfig.status);
|
||||||
const Icon = statusConfig.icon;
|
|
||||||
const isSelected = selectedStatus === statusConfig.status;
|
const isSelected = selectedStatus === statusConfig.status;
|
||||||
const isClickable = statusType === "available";
|
const isClickable = statusType === "available";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center" key={statusConfig.status}>
|
<StatusNode
|
||||||
{/* Botón de estado */}
|
activeBg={statusConfig.activeBg}
|
||||||
<button
|
activeBorder={statusConfig.activeBorder}
|
||||||
className={cn(
|
activeColor={statusConfig.activeColor}
|
||||||
"relative z-10 flex size-24 flex-col items-center justify-center rounded-xl border-2 transition-all",
|
activeRing={statusConfig.activeRing}
|
||||||
// Estado pasado
|
description={t(`catalog.proformas.status.${statusConfig.status}.description`)}
|
||||||
statusType === "past" && ["border-gray-200 bg-gray-50", "opacity-60"],
|
icon={getProformaStatusIcon(statusConfig.status)}
|
||||||
// Estado actual
|
isSelected={isSelected}
|
||||||
statusType === "current" && [
|
key={statusConfig.status}
|
||||||
statusConfig.borderColor,
|
label={t(`catalog.proformas.status.${statusConfig.status}.label`)}
|
||||||
statusConfig.bgColor,
|
onClick={() => {
|
||||||
"ring-2 ring-offset-2",
|
if (isClickable) {
|
||||||
`ring-${statusConfig.color.split("-")[1]}-200`,
|
setSelectedStatus(statusConfig.status);
|
||||||
],
|
}
|
||||||
// Estado disponible para seleccionar
|
}}
|
||||||
statusType === "available" && [
|
status={statusConfig.status}
|
||||||
isSelected
|
statusType={statusType}
|
||||||
? [
|
/>
|
||||||
statusConfig.borderColor,
|
|
||||||
statusConfig.bgColor,
|
|
||||||
"ring-2 ring-offset-2",
|
|
||||||
`ring-${statusConfig.color.split("-")[1]}-300`,
|
|
||||||
"scale-105",
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
"border-gray-300 bg-white hover:border-gray-400",
|
|
||||||
"hover:scale-105 cursor-pointer",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
// Estado no disponible
|
|
||||||
statusType === "unavailable" && [
|
|
||||||
"border-gray-200 bg-gray-50",
|
|
||||||
"opacity-40 cursor-not-allowed",
|
|
||||||
]
|
|
||||||
)}
|
|
||||||
disabled={!isClickable}
|
|
||||||
onClick={() => isClickable && setSelectedStatus(statusConfig.status)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={cn(
|
|
||||||
"size-8 mb-1",
|
|
||||||
statusType === "past" && "text-gray-400",
|
|
||||||
statusType === "current" && statusConfig.color,
|
|
||||||
statusType === "available" &&
|
|
||||||
(isSelected ? statusConfig.color : "text-gray-600"),
|
|
||||||
statusType === "unavailable" && "text-gray-300"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Indicador de estado actual */}
|
|
||||||
{statusType === "current" && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"absolute -top-2 -right-2 size-6 rounded-full border-2 border-white flex items-center justify-center",
|
|
||||||
statusConfig.bgColor
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"size-2 rounded-full",
|
|
||||||
statusConfig.color.replace("text-", "bg-")
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Check de selección */}
|
|
||||||
{isSelected && statusType !== "current" && (
|
|
||||||
<div className="absolute -top-2 -right-2 size-6 rounded-full bg-blue-600 border-2 border-white flex items-center justify-center">
|
|
||||||
<CheckCircle2Icon className="size-4 text-white" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Etiqueta y descripción */}
|
|
||||||
<div className="mt-3 text-center space-y-1 px-1">
|
|
||||||
<p
|
|
||||||
className={cn(
|
|
||||||
"text-sm font-medium",
|
|
||||||
statusType === "past" && "text-gray-500",
|
|
||||||
statusType === "current" && statusConfig.color,
|
|
||||||
statusType === "available" &&
|
|
||||||
(isSelected ? statusConfig.color : "text-gray-700"),
|
|
||||||
statusType === "unavailable" && "text-gray-400"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t(`catalog.proformas.status.${statusConfig.status}`)}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
||||||
{t(`catalog.proformas.status.${statusConfig.status}.description`)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Flecha de conexión (excepto el último) */}
|
|
||||||
{index < STATUS_FLOW.length - 1 && (
|
|
||||||
<ChevronRightIcon className="absolute top-10 -right-4 z-20 size-5 text-gray-300" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 p-4 bg-muted/50 rounded-lg">
|
<TimelineConnector
|
||||||
<div className="flex items-start gap-6 text-sm">
|
nodeCount={STATUS_FLOW.length}
|
||||||
{currentStatus && (
|
statusColors={STATUS_FLOW.map((statusConfig) => ({
|
||||||
<div className="flex items-center gap-2">
|
bgClass: statusConfig.activeBg,
|
||||||
<div className="size-3 rounded-full bg-blue-600 ring-2 ring-blue-200" />
|
statusType: getStatusType(statusConfig.status),
|
||||||
<span className="text-muted-foreground">Estado actual</span>
|
}))}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="size-3 rounded-full border-2 border-gray-400 bg-white" />
|
|
||||||
<span className="text-muted-foreground">Estados disponibles</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="size-3 rounded-full bg-gray-300" />
|
|
||||||
<span className="text-muted-foreground">Estados no disponibles</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter className="sm:justify-between">
|
||||||
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline">
|
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline">
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!selectedStatus || availableStatuses.length === 0 || isSubmitting}
|
disabled={!selectedStatus || availableStatuses.length === 0 || isSubmitting}
|
||||||
onClick={onConfirm}
|
onClick={() => {
|
||||||
|
if (!selectedStatus) return;
|
||||||
|
onConfirm(selectedStatus); // ← enviamos el estado al controlador
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./status-legend";
|
||||||
|
export * from "./status-node";
|
||||||
|
export * from "./timeline-connector";
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
|
||||||
|
interface StatusLegendProps {
|
||||||
|
hasCurrentStatus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusLegend = ({ hasCurrentStatus }: StatusLegendProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="mt-8 p-4 bg-muted/50 rounded-lg">
|
||||||
|
<div className="flex items-start gap-6 text-sm">
|
||||||
|
{hasCurrentStatus && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="size-3 rounded-full bg-amber-500 ring-2 ring-amber-200" />
|
||||||
|
<span className="text-muted-foreground">Estado actual</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="size-3 rounded-full bg-linear-to-r from-blue-400 to-green-400" />
|
||||||
|
<span className="text-muted-foreground">Estados disponibles (colores originales)</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="size-3 rounded-full bg-gray-300 opacity-50" />
|
||||||
|
<span className="text-muted-foreground">Estados no disponibles</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { CheckCircle2, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
import type { PROFORMA_STATUS } from "../../../types";
|
||||||
|
|
||||||
|
interface StatusNodeProps {
|
||||||
|
status: PROFORMA_STATUS;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
activeColor: string;
|
||||||
|
activeBg: string;
|
||||||
|
activeBorder: string;
|
||||||
|
activeRing: string;
|
||||||
|
statusType: "past" | "current" | "available" | "unavailable";
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusNode = ({
|
||||||
|
status,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
activeColor,
|
||||||
|
activeBg,
|
||||||
|
activeBorder,
|
||||||
|
activeRing,
|
||||||
|
statusType,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
}: StatusNodeProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isClickable = statusType === "available";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 flex size-24 flex-col items-center justify-center rounded-xl border-2 transition-all",
|
||||||
|
statusType === "past" && ["border-gray-200 bg-gray-50", "opacity-50 saturate-50"],
|
||||||
|
statusType === "current" && [activeBorder, activeBg, "border-4 scale-110 shadow-lg"],
|
||||||
|
statusType === "available" && [
|
||||||
|
activeBorder,
|
||||||
|
activeBg,
|
||||||
|
isSelected
|
||||||
|
? ["ring-4", activeRing, "ring-offset-2", "scale-105 cursor-pointer"]
|
||||||
|
: ["hover:scale-105 cursor-pointer", "opacity-80 hover:opacity-100"],
|
||||||
|
],
|
||||||
|
statusType === "unavailable" && [
|
||||||
|
"border-gray-200 bg-gray-50",
|
||||||
|
"opacity-30 saturate-0 cursor-not-allowed",
|
||||||
|
]
|
||||||
|
)}
|
||||||
|
disabled={!isClickable}
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={cn(
|
||||||
|
"size-8 mb-1",
|
||||||
|
statusType === "past" && "text-gray-400",
|
||||||
|
statusType === "current" && activeColor,
|
||||||
|
statusType === "available" && activeColor,
|
||||||
|
statusType === "unavailable" && "text-gray-300"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{statusType === "current" && (
|
||||||
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-2 py-0.5 rounded-full bg-primary text-primary-foreground text-[10px] font-bold tracking-wider shadow-md">
|
||||||
|
ACTUAL
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSelected && statusType !== "current" && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute -top-2 -right-2 size-6 rounded-full border-2 border-white flex items-center justify-center",
|
||||||
|
activeBg
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className={cn("size-4", activeColor)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mt-3 text-center space-y-1 px-1">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-sm font-medium",
|
||||||
|
statusType === "past" && "text-gray-500",
|
||||||
|
statusType === "current" && `${activeColor} font-semibold`,
|
||||||
|
statusType === "available" && activeColor,
|
||||||
|
statusType === "unavailable" && "text-gray-400"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
|
interface TimelineConnectorProps {
|
||||||
|
nodeCount: number;
|
||||||
|
statusColors: Array<{
|
||||||
|
bgClass: string;
|
||||||
|
statusType: "past" | "current" | "available" | "unavailable";
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
export function TimelineConnector({ nodeCount, statusColors }: TimelineConnectorProps) {
|
||||||
|
const segments = Array.from({ length: nodeCount - 1 }).map((_, index) => {
|
||||||
|
const fromStatus = statusColors[index];
|
||||||
|
const toStatus = statusColors[index + 1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: fromStatus,
|
||||||
|
to: toStatus,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-3 mx-8">
|
||||||
|
<div className="absolute inset-0 bg-gray-200 rounded-full" />
|
||||||
|
|
||||||
|
<div className="absolute inset-0 flex">
|
||||||
|
{segments.map((segment, index) => {
|
||||||
|
const segmentWidth = `${100 / (nodeCount - 1)}%`;
|
||||||
|
|
||||||
|
// Extraer el color del bgClass (ej: "bg-gray-100" -> "gray")
|
||||||
|
const getColorFromBgClass = (bgClass: string) => {
|
||||||
|
const match = bgClass.match(/bg-(\w+)-/);
|
||||||
|
return match ? match[1] : "gray";
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromColor = getColorFromBgClass(segment.from.bgClass);
|
||||||
|
const toColor = getColorFromBgClass(segment.to.bgClass);
|
||||||
|
|
||||||
|
// Determinar opacidad basada en el tipo de estado
|
||||||
|
const getOpacity = (statusType: string) => {
|
||||||
|
if (statusType === "past") return 0.3;
|
||||||
|
if (statusType === "unavailable") return 0.15;
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromOpacity = getOpacity(segment.from.statusType);
|
||||||
|
const toOpacity = getOpacity(segment.to.statusType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full relative overflow-hidden"
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
width: segmentWidth,
|
||||||
|
background: `linear-gradient(to right,
|
||||||
|
hsl(var(--${fromColor}-200) / ${fromOpacity}),
|
||||||
|
hsl(var(--${toColor}-200) / ${toOpacity})
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-linear-to-r from-transparent via-white/20 to-transparent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 flex justify-between items-center px-14">
|
||||||
|
{Array.from({ length: nodeCount }).map((_, index) => {
|
||||||
|
const statusColor = statusColors[index];
|
||||||
|
const isPast = statusColor.statusType === "past";
|
||||||
|
const isUnavailable = statusColor.statusType === "unavailable";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"size-4 rounded-full bg-white border-2 shadow-sm transition-all",
|
||||||
|
isPast && "border-gray-300 opacity-50",
|
||||||
|
!(isPast || isUnavailable) &&
|
||||||
|
statusColor.bgClass.replace("bg-", "border-").replace("-100", "-400"),
|
||||||
|
isUnavailable && "border-gray-200 opacity-30"
|
||||||
|
)}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,55 +5,76 @@ import { useTranslation } from "../../../i18n";
|
|||||||
import type { ProformaSummaryData } from "../../types";
|
import type { ProformaSummaryData } from "../../types";
|
||||||
import { useDeleteProforma } from "../hooks";
|
import { useDeleteProforma } from "../hooks";
|
||||||
|
|
||||||
interface ProformaState {
|
interface DeleteProformaDialogState {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
proforma: ProformaSummaryData | null;
|
proformas: ProformaSummaryData[];
|
||||||
|
loading: boolean;
|
||||||
|
requireSecondConfirm: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDeleteProformaDialogController() {
|
export function useDeleteProformaDialogController() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mutate, isPending } = useDeleteProforma();
|
const { deleteProforma } = useDeleteProforma();
|
||||||
|
|
||||||
const [state, setState] = React.useState<ProformaState>({
|
const [state, setState] = React.useState<DeleteProformaDialogState>({
|
||||||
open: false,
|
open: false,
|
||||||
proforma: null,
|
proformas: [],
|
||||||
|
loading: false,
|
||||||
|
requireSecondConfirm: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const openDialog = (proforma: ProformaSummaryData) => {
|
const openDialog = (proformas: ProformaSummaryData[]) => {
|
||||||
setState({ open: true, proforma });
|
const needDoubleCheck = proformas.length > 5;
|
||||||
|
setState({
|
||||||
|
open: true,
|
||||||
|
proformas,
|
||||||
|
loading: false,
|
||||||
|
requireSecondConfirm: needDoubleCheck,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
setState((s) => ({ ...s, open: false }));
|
setState((s) => ({ ...s, open: false }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmDelete = () => {
|
const confirmDelete = async () => {
|
||||||
if (!state.proforma) return;
|
if (state.proformas.length === 0) return;
|
||||||
|
|
||||||
mutate(
|
if (state.requireSecondConfirm) {
|
||||||
{ proformaId: state.proforma.id },
|
setState((s) => ({ ...s, requireSecondConfirm: false }));
|
||||||
{
|
return; // ahora el UI mostrará un segundo mensaje de confirmación
|
||||||
onSuccess(data, variables, onMutateResult, context) {
|
}
|
||||||
console.log("adios");
|
|
||||||
console.log(data);
|
setState((s) => ({ ...s, loading: true }));
|
||||||
showSuccessToast(t("proformas.delete_proforma_dialog.success_title"));
|
|
||||||
closeDialog();
|
for (const p of state.proformas) {
|
||||||
|
await deleteProforma(p.id, {
|
||||||
|
onSuccess: () => {
|
||||||
|
showSuccessToast(
|
||||||
|
"Proforma eliminada",
|
||||||
|
`La proforma ${p.reference ?? `#${p.id}`} ha sido eliminada.`
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError(error, variables, onMutateResult, context) {
|
onError: (err) => {
|
||||||
showErrorToast(t("proformas.delete_proforma_dialog.error_title"), error.message);
|
showErrorToast(
|
||||||
|
"Error al eliminar",
|
||||||
|
err instanceof Error ? err.message : "Ocurrió un error al eliminar la proforma"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
setState((s) => ({ ...s, loading: false }));
|
||||||
|
closeDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open: state.open,
|
open: state.open,
|
||||||
proforma: state.proforma,
|
proformas: state.proformas,
|
||||||
isSubmitting: isPending,
|
isSubmitting: state.loading,
|
||||||
|
|
||||||
openDialog,
|
openDialog,
|
||||||
closeDialog,
|
closeDialog,
|
||||||
|
|
||||||
confirmDelete,
|
confirmDelete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,13 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { deleteProformaApi } from "../api";
|
import { deleteProformaApi } from "../api";
|
||||||
|
|
||||||
interface useDeleteProformaPayload {
|
interface DeleteProformaOptions {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (err: unknown) => void;
|
||||||
|
onLoadingChange?: (loading: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteProformaPayload {
|
||||||
proformaId: string;
|
proformaId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11,13 +17,29 @@ export function useDeleteProforma() {
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: ({ proformaId }: useDeleteProformaPayload) =>
|
mutationFn: ({ proformaId }: DeleteProformaPayload) =>
|
||||||
deleteProformaApi(dataSource, proformaId),
|
deleteProformaApi(dataSource, proformaId),
|
||||||
|
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
console.log("hola");
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["proformas"] });
|
queryClient.invalidateQueries({ queryKey: ["proformas"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function deleteProforma(proformaId: string, opts?: DeleteProformaOptions) {
|
||||||
|
try {
|
||||||
|
opts?.onLoadingChange?.(true);
|
||||||
|
await mutation.mutateAsync({ proformaId });
|
||||||
|
opts?.onSuccess?.();
|
||||||
|
} catch (err) {
|
||||||
|
opts?.onError?.(err);
|
||||||
|
} finally {
|
||||||
|
opts?.onLoadingChange?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleteProforma,
|
||||||
|
isPending: mutation.isPending,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,40 +8,83 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { Trans } from "react-i18next";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import type { ProformaSummaryData } from "../../types";
|
||||||
|
|
||||||
interface DeleteProformaDialogProps {
|
interface DeleteProformaDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
proformaRef?: string;
|
proformas: ProformaSummaryData[];
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
|
requireSecondConfirm: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteProformaDialog({
|
export function DeleteProformaDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
proformaRef,
|
proformas,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
requireSecondConfirm,
|
||||||
}: DeleteProformaDialogProps) {
|
}: DeleteProformaDialogProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const total = proformas.length;
|
||||||
|
const isSingle = total === 1;
|
||||||
|
|
||||||
|
const title = requireSecondConfirm
|
||||||
|
? "Confirmación adicional"
|
||||||
|
: isSingle
|
||||||
|
? `Eliminar proforma ${proformas[0].reference ?? `#${proformas[0].id}`}`
|
||||||
|
: `Eliminar ${total} proformas`;
|
||||||
|
|
||||||
|
const description = requireSecondConfirm
|
||||||
|
? `Estás a punto de borrar ${total} proformas. Esta acción masiva no se puede deshacer. ¿Seguro que quieres continuar?`
|
||||||
|
: isSingle
|
||||||
|
? "¿Seguro que deseas eliminar esta proforma? Esta acción no se puede deshacer."
|
||||||
|
: `¿Seguro que deseas eliminar las ${total} proformas seleccionadas? Esta acción no se puede deshacer.`;
|
||||||
|
|
||||||
|
// Usar teclado para confirmar (Enter / Escape)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKey);
|
||||||
|
return () => window.removeEventListener("keydown", handleKey);
|
||||||
|
}, [open, onConfirm, onOpenChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent className="max-w-lg">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>{t("proformas.delete_proforma_dialog.title")}</AlertDialogTitle>
|
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||||
<Trans
|
|
||||||
i18nKey={t("proformas.delete_proforma_dialog.description")}
|
|
||||||
values={{ proformaRef }}
|
|
||||||
/>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
{!requireSecondConfirm && total > 1 && (
|
||||||
|
<div className="mt-4 max-h-48 overflow-y-auto rounded-md border p-3 text-sm">
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{proformas.map((p) => (
|
||||||
|
<li className="flex justify-between text-muted-foreground" key={p.id}>
|
||||||
|
<span>Proforma {p.reference ?? `#${p.id}`}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AlertDialogFooter className="sm:justify-between">
|
||||||
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline">
|
<Button disabled={isSubmitting} onClick={() => onOpenChange(false)} variant="outline">
|
||||||
{t("proformas.delete_proforma_dialog.cancel")}
|
{t("proformas.delete_proforma_dialog.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
@ -52,8 +95,12 @@ export function DeleteProformaDialog({
|
|||||||
<Spinner className="mr-2 size-4" />
|
<Spinner className="mr-2 size-4" />
|
||||||
{t("proformas.delete_proforma_dialog.deleting")}
|
{t("proformas.delete_proforma_dialog.deleting")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : requireSecondConfirm ? (
|
||||||
|
<>{t("proformas.delete_proforma_dialog.mass_delete")}</>
|
||||||
|
) : isSingle ? (
|
||||||
<>{t("proformas.delete_proforma_dialog.delete")}</>
|
<>{t("proformas.delete_proforma_dialog.delete")}</>
|
||||||
|
) : (
|
||||||
|
<>{t("proformas.delete_proforma_dialog.delete_plural")}</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|||||||
@ -37,15 +37,14 @@ export function useProformaListPageController() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeStatusDialogCtrl.openDialog(proforma, nextStatus);
|
changeStatusDialogCtrl.openDialog([proforma]);
|
||||||
},
|
},
|
||||||
[changeStatusDialogCtrl]
|
[changeStatusDialogCtrl]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteProforma = React.useCallback(
|
const handleDeleteProforma = React.useCallback(
|
||||||
(proforma: ProformaSummaryData) => {
|
(proforma: ProformaSummaryData) => {
|
||||||
console.log(proforma);
|
deleteDialogCtrl.openDialog([proforma]);
|
||||||
deleteDialogCtrl.openDialog(proforma);
|
|
||||||
},
|
},
|
||||||
[deleteDialogCtrl]
|
[deleteDialogCtrl]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -244,29 +244,28 @@ export function useProformasGridColumns(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cambiar estado */}
|
{/* Cambiar estado */}
|
||||||
{!isIssued &&
|
{!isIssued && availableTransitions.length && (
|
||||||
availableTransitions.map((next_status) => (
|
<TooltipProvider key={availableTransitions[0]}>
|
||||||
<TooltipProvider key={next_status}>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
className="size-8 cursor-pointer"
|
||||||
className="size-8 cursor-pointer"
|
onClick={() =>
|
||||||
onClick={() =>
|
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0])
|
||||||
actionHandlers.onChangeStatusClick?.(proforma, next_status)
|
}
|
||||||
}
|
size="icon"
|
||||||
size="icon"
|
variant="ghost"
|
||||||
variant="ghost"
|
>
|
||||||
>
|
<RefreshCwIcon className="size-4" />
|
||||||
<RefreshCwIcon className="size-4" />
|
<span className="sr-only">Cambiar estado</span>
|
||||||
<span className="sr-only">Cambiar estado</span>
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
||||||
Cambiar a {t(`catalog.proformas.status.${next_status}`)}
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
</TooltipProvider>
|
||||||
</TooltipProvider>
|
)}
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Emitir factura: solo si approved */}
|
{/* Emitir factura: solo si approved */}
|
||||||
{!isIssued && proforma.status === "approved" && (
|
{!isIssued && proforma.status === "approved" && (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Badge } from "@repo/shadcn-ui/components";
|
import { Badge, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -6,6 +6,7 @@ import {
|
|||||||
type ProformaStatus,
|
type ProformaStatus,
|
||||||
getProformaStatusButtonVariant,
|
getProformaStatusButtonVariant,
|
||||||
getProformaStatusColor,
|
getProformaStatusColor,
|
||||||
|
getProformaStatusIcon,
|
||||||
} from "../../../types";
|
} from "../../../types";
|
||||||
|
|
||||||
export type ProformaStatusBadgeProps = {
|
export type ProformaStatusBadgeProps = {
|
||||||
@ -16,14 +17,23 @@ export type ProformaStatusBadgeProps = {
|
|||||||
export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgeProps) => {
|
export const ProformaStatusBadge = ({ status, className }: ProformaStatusBadgeProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const normalizedStatus = status.toLowerCase() as ProformaStatus;
|
const normalizedStatus = status.toLowerCase() as ProformaStatus;
|
||||||
|
const Icon = getProformaStatusIcon(normalizedStatus);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Tooltip>
|
||||||
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
|
<TooltipTrigger asChild>
|
||||||
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
<Badge
|
||||||
>
|
className={cn(getProformaStatusColor(normalizedStatus), "font-semibold", className)}
|
||||||
{t(`catalog.proformas.status.${normalizedStatus}`, { defaultValue: status })}
|
variant={getProformaStatusButtonVariant(normalizedStatus)}
|
||||||
</Badge>
|
>
|
||||||
|
<Icon />
|
||||||
|
{t(`catalog.proformas.status.${normalizedStatus}.label`, { defaultValue: status })}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t(`catalog.proformas.status.${normalizedStatus}.description`)}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -86,12 +86,16 @@ export const ProformaListPage = () => {
|
|||||||
<SelectValue placeholder={t("filters.status")} />
|
<SelectValue placeholder={t("filters.status")} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">{t("catalog.proformas.status.all")}</SelectItem>
|
<SelectItem value="all">{t("catalog.proformas.status.all.label")}</SelectItem>
|
||||||
<SelectItem value="draft">{t("catalog.proformas.status.draft")}</SelectItem>
|
<SelectItem value="draft">{t("catalog.proformas.status.draft.label")}</SelectItem>
|
||||||
<SelectItem value="sent">{t("catalog.proformas.status.sent")}</SelectItem>
|
<SelectItem value="sent">{t("catalog.proformas.status.sent.label")}</SelectItem>
|
||||||
<SelectItem value="approved">{t("catalog.proformas.status.approved")}</SelectItem>
|
<SelectItem value="approved">
|
||||||
<SelectItem value="rejected">{t("catalog.proformas.status.rejected")}</SelectItem>
|
{t("catalog.proformas.status.approved.label")}
|
||||||
<SelectItem value="issued">{t("catalog.proformas.status.issued")}</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem value="rejected">
|
||||||
|
{t("catalog.proformas.status.rejected.label")}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="issued">{t("catalog.proformas.status.issued.label")}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@ -125,22 +129,18 @@ export const ProformaListPage = () => {
|
|||||||
<ChangeStatusDialog
|
<ChangeStatusDialog
|
||||||
isSubmitting={changeStatusDialogCtrl.isSubmitting}
|
isSubmitting={changeStatusDialogCtrl.isSubmitting}
|
||||||
onConfirm={changeStatusDialogCtrl.confirmChangeStatus}
|
onConfirm={changeStatusDialogCtrl.confirmChangeStatus}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => !open && changeStatusDialogCtrl.closeDialog()}
|
||||||
if (!open) changeStatusDialogCtrl.closeDialog();
|
|
||||||
}}
|
|
||||||
open={changeStatusDialogCtrl.open}
|
open={changeStatusDialogCtrl.open}
|
||||||
proformaRef={changeStatusDialogCtrl.proforma?.reference}
|
proformas={changeStatusDialogCtrl.proformas} // ← recibe el status seleccionado
|
||||||
proformas={changeStatusDialogCtrl.proformas}
|
|
||||||
targetStatus={changeStatusDialogCtrl.targetStatus ?? undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Eliminar */}
|
{/* Eliminar */}
|
||||||
<DeleteProformaDialog
|
<DeleteProformaDialog
|
||||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||||
onOpenChange={(o) => !o && deleteDialogCtrl.closeDialog()}
|
onOpenChange={(open) => !open && deleteDialogCtrl.closeDialog()}
|
||||||
open={deleteDialogCtrl.open}
|
open={deleteDialogCtrl.open}
|
||||||
proformaRef={deleteDialogCtrl.proforma?.reference}
|
proformas={deleteDialogCtrl.proformas}
|
||||||
/>
|
/>
|
||||||
</AppContent>
|
</AppContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
|
import {
|
||||||
|
CheckCircle2Icon,
|
||||||
|
FileCheckIcon,
|
||||||
|
FileQuestionIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
SendIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export enum PROFORMA_STATUS {
|
export enum PROFORMA_STATUS {
|
||||||
DRAFT = "draft",
|
DRAFT = "draft",
|
||||||
SENT = "sent",
|
SENT = "sent",
|
||||||
@ -52,3 +61,20 @@ export const getProformaStatusColor = (status: ProformaStatus): string => {
|
|||||||
return "bg-gray-100 text-gray-700 hover:bg-gray-100";
|
return "bg-gray-100 text-gray-700 hover:bg-gray-100";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProformaStatusIcon = (status: ProformaStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case "draft":
|
||||||
|
return FileTextIcon;
|
||||||
|
case "sent":
|
||||||
|
return SendIcon;
|
||||||
|
case "approved":
|
||||||
|
return CheckCircle2Icon;
|
||||||
|
case "rejected":
|
||||||
|
return XCircleIcon;
|
||||||
|
case "issued":
|
||||||
|
return FileCheckIcon;
|
||||||
|
default:
|
||||||
|
return FileQuestionIcon;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -1,5 +1,2 @@
|
|||||||
export * from "../../issue-proforma/ui/issue-proforma-dialog";
|
|
||||||
|
|
||||||
export * from "./proforma-delete-dialog";
|
|
||||||
export * from "./proforma-layout";
|
export * from "./proforma-layout";
|
||||||
export * from "./proforma-tax-summary";
|
export * from "./proforma-tax-summary";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user