Repaso de proformas

This commit is contained in:
David Arranz 2026-04-05 12:47:54 +02:00
parent ba67750e17
commit 6ea2e2df5b
84 changed files with 1192 additions and 330 deletions

View File

@ -63,6 +63,7 @@ export class ReportProformaUseCase {
if (documentResult.isFailure) {
return Result.fail(documentResult.error);
}
11;
// 5. Devolver artefacto firmado
return Result.ok({

View File

@ -13,7 +13,7 @@ import {
import {
CreateProformaRequestSchema,
GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema,
IssueProformaByIdResponseSchema,
ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema,
@ -148,7 +148,7 @@ export const proformasRouter = (params: StartParams) => {
"/:proforma_id/issue",
//checkTabContext,
validateRequest(IssueProformaByIdParamsRequestSchema, "params"),
validateRequest(IssueProformaByIdResponseSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.issueProforma(publicServices);

View File

@ -3,3 +3,7 @@ import { z } from "zod/v4";
export const IssueProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
});
export type IssueProformaByIdParamsRequestDTO = z.infer<
typeof IssueProformaByIdParamsRequestSchema
>;

View File

@ -1,6 +1,16 @@
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { NumericStringSchema, PercentageSchema } from "@erp/core";
import { z } from "zod/v4";
export const UpdateProformaItemRequestSchema = z.object({
id: z.uuid(),
position: z.string(),
description: z.string().optional(),
quantity: NumericStringSchema.optional(),
unit_amount: NumericStringSchema.optional(),
item_discount_percentage: PercentageSchema.optional(),
taxes: z.string().optional(),
});
export const UpdateProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
});
@ -20,22 +30,7 @@ export const UpdateProformaByIdRequestSchema = z.object({
language_code: z.string().optional(),
currency_code: z.string().optional(),
items: z
.array(
z.object({
is_valued: z.string().optional(),
description: z.string().optional(),
quantity: QuantitySchema.optional(),
unit_amount: MoneySchema.optional(),
discount_percentage: PercentageSchema.optional(),
tax_codes: z.array(z.string()).default([]),
})
)
.optional()
.default([]),
items: z.array(UpdateProformaItemRequestSchema).default([]),
});
export type UpdateProformaByIdRequestDTO = Partial<z.infer<typeof UpdateProformaByIdRequestSchema>>;

View File

@ -1,3 +1,6 @@
export * from "./create-proforma.response.dto";
export * from "./get-proforma-by-id.response.dto";
export * from "./issue-proforma-by-id.response.dto";
export * from "./list-proformas.response.dto";
export * from "./report-proforma-by-id.response.dto";
export * from "./update-proformas-by-id.response.dto";

View File

@ -0,0 +1,9 @@
import { z } from "zod/v4";
export const IssueProformaByIdResponseSchema = z.object({
issuedinvoice_id: z.string(),
proforma_id: z.string(),
customer_id: z.string(),
});
export type IssueProformaByIdResponseDTO = z.infer<typeof IssueProformaByIdResponseSchema>;

View File

@ -10,7 +10,7 @@ export const ListProformasResponseSchema = createPaginatedListSchema(
z.object({
id: z.uuid(),
company_id: z.uuid(),
is_proforma: z.boolean(),
is_proforma: z.string(),
customer_id: z.string(),

View File

@ -0,0 +1 @@
export type ReportProformaByIdResponseDTO = Blob;

View File

@ -0,0 +1,7 @@
import {
type GetProformaByIdResponseDTO,
GetProformaByIdResponseSchema,
} from "./get-proforma-by-id.response.dto";
export const UpdateProformaByIdResponseSchema = GetProformaByIdResponseSchema;
export type UpdateProformaByIdResponseDTO = GetProformaByIdResponseDTO;

View File

@ -3,7 +3,7 @@ import { lazy } from "react";
import { Outlet, type RouteObject } from "react-router-dom";
const ProformaLayout = lazy(() =>
import("./proformas/shared").then((m) => ({ default: m.ProformaLayout }))
import("./proformas/shared2").then((m) => ({ default: m.ProformaLayout }))
);
const ProformasListPage = lazy(() =>

View File

@ -38,8 +38,8 @@ import { useFieldArray, useForm } from "react-hook-form";
import * as z from "zod";
import { useTranslation } from "../../i18n";
import { CustomerInvoicePricesCard } from "../../proformas/shared/ui/components";
import { CustomerInvoiceItemsCardEditor } from "../../proformas/shared/ui/components/items";
import { CustomerInvoicePricesCard } from "../../proformas/shared2/ui/components";
import { CustomerInvoiceItemsCardEditor } from "../../proformas/shared2/ui/components/items";
import type { CustomerInvoiceData } from "./customer-invoice.schema";
import { formatCurrency } from "./utils";

View File

@ -2,9 +2,9 @@ import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import * as React from "react";
import { useTranslation } from "../../../i18n";
import type { ProformaListRow } from "../../shared";
import type { PROFORMA_STATUS } from "../../shared/entities";
import { useChangeProformaStatusMutation } from "../../shared/hooks";
import type { ProformaListRow } from "../../shared2";
import type { PROFORMA_STATUS } from "../../shared2/entities";
import { useChangeProformaStatusMutation } from "../../shared2/hooks";
interface ChangeStatusDialogState {
open: boolean;

View File

@ -7,7 +7,7 @@ import {
XCircleIcon,
} from "lucide-react";
import type { ProformaStatus } from "../../shared";
import type { ProformaStatus } from "../../shared2";
export const getProformaStatusButtonVariant = (
status: ProformaStatus

View File

@ -11,7 +11,7 @@ import {
import { useState } from "react";
import { useTranslation } from "../../../i18n";
import { PROFORMA_STATUS, PROFORMA_STATUS_TRANSITIONS, type ProformaListRow } from "../../shared";
import { PROFORMA_STATUS, PROFORMA_STATUS_TRANSITIONS, type ProformaListRow } from "../../shared2";
import { getProformaStatusIcon } from "../helpers";
import { StatusNode, TimelineConnector } from "./components";

View File

@ -4,7 +4,7 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
import { CheckCircle2, type LucideIcon } from "lucide-react";
import { useTranslation } from "../../../../i18n";
import type { PROFORMA_STATUS } from "../../../shared";
import type { PROFORMA_STATUS } from "../../../shared2";
interface StatusNodeProps {
status: PROFORMA_STATUS;

View File

@ -0,0 +1,3 @@
export * from "./proforma-create-form.entity";
export * from "./proforma-create-form.schema";
export * from "./proforma-create-form-default";

View File

@ -0,0 +1,49 @@
/**
* Valor por defecto para el formulario de creación de proformas.
*
* Reglas:
* - debe ser un objeto que cumpla con la interfaz ProformaCreateForm
* - debe tener valores por defecto razonables para cada campo, evitando campos vacíos o nulos cuando sea posible
* - el shape del objeto debe coincidir exactamente con el de ProformaCreateForm, sin campos adicionales ni transformaciones
* - orientado a la UI, no a la API ni al dominio, es decir, debe ser un objeto que se pueda usar directamente para inicializar un formulario en la interfaz de usuario
*/
export const defaultProformaCreateForm: ProformaCreateForm = {
invoiceNumber: "",
series: "",
invoiceDate: "",
operationDate: "",
customerId: "",
reference: "",
isCompany: true,
name: "",
tradeName: "",
tin: "",
defaultTaxes: [],
street: "",
street2: "",
city: "",
province: "",
postalCode: "",
country: "es",
primaryEmail: "",
secondaryEmail: "",
primaryPhone: "",
secondaryPhone: "",
primaryMobile: "",
secondaryMobile: "",
fax: "",
website: "",
legalRecord: "",
languageCode: "es",
currencyCode: "EUR",
};

View File

@ -0,0 +1,37 @@
/**
* ProformaCreateForm representa el shape de datos del formulario de creación de proforma.
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
*
* Este shape es específico para la UI y no tiene por qué coincidir
* con el shape del dominio ni con el de la API.
*
* Debe cumplir las siguientes reglas:
* - nombres en camelCase
* - tipos orientados a UI/form
* - sin campos de solo lectura que no se editen
* - sin shape DTO
* - sin detalles impuestos por el widget
*/
import type { ProformaItemForm } from "../../shared/entities";
export interface ProformaCreateForm {
series: string;
invoiceDate: string;
operationDate: string;
customerId: string;
reference: string;
notes: string;
languageCode: string;
currencyCode: string;
globalDiscountPercentage: number;
paymentMethod: string;
items: ProformaItemForm[];
}

View File

@ -0,0 +1,48 @@
import { z } from "zod/v4";
/**
* Este esquema es para validar los datos del formulario de creación de cliente.
* No tiene por qué coincidir con el shape de la entidad ni con el de la API.
* Solo define los campos que se muestran en el formulario y sus validaciones.
*
* Reglas:
* - no meter transformaciones silenciosas raras en el esquema (ej: .toUpperCase())
* - nombres en camelCase
* - tipos orientados a UI/form
* - sin campos de solo lectura que no se editen
* - sin shape DTO
* - sin detalles impuestos por el widget
*/
export const CustomerCreateFormSchema = z.object({
reference: z.string(),
isCompany: z.boolean(),
name: z.string().min(1, "El nombre es obligatorio"),
tradeName: z.string(),
tin: z.string(),
defaultTaxes: z.array(z.string()),
street: z.string(),
street2: z.string(),
city: z.string(),
province: z.string(),
postalCode: z.string(),
country: z.string().min(1, "El país es obligatorio"),
primaryEmail: z.email("Email inválido"),
secondaryEmail: z.email("Email inválido"),
primaryPhone: z.string(),
secondaryPhone: z.string(),
primaryMobile: z.string(),
secondaryMobile: z.string(),
fax: z.string(),
website: z.url("URL inválida"),
legalRecord: z.string(),
languageCode: z.string().min(1, "El idioma es obligatorio"),
currencyCode: z.string().min(1, "La moneda es obligatoria"),
});

View File

@ -0,0 +1,9 @@
export interface ProformaItemForm {
id: string;
position: string;
description: string;
quantity: number;
unitAmount: number;
discountPercentage: number;
taxes: string;
}

View File

@ -2,7 +2,7 @@ import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
import React from "react";
import { useTranslation } from "../../../i18n";
import { type ProformaListRow, useDeleteProformaMutation } from "../../shared";
import { type ProformaListRow, useDeleteProformaMutation } from "../../shared2";
interface DeleteProformaDialogState {
open: boolean;

View File

@ -10,7 +10,7 @@ import {
} from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../../i18n";
import type { ProformaListRow } from "../../../shared";
import type { ProformaListRow } from "../../../shared2";
interface DeleteProformaDialogProps {
open: boolean;

View File

@ -1 +0,0 @@
export * from "./issue-proforma-invoice.api";

View File

@ -1,18 +0,0 @@
import type { IDataSource } from "@erp/core/client";
export interface IssueProformaInvoiceResponse {
invoiceId: string;
proformaId: string;
customerId: string;
}
export async function issueProformaInvoiceApi(
dataSource: IDataSource,
proformaId: string
): Promise<IssueProformaInvoiceResponse> {
return dataSource.custom<IssueProformaInvoiceResponse>({
path: `proformas/${proformaId}/issue`,
method: "put",
data: {},
});
}

View File

@ -3,11 +3,11 @@ import React from "react";
import { useChangeProformaStatusDialogController } from "../../change-status";
import { useDeleteProformaDialogController } from "../../delete";
import { useProformaIssueDialogController } from "../../issue-proforma";
import type { ProformaListRow } from "../../shared";
import type { ProformaListRow } from "../../shared2";
import {
type PROFORMA_STATUS,
PROFORMA_STATUS_TRANSITIONS,
} from "../../shared/entities/proforma-status.entity";
} from "../../shared2/entities/proforma-status.entity";
import { useListProformasController } from "./use-list-proformas.controller";

View File

@ -2,7 +2,7 @@ import type { CriteriaDTO } from "@erp/core";
import { useDebounce } from "@repo/rdx-ui/components";
import { useMemo, useState } from "react";
import { useListProformasQuery } from "../../shared";
import { useListProformasQuery } from "../../shared2";
export const useListProformasController = () => {
const [pageIndex, setPageIndex] = useState(0);

View File

@ -3,7 +3,7 @@ import type { ColumnDef } from "@tanstack/react-table";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../../i18n";
import type { ProformaList, ProformaListRow } from "../../../../shared";
import type { ProformaList, ProformaListRow } from "../../../../shared2";
interface ProformasGridProps {
data?: ProformaList;

View File

@ -21,7 +21,7 @@ import {
PROFORMA_STATUS_TRANSITIONS,
type ProformaListRow,
type ProformaStatus,
} from "../../../../shared";
} from "../../../../shared2";
import { ProformaStatusBadge } from "../../components";
type GridActionHandlers = {

View File

@ -7,7 +7,7 @@ import {
getProformaStatusColor,
getProformaStatusIcon,
} from "../../../change-status/helpers";
import type { ProformaStatus } from "../../../shared";
import type { ProformaStatus } from "../../../shared2";
export type ProformaStatusBadgeProps = {
status: string | ProformaStatus; // permitir cualquier valor

View File

@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../i18n";
import { ProformaDtoAdapter } from "../../adapters";
import { useUpdateProforma } from "../../shared/hooks/use-proforma-update-mutation";
import { useUpdateProforma } from "../../shared2/hooks/use-proforma-update-mutation";
import {
type Proforma,
type ProformaFormData,

View File

@ -0,0 +1,129 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { GetProformaByIdResponseDTO } from "../../../../common";
import type {
Proforma,
ProformaItem,
ProformaRecipient,
ProformaStatus,
ProformaTaxSummary,
} from "../entities";
export const GetProformaByIdAdapter = {
fromDto(dto: GetProformaByIdResponseDTO): Proforma {
return {
id: dto.id,
companyId: dto.company_id,
isProforma: dto.is_proforma === "1",
invoiceNumber: dto.invoice_number,
status: dto.status as ProformaStatus,
series: dto.series,
invoiceDate: dto.invoice_date,
operationDate: dto.operation_date,
reference: dto.reference,
description: dto.description,
notes: dto.notes,
languageCode: dto.language_code,
currencyCode: dto.currency_code,
customerId: dto.customer_id,
recipient: mapRecipient(dto.recipient),
taxes: dto.taxes.map(mapTaxSummary),
paymentMethod: dto.payment_method?.payment_id ?? "",
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount),
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
discountAmount: MoneyDTOHelper.toNumber(dto.discount_amount),
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
ivaAmount: MoneyDTOHelper.toNumber(dto.iva_amount),
recAmount: MoneyDTOHelper.toNumber(dto.rec_amount),
retentionAmount: MoneyDTOHelper.toNumber(dto.retention_amount),
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
items: dto.items.map(mapItem),
};
},
};
const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem => {
return {
id: dto.id,
position: dto.position,
description: dto.description,
quantity: QuantityDTOHelper.toNumber(dto.quantity),
unitAmount: MoneyDTOHelper.toNumber(dto.unit_amount),
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemDiscountPercentage: PercentageDTOHelper.toNumber(dto.item_discount_percentage),
itemDiscountAmount: MoneyDTOHelper.toNumber(dto.item_discount_amount),
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.global_discount_percentage),
globalDiscountAmount: MoneyDTOHelper.toNumber(dto.global_discount_amount),
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
ivaCode: dto.iva_code,
ivaPercentage: PercentageDTOHelper.toNumber(dto.iva_percentage),
ivaAmount: MoneyDTOHelper.toNumber(dto.iva_amount),
recCode: dto.rec_code,
recPercentage: PercentageDTOHelper.toNumber(dto.rec_percentage),
recAmount: MoneyDTOHelper.toNumber(dto.rec_amount),
retentionCode: dto.retention_code,
retentionPercentage: PercentageDTOHelper.toNumber(dto.retention_percentage),
retentionAmount: MoneyDTOHelper.toNumber(dto.retention_amount),
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
// Nota: el DTO de detalle actual no incluye `taxes` por línea.
// Dejamos fallback explícito para no inventar parseos.
taxes: "",
};
};
const mapRecipient = (dto: GetProformaByIdResponseDTO["recipient"]): ProformaRecipient => {
return {
id: dto.id,
name: dto.name,
tin: dto.tin,
street: dto.street,
street2: dto.street2,
city: dto.city,
province: dto.province,
postalCode: dto.postal_code,
country: dto.country,
};
};
const mapTaxSummary = (dto: GetProformaByIdResponseDTO["taxes"][number]): ProformaTaxSummary => {
return {
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
ivaCode: dto.iva_code,
ivaPercentage: PercentageDTOHelper.toNumber(dto.iva_percentage),
ivaAmount: MoneyDTOHelper.toNumber(dto.iva_amount),
recCode: dto.rec_code,
recPercentage: PercentageDTOHelper.toNumber(dto.rec_percentage),
recAmount: MoneyDTOHelper.toNumber(dto.rec_amount),
retentionCode: dto.retention_code,
retentionPercentage: PercentageDTOHelper.toNumber(dto.retention_percentage),
retentionAmount: MoneyDTOHelper.toNumber(dto.retention_amount),
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
};
};

View File

@ -1 +0,0 @@
export * from "./list-proformas.adapter";

View File

@ -1,101 +1,98 @@
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
import type { ListProformasResponseDTO } from "../../../../common";
import type { ProformaList, ProformaListRow } from "../entities";
import type { ProformaList, ProformaListRow, ProformaStatus } from "../entities";
export const ListProformasAdapter = {
fromDto(dto: ListProformasResponseDTO): ProformaList {
return {
items: dto.items.map(ProformaListRowAdapter.fromDto),
page: dto.page,
per_page: dto.per_page,
total_pages: dto.total_pages,
total_items: dto.total_items,
};
},
};
type ListProformasItemDTO = ListProformasResponseDTO["items"][number];
export const ListProformasAdapter = {
fromDTO(pageDto: ListProformasResponseDTO, context?: unknown): ProformaList {
const ProformaListRowAdapter = {
fromDto(dto: ListProformasItemDTO): ProformaListRow {
return {
//...pageDto,
page: pageDto.page,
per_page: pageDto.per_page,
total_pages: pageDto.total_pages,
total_items: pageDto.total_items,
items: pageDto.items.map((row) => ListProformasRowAdapter.fromDTO(row, context)),
};
},
};
id: dto.id,
companyId: dto.company_id,
isProforma: dto.is_proforma === "1",
export const ListProformasRowAdapter = {
fromDTO(rowDto: ListProformasItemDTO, context?: unknown): ProformaListRow {
return {
//...rowDto,
id: rowDto.id,
company_id: rowDto.company_id,
invoiceNumber: dto.invoice_number,
status: dto.status as ProformaStatus,
series: dto.series,
customer_id: rowDto.company_id,
invoiceDate: dto.invoice_date,
operationDate: dto.operation_date,
invoice_number: rowDto.invoice_number,
status: rowDto.status,
series: rowDto.series,
invoice_date: rowDto.invoice_date,
operation_date: rowDto.operation_date,
language_code: rowDto.language_code,
currency_code: rowDto.currency_code,
reference: rowDto.reference,
description: rowDto.description,
languageCode: dto.language_code,
currencyCode: dto.currency_code,
reference: dto.reference,
description: dto.description,
recipient: {
tin: rowDto.recipient.tin,
name: rowDto.recipient.name,
id: dto.customer_id,
tin: dto.recipient.tin,
name: dto.recipient.name,
street: rowDto.recipient.street,
street2: rowDto.recipient.street2,
city: rowDto.recipient.city,
province: rowDto.recipient.province,
postal_code: rowDto.recipient.postal_code,
country: rowDto.recipient.country,
street: dto.recipient.street,
street2: dto.recipient.street2,
city: dto.recipient.city,
province: dto.recipient.province,
postalCode: dto.recipient.postal_code,
country: dto.recipient.country,
},
subtotal_amount: MoneyDTOHelper.toNumber(rowDto.subtotal_amount),
subtotal_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(rowDto.subtotal_amount),
Number(rowDto.total_amount.scale || 2),
rowDto.currency_code,
rowDto.language_code
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
subtotalAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.subtotal_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
discount_percentage: PercentageDTOHelper.toNumber(rowDto.discount_percentage),
discount_percentage_fmt: PercentageDTOHelper.toNumericString(rowDto.discount_percentage),
discountPercentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
discountPercentageFmt: PercentageDTOHelper.toNumericString(dto.discount_percentage),
discount_amount: MoneyDTOHelper.toNumber(rowDto.discount_amount),
discount_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(rowDto.discount_amount),
Number(rowDto.total_amount.scale || 2),
rowDto.currency_code,
rowDto.language_code
discountAmount: MoneyDTOHelper.toNumber(dto.discount_amount),
discountAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.discount_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
taxable_amount: MoneyDTOHelper.toNumber(rowDto.taxable_amount),
taxable_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(rowDto.taxable_amount),
Number(rowDto.total_amount.scale || 2),
rowDto.currency_code,
rowDto.language_code
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
taxableAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.taxable_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
taxes_amount: MoneyDTOHelper.toNumber(rowDto.taxes_amount),
taxes_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(rowDto.taxes_amount),
Number(rowDto.total_amount.scale || 2),
rowDto.currency_code,
rowDto.language_code
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
taxesAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.taxes_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
total_amount: MoneyDTOHelper.toNumber(rowDto.total_amount),
total_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(rowDto.total_amount),
Number(rowDto.total_amount.scale || 2),
rowDto.currency_code,
rowDto.language_code
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
totalAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.total_amount),
Number(dto.total_amount.scale || 2),
dto.currency_code,
dto.language_code
),
linked_invoice_id: rowDto.linked_invoice_id,
linkedInvoiceId: dto.linked_invoice_id,
};
},
};

View File

@ -0,0 +1,42 @@
import type { Proforma, ProformaListRow } from "../entities";
/**
* Utilizado por el gestor de caché para actualizar algunos campos del objeto ProfromaListRow.
* Adaptador para transformar un objeto Proforma a un objeto ProformaListRowPatch,
* que es una versión parcial de ProformaListRow.
* Reglas de adaptación:
* - los campos se asignan directamente
* - los campos son opcionales excepto el id.
*
* @param proforma - objeto Proforma a adaptar.
* @returns {ProformaListRowPatch} Objeto adaptado a ProformaListRowPatch.
*/
export type ProformaListRowPatch = Partial<ProformaListRow> & Pick<ProformaListRow, "id">;
export const ProformaToListRowPatchAdapter = {
fromProforma(proforma: Proforma): ProformaListRowPatch {
return {
id: proforma.id,
customerId: proforma.customerId,
invoiceNumber: proforma.invoiceNumber,
status: proforma.status,
series: proforma.series,
invoiceDate: proforma.invoiceDate,
operationDate: proforma.operationDate,
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
reference: proforma.reference,
description: proforma.description,
recipientName: proforma.recipient.name,
recipientTin: proforma.recipient.tin,
subtotalAmount: proforma.subtotalAmount,
discountPercentage: proforma.discountPercentage,
discountAmount: proforma.discountAmount,
taxableAmount: proforma.taxableAmount,
taxesAmount: proforma.taxesAmount,
totalAmount: proforma.totalAmount,
};
},
};

View File

@ -1,17 +1,25 @@
import type { IDataSource } from "@erp/core/client";
export interface ChangeStatusResponse {
success: boolean;
import type { ChangeStatusProformaByIdRequestDTO } from "../../../../common";
export interface ChangeProformaStatusByIdParams {
id: string;
data: ChangeStatusProformaByIdRequestDTO;
}
export type ChangeProformaStatusByIdResult = void;
export async function changeProformaStatusById(
dataSource: IDataSource,
proformaId: string,
newStatus: string
): Promise<ChangeStatusResponse> {
return dataSource.custom<ChangeStatusResponse>({
path: `proformas/${proformaId}/status`,
params: ChangeProformaStatusByIdParams
): Promise<ChangeProformaStatusByIdResult> {
const { id, data } = params;
if (!id) throw new Error("proformaId is required");
return dataSource.custom<ChangeStatusProformaByIdRequestDTO, ChangeProformaStatusByIdResult>({
path: `proformas/${id}/status`,
method: "patch",
data: { new_status: newStatus },
data,
});
}

View File

@ -0,0 +1,33 @@
import type { IDataSource } from "@erp/core/client";
import type { CreateProformaRequestDTO, CreateProformaResponseDTO } from "../../../../common";
/**
* Crea una nueva proforma en el sistema utilizando la fuente de datos proporcionada.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para crear la proforma.
* @returns Una promesa que resuelve con los detalles de la proforma creada.
* @throws Error si el ID de la proforma no es proporcionado o si la creación falla.
*/
export interface CreateProformaParams {
id: string;
data: CreateProformaRequestDTO;
}
export type CreateProformaResult = CreateProformaResponseDTO;
export const createProforma = (
dataSource: IDataSource,
params: CreateProformaParams
): Promise<CreateProformaResult> => {
const { id, data } = params;
if (!id) throw new Error("proformaId is required");
return dataSource.createOne<CreateProformaRequestDTO, CreateProformaResponseDTO>(
"proformas",
data
);
};

View File

@ -0,0 +1,28 @@
import type { IDataSource } from "@erp/core/client";
/**
* Elimina una proforma existente en el sistema utilizando la fuente de datos proporcionada.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para eliminar la proforma.
* @returns Una promesa que se resuelve cuando la proforma ha sido eliminada exitosamente.
* @throws Error si el ID de la proforma no es proporcionado o si la eliminación falla.
*/
export interface DeleteProformaByIdParams {
id: string;
signal?: AbortSignal;
}
export type DeleteProformaByIdResult = void;
export const deleteProformaById = (
dataSource: IDataSource,
params: DeleteProformaByIdParams
): Promise<DeleteProformaByIdResult> => {
const { id, signal } = params;
if (!id) throw new Error("proformaId is required");
return dataSource.deleteOne("proformas", id, { signal });
};

View File

@ -1,8 +0,0 @@
import type { IDataSource } from "@erp/core/client";
export async function deleteProformaById(
dataSource: IDataSource,
proformaId: string
): Promise<void> {
await dataSource.deleteOne("proformas", proformaId);
}

View File

@ -0,0 +1,30 @@
import type { IDataSource } from "@erp/core/client";
import type { GetProformaByIdResponseDTO } from "../../../../common";
/**
* Recupera los detalles de una proforma específica utilizando su ID a través de la fuente de datos proporcionada.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para obtener la proforma, incluyendo su ID.
* @returns Una promesa que resuelve con los detalles de la proforma solicitada.
* @throws Error si el ID de la proforma no es proporcionado o si la recuperación falla.
*/
export interface GetProformaByIdParams {
id: string;
signal?: AbortSignal;
}
export type GetProformaByIdResult = GetProformaByIdResponseDTO;
export function getProformaById(
dataSource: IDataSource,
params: GetProformaByIdParams
): Promise<GetProformaByIdResult> {
const { id, signal } = params;
if (!id) throw new Error("proformaId is required");
return dataSource.getOne<GetProformaByIdResponseDTO>("proformas", id, { signal });
}

View File

@ -1,9 +0,0 @@
import type { IDataSource } from "@erp/core/client";
import type { Proforma } from "../entities";
export async function getProformaById(dataSource: IDataSource, signal: AbortSignal, id?: string) {
if (!id) throw new Error("proformaId is required");
const response = dataSource.getOne<Proforma>("proformas", id, { signal });
return response;
}

View File

@ -1,4 +1,7 @@
export * from "./change-proforma-status-by-id.api";
export * from "./delete-proforma-by-ip.api";
export * from "./get-proforma-by-ip.api";
export * from "./list-proformas.api";
export * from "./create-proforma.api";
export * from "./delete-proforma-by-id.api";
export * from "./get-proforma-by-id.api";
export * from "./issue-proforma-by-id.api";
export * from "./list-proformas-by-criteria.api";
export * from "./update-proforma-by-id.api";

View File

@ -0,0 +1,23 @@
import type { IDataSource } from "@erp/core/client";
import type { IssueProformaByIdResponseDTO } from "@erp/customer-invoices/common";
export interface IssueProformaByIdParams {
id: string;
}
export type IssueProformaByIdResult = IssueProformaByIdResponseDTO;
export const issueProformaById = (
dataSource: IDataSource,
params: IssueProformaByIdParams
): Promise<IssueProformaByIdResult> => {
const { id } = params;
if (!id) throw new Error("proformaId is required");
return dataSource.custom<Record<string, never>, IssueProformaByIdResponseDTO>({
path: `proformas/${id}/issue`,
method: "put",
data: {},
});
};

View File

@ -0,0 +1,32 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { ListProformasResponseDTO } from "../../../../common";
/**
* Recupera una lista de proformas del sistema utilizando la
* fuente de datos proporcionada y los criterios de búsqueda especificados.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para listar las proformas, incluyendo los criterios de búsqueda.
* @returns Una promesa que resuelve con una lista de proformas que cumplen con los criterios especificados.
* @throws Error si la recuperación de la lista de proformas falla.
*/
export type ListProformasByCriteriaParams = {
criteria?: CriteriaDTO;
signal?: AbortSignal;
};
export type ListProformasResult = ListProformasResponseDTO;
export function getListProformasByCriteria(
dataSource: IDataSource,
params: ListProformasByCriteriaParams
): Promise<ListProformasResult> {
const { criteria, signal } = params || { criteria: undefined, signal: undefined };
return dataSource.getList<ListProformasResponseDTO>("proformas", {
signal,
...criteria,
});
}

View File

@ -1,17 +0,0 @@
import type { CriteriaDTO } from "@erp/core";
import type { IDataSource } from "@erp/core/client";
import type { ListProformasResponseDTO } from "../../../../common";
export async function getListProformas(
dataSource: IDataSource,
signal: AbortSignal,
criteria: CriteriaDTO
) {
const response = dataSource.getList<ListProformasResponseDTO>("proformas", {
signal,
...criteria,
});
return response;
}

View File

@ -0,0 +1,35 @@
import type { IDataSource } from "@erp/core/client";
import type {
ReportProformaByIdQueryRequestDTO,
ReportProformaByIdResponseDTO,
} from "../../../../common";
export type ProformaReportFormat = "PDF" | "HTML" | "JSON";
export interface ReportProformaByIdParams {
id: string;
data: ReportProformaByIdQueryRequestDTO;
}
export type ReportProformaByIdResult = ReportProformaByIdResponseDTO;
export const reportProformaById = (
dataSource: IDataSource,
params: ReportProformaByIdParams
): Promise<ReportProformaByIdResult> => {
const {
id,
data: { format = "PDF" },
} = params;
if (!id) throw new Error("proformaId is required");
return dataSource.custom<ReportProformaByIdQueryRequestDTO, ReportProformaByIdResponseDTO>({
path: `proformas/${id}/report`,
method: "get",
data: {
format,
},
});
};

View File

@ -0,0 +1,37 @@
import type { IDataSource } from "@erp/core/client";
import type {
UpdateProformaByIdRequestDTO,
UpdateProformaByIdResponseDTO,
} from "../../../../common";
/**
* Actualiza una proforma existente en el sistema utilizando la fuente de datos proporcionada.
*
* @param dataSource - La fuente de datos para interactuar con la API.
* @param params - Los parámetros necesarios para actualizar la proforma.
* @returns Una promesa que resuelve con los detalles de la proforma actualizada.
* @throws Error si el ID de la proforma no es proporcionado o si la actualización falla.
*/
export interface UpdateProformaByIdParams {
id: string;
data: UpdateProformaByIdRequestDTO;
}
export type UpdateProformaByIdResult = UpdateProformaByIdResponseDTO;
export const updateProformaById = (
dataSource: IDataSource,
params: UpdateProformaByIdParams
): Promise<UpdateProformaByIdResult> => {
const { id, data } = params;
if (!id) throw new Error("proformaId is required");
return dataSource.updateOne<UpdateProformaByIdRequestDTO, UpdateProformaByIdResponseDTO>(
"proformas",
id,
data
);
};

View File

@ -0,0 +1 @@
export * from "./payment-method-options.constants";

View File

@ -0,0 +1,5 @@
export const PAYMENT_METHOD_OPTIONS = [
{ label: "Transferencia", value: "6" },
{ label: "Transferencia bancaria", value: "15" },
{ label: "Domiciliación bancaria", value: "14" },
];

View File

@ -0,0 +1 @@
export * from "./proforma-item-form.entity";

View File

@ -0,0 +1,9 @@
export interface ProformaItemForm {
id: string;
position: string;
description: string;
quantity: number; // scale = 2
unitAmount: number; // pendiente confirmar escala exacta de importe
discountPercentage: number; // 0..100
taxes: string;
}

View File

@ -1,3 +1,8 @@
export * from "./forms/proforma-item-form.entity";
export * from "./proforma.entity";
export * from "./proforma-item.entity";
export * from "./proforma-list.entity";
export * from "./proforma-list-row.entity";
export * from "./proforma-recipient.entity";
export * from "./proforma-status.entity";
export * from "./proforma-tax-summary.entity";

View File

@ -0,0 +1,42 @@
/**
* Interface que representa una línea de proforma en el sistema,
* adaptada desde la respuesta de la API.
* Contiene todos los campos detallados de la línea.
*/
export interface ProformaItem {
id: string;
position: string;
description: string;
quantity: number;
unitAmount: number;
subtotalAmount: number;
itemDiscountPercentage: number;
itemDiscountAmount: number;
globalDiscountPercentage: number;
globalDiscountAmount: number;
taxableAmount: number;
ivaCode: string;
ivaPercentage: number;
ivaAmount: number;
recCode: string;
recPercentage: number;
recAmount: number;
retentionCode: string;
retentionPercentage: number;
retentionAmount: number;
taxesAmount: number;
totalAmount: number;
taxes: string;
}

View File

@ -1,51 +1,50 @@
import type { ProformaRecipient } from "./proforma-recipient.entity";
import type { ProformaStatus } from "./proforma-status.entity";
/**
* Interface que representa una fila de la lista de
* proformas en el sistema, adaptada desde la respuesta de la API.
* Contiene los campos justos para mostrar
* la información básica de cada proforma en la lista.
*/
export interface ProformaListRow {
id: string;
company_id: string;
companyId: string;
isProforma: boolean;
customer_id: string;
invoice_number: string;
status: string;
invoiceNumber: string;
status: ProformaStatus;
series: string;
invoice_date: string;
operation_date: string;
invoiceDate: string;
operationDate: string;
language_code: string;
currency_code: string;
languageCode: string;
currencyCode: string;
reference: string;
description: string;
recipient: {
tin: string;
name: string;
recipient: ProformaRecipient;
street: string;
street2: string;
city: string;
province: string;
postal_code: string;
country: string;
};
subtotalAmount: number;
subtotalAmountFmt: string;
subtotal_amount: number;
subtotal_amount_fmt: string;
discountPercentage: number;
discountPercentageFmt: string;
discount_percentage: number;
discount_percentage_fmt: string;
discountAmount: number;
discountAmountFmt: string;
discount_amount: number;
discount_amount_fmt: string;
taxableAmount: number;
taxableAmountFmt: string;
taxable_amount: number;
taxable_amount_fmt: string;
taxesAmount: number;
taxesAmountFmt: string;
taxes_amount: number;
taxes_amount_fmt: string;
totalAmount: number;
totalAmountFmt: string;
total_amount: number;
total_amount_fmt: string;
linked_invoice_id: string;
linkedInvoiceId: string;
}

View File

@ -1,5 +1,10 @@
import type { ProformaListRow } from "./proforma-list-row.entity";
/**
* Interface que representa la respuesta paginada de una lista de proformas,
* adaptada desde la respuesta de la API.
*/
export interface ProformaList {
items: ProformaListRow[];
total_pages: number;

View File

@ -0,0 +1,18 @@
/**
* Interface que representa el destinatario de una proforma en el sistema,
* adaptada desde la respuesta de la API.
*/
export interface ProformaRecipient {
id: string;
name: string;
tin: string;
street: string;
street2: string;
city: string;
province: string;
postalCode: string;
country: string;
}

View File

@ -1,3 +1,8 @@
/**
* Enumeración que representa
* los posibles estados de una proforma en el sistema.
*/
export enum PROFORMA_STATUS {
DRAFT = "draft",
SENT = "sent",
@ -6,13 +11,4 @@ export enum PROFORMA_STATUS {
ISSUED = "issued",
}
// Transiciones válidas según reglas del dominio
export const PROFORMA_STATUS_TRANSITIONS: Record<PROFORMA_STATUS, PROFORMA_STATUS[]> = {
[PROFORMA_STATUS.DRAFT]: [PROFORMA_STATUS.SENT],
[PROFORMA_STATUS.SENT]: [PROFORMA_STATUS.APPROVED, PROFORMA_STATUS.REJECTED],
[PROFORMA_STATUS.APPROVED]: [PROFORMA_STATUS.ISSUED, PROFORMA_STATUS.DRAFT],
[PROFORMA_STATUS.REJECTED]: [PROFORMA_STATUS.DRAFT],
[PROFORMA_STATUS.ISSUED]: [],
};
export type ProformaStatus = `${PROFORMA_STATUS}`;

View File

@ -0,0 +1,23 @@
/**
* Interface que representa el resumen de impuestos
* de una proforma en el sistema,
*
*/
export interface ProformaTaxSummary {
taxableAmount: number;
ivaCode: string;
ivaPercentage: number;
ivaAmount: number;
recCode: string;
recPercentage: number;
recAmount: number;
retentionCode: string;
retentionPercentage: number;
retentionAmount: number;
taxesAmount: number;
}

View File

@ -0,0 +1,54 @@
import type { ProformaItem } from "./proforma-item.entity";
import type { ProformaRecipient } from "./proforma-recipient.entity";
import type { ProformaStatus } from "./proforma-status.entity";
import type { ProformaTaxSummary } from "./proforma-tax-summary.entity";
/**
* Interface que representa una proforma en el sistema,
* adaptada desde la respuesta de la API.
* Contiene todos los campos detallados de la proforma.
*
*/
export interface Proforma {
id: string;
companyId: string;
isProforma: boolean;
invoiceNumber: string;
status: ProformaStatus;
series: string;
invoiceDate: string;
operationDate: string;
reference: string;
description: string;
notes: string;
languageCode: string;
currencyCode: string;
customerId: string;
recipient: ProformaRecipient;
taxes: ProformaTaxSummary[];
paymentMethod?: string;
subtotalAmount: number;
itemsDiscountAmount: number;
globalDiscountPercentage: number;
discountAmount: number;
taxableAmount: number;
ivaAmount: number;
recAmount: number;
retentionAmount: number;
taxesAmount: number;
totalAmount: number;
items: ProformaItem[];
}

View File

@ -1,3 +0,0 @@
export * from "./use-change-proforma-status-mutation";
export * from "./use-delete-proforma-mutation";
export * from "./use-list-proformas-query";

View File

@ -1,47 +0,0 @@
import { useDataSource } from "@erp/core/hooks";
import {
type DefaultError,
type QueryKey,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { type Proforma, getProformaById } from "../../api";
export const PROFORMA_QUERY_KEY = (proformaId?: string): QueryKey => [
"proformas:detail",
{
proformaId,
},
];
type ProformaQueryOptions = {
enabled?: boolean;
};
export const useProformaGetQuery = (
proformaId?: string,
options?: ProformaQueryOptions
): UseQueryResult<Proforma, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? Boolean(proformaId);
return useQuery<Proforma, DefaultError>({
queryKey: PROFORMA_QUERY_KEY(proformaId),
queryFn: async ({ signal }) => getProformaById(dataSource, signal, proformaId),
enabled,
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
});
};
/*export function invalidateProformaDetailCache(qc: QueryClient, id: string) {
return qc.invalidateQueries({
queryKey: getProformaQueryKey(id ?? "unknown"),
exact: Boolean(id),
});
}
export function setProformaDetailCache(qc: QueryClient, id: string, data: unknown) {
qc.setQueryData(getProformaQueryKey(id), data);
}
*/

View File

@ -1,58 +0,0 @@
import { useDataSource } from "@erp/core/hooks";
import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UpdateProformaByIdRequestSchema } from "../../../../common";
import type { ProformaFormData } from "../../update/types";
export const PROFORMA_UPDATE_KEY = ["proformas", "update"] as const;
type UpdateProformaContext = {};
type UpdateProformaPayload = {
id: string;
data: Partial<ProformaFormData>;
};
export const useProformaUpdateMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const schema = UpdateProformaByIdRequestSchema;
return useMutation<Proforma, DefaultError, UpdateProformaPayload, UpdateProformaContext>({
mutationKey: PROFORMA_UPDATE_KEY,
mutationFn: async (payload) => {
const { id: proformaId, data } = payload;
if (!proformaId) {
throw new Error("proformaId is required");
}
const result = schema.safeParse(data);
if (!result.success) {
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
}
const updated = await dataSource.updateOne("proformas", proformaId, data);
return updated as Proforma;
},
onSuccess: (updated: Proforma, variables) => {
const { id: proformaId } = updated;
// Invalida el listado para refrescar desde servidor
//invalidateProformaListCache(queryClient);
// Actualiza detalle
//setProformaDetailCache(queryClient, proformaId, updated);
// Actualiza todas las páginas donde aparezca
//upsertProformaIntoListCaches(queryClient, { ...updated });
},
onSettled: () => {
// Refresca todos los listados
//invalidateProformaListCache(queryClient);
},
});
};

View File

@ -1,4 +0,0 @@
export * from "./api";
export * from "./entities";
export * from "./hooks";
export * from "./ui";

View File

@ -0,0 +1,3 @@
export * from "./use-change-proforma-status-mutation";
export * from "./use-delete-proforma-mutation";
export * from "./use-list-proformas-query";

View File

@ -1,7 +1,7 @@
import { useDataSource } from "@erp/core/hooks";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { changeProformaStatusById } from "../api/change-proforma-status-by-id.api";
import { changeProformaStatusById } from "../../shared/api/change-proforma-status-by-id.api";
import type { PROFORMA_STATUS } from "../entities";
import { LIST_PROFORMAS_QUERY_KEY_PREFIX } from "./use-list-proformas-query";

View File

@ -0,0 +1,47 @@
import { useDataSource } from "@erp/core/hooks";
import {
type DefaultError,
type QueryKey,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { type Proforma, getProformaById } from "../../api";
export const PROFORMA_QUERY_KEY = (proformaId?: string): QueryKey => [
"proformas:detail",
{
proformaId,
},
];
type ProformaQueryOptions = {
enabled?: boolean;
};
export const useProformaGetQuery = (
proformaId?: string,
options?: ProformaQueryOptions
): UseQueryResult<Proforma, DefaultError> => {
const dataSource = useDataSource();
const enabled = options?.enabled ?? Boolean(proformaId);
return useQuery<Proforma, DefaultError>({
queryKey: PROFORMA_QUERY_KEY(proformaId),
queryFn: async ({ signal }) => getProformaById(dataSource, signal, proformaId),
enabled,
placeholderData: (previousData, _previousQuery) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
});
};
/*export function invalidateProformaDetailCache(qc: QueryClient, id: string) {
return qc.invalidateQueries({
queryKey: getProformaQueryKey(id ?? "unknown"),
exact: Boolean(id),
});
}
export function setProformaDetailCache(qc: QueryClient, id: string, data: unknown) {
qc.setQueryData(getProformaQueryKey(id), data);
}
*/

View File

@ -0,0 +1,58 @@
import { useDataSource } from "@erp/core/hooks";
import { ValidationErrorCollection } from "@repo/rdx-ddd";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UpdateProformaByIdRequestSchema } from "../../../../common";
import type { ProformaFormData } from "../../update/types";
export const PROFORMA_UPDATE_KEY = ["proformas", "update"] as const;
type UpdateProformaContext = {};
type UpdateProformaPayload = {
id: string;
data: Partial<ProformaFormData>;
};
export const useProformaUpdateMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
const schema = UpdateProformaByIdRequestSchema;
return useMutation<Proforma, DefaultError, UpdateProformaPayload, UpdateProformaContext>({
mutationKey: PROFORMA_UPDATE_KEY,
mutationFn: async (payload) => {
const { id: proformaId, data } = payload;
if (!proformaId) {
throw new Error("proformaId is required");
}
const result = schema.safeParse(data);
if (!result.success) {
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
}
const updated = await dataSource.updateOne("proformas", proformaId, data);
return updated as Proforma;
},
onSuccess: (updated: Proforma, variables) => {
const { id: proformaId } = updated;
// Invalida el listado para refrescar desde servidor
//invalidateProformaListCache(queryClient);
// Actualiza detalle
//setProformaDetailCache(queryClient, proformaId, updated);
// Actualiza todas las páginas donde aparezca
//upsertProformaIntoListCaches(queryClient, { ...updated });
},
onSettled: () => {
// Refresca todos los listados
//invalidateProformaListCache(queryClient);
},
});
};

View File

@ -0,0 +1,4 @@
export * from "./api";
export * from "./entities";
export * from "./hooks";
export * from "./ui";

View File

@ -0,0 +1,4 @@
export * from "./proforma-update-form.entity";
export * from "./proforma-update-form.schema";
export * from "./proforma-update-form-defaults";
export * from "./proforma-update-patch.entity";

View File

@ -0,0 +1,9 @@
export interface ProformaItemUpdateForm {
id: string;
position: string;
description: string;
quantity: number;
unitAmount: number;
discountPercentage: number;
taxes: string;
}

View File

@ -0,0 +1,33 @@
import type { CustomerUpdateForm } from "./proforma-update-form.entity";
export const defaultCustomerUpdateForm: CustomerUpdateForm = {
reference: "",
isCompany: true,
name: "",
tradeName: "",
tin: "",
defaultTaxes: [],
street: "",
street2: "",
city: "",
province: "",
postalCode: "",
country: "es",
primaryEmail: "",
secondaryEmail: "",
primaryPhone: "",
secondaryPhone: "",
primaryMobile: "",
secondaryMobile: "",
fax: "",
website: "",
legalRecord: "",
languageCode: "es",
currencyCode: "EUR",
};

View File

@ -0,0 +1,37 @@
/**
* ProformaUpdateForm representa el shape de datos del formulario de actualización de proformas.
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
*
* Es específico de la UI y no tiene por qué coincidir
* con el shape del dominio ni con el de la API.
*
* Debe cumplir las siguientes reglas:
* - nombres en camelCase
* - tipos orientados a UI/form
* - sin campos de solo lectura que no se editen
* - sin shape DTO
* - sin detalles impuestos por el widget
*/
import type { ProformaItemForm } from "../../shared/entities";
export interface ProformaUpdateForm {
series: string;
invoiceDate: string;
operationDate: string;
customerId: string;
reference: string;
notes: string;
languageCode: string;
currencyCode: string;
globalDiscountPercentage: number;
paymentMethod: string;
items: ProformaItemForm[];
}

View File

@ -0,0 +1,95 @@
import { z } from "zod/v4";
/**
* Este esquema es para validar los datos del formulario de actualización de proformas.
* No tiene por qué coincidir con el shape de la entidad ni con el de la API.
* Solo define los campos que se muestran en el formulario y sus validaciones.
*
* Reglas:
* - no meter transformaciones silenciosas raras en el esquema (ej: .toUpperCase())
* - nombres en camelCase
* - tipos orientados a UI/form
* - sin campos de solo lectura que no se editen
* - sin shape DTO
* - sin detalles impuestos por el widget
*/
export const ProformaItemFormSchema = z.object({
description: z.string().max(2000).optional().default(""),
quantity: z.any(), //NumericStringSchema.optional(),
unit_amount: z.any(), //NumericStringSchema.optional(),
subtotal_amount: z.any(), //z.number(),
discount_percentage: z.any(), //NumericStringSchema.optional(),
discount_amount: z.number(),
taxable_amount: z.number(),
tax_codes: z.array(z.string()).default([]),
taxes_amount: z.number(),
total_amount: z.number(),
});
export const ProformaFormSchema = z.object({
invoice_number: z.string().optional(),
series: z.string().optional(),
invoice_date: z.string().optional(),
operation_date: z.string().optional(),
customer_id: z.string().optional(),
recipient: z
.object({
id: z.string().optional(),
name: z.string().optional(),
tin: z.string().optional(),
street: z.string().optional(),
street2: z.string().optional(),
city: z.string().optional(),
province: z.string().optional(),
postal_code: z.string().optional(),
country: z.string().optional(),
})
.optional(),
reference: z.string().optional(),
description: z.string().optional(),
notes: z.string().optional(),
language_code: z
.string({
error: "El idioma es obligatorio",
})
.min(1, "Debe indicar un idioma")
.toUpperCase() // asegura mayúsculas
.default("es"),
currency_code: z
.string({
error: "La moneda es obligatoria",
})
.min(1, "La moneda no puede estar vacía")
.toUpperCase() // asegura mayúsculas
.default("EUR"),
taxes: z
.array(
z.object({
tax_code: z.string(),
tax_label: z.string(),
taxable_amount: z.number(),
taxes_amount: z.number(),
})
)
.optional(),
items: z.array(ProformaItemFormSchema).optional(),
subtotal_amount: z.number(),
items_discount_amount: z.number(),
discount_percentage: z.number(),
discount_amount: z.number(),
taxable_amount: z.number(),
taxes_amount: z.number(),
total_amount: z.number(),
});

View File

@ -0,0 +1,17 @@
import type { ProformaUpdateForm } from "./proforma-update-form.entity";
/**
* ProformaUpdatePatch representa los cambios que se van a aplicar a una proforma.
* Se representa con las mismas propiedades que ProformaUpdateForm,
* pero todas ellas son opcionales.
*
* A la API solo hay que enviar los campos que han cambiado.
*
* Reglas:
* - debe ser un Partial de ProformaUpdateForm
* - no debe tener campos adicionales ni transformaciones
* - debe ser un shape orientado a la API, no a la UI ni al dominio
* - sin shape DTO, solo tipos simples y directos
*/
export type ProformaUpdatePatch = Partial<ProformaUpdateForm>;