This commit is contained in:
David Arranz 2026-03-19 19:10:52 +01:00
parent 520373cbd6
commit 700445499b
16 changed files with 148 additions and 57 deletions

View File

@ -13,6 +13,7 @@ import type {
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases"; import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
import { CreateProformaUseCase } from "../use-cases/create-proforma"; import { CreateProformaUseCase } from "../use-cases/create-proforma";
import { IssueProformaUseCase } from "../use-cases/issue-proforma.use-case";
export function buildGetProformaByIdUseCase(deps: { export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder; finder: IProformaFinder;
@ -64,6 +65,10 @@ export function buildCreateProformaUseCase(deps: {
}); });
} }
export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) {
return new IssueProformaUseCase(deps.finder);
}
/*export function buildUpdateProformaUseCase(deps: { /*export function buildUpdateProformaUseCase(deps: {
finder: IProformaFinder; finder: IProformaFinder;
fullSnapshotBuilder: IProformaFullSnapshotBuilder; fullSnapshotBuilder: IProformaFullSnapshotBuilder;
@ -75,9 +80,6 @@ export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
return new DeleteProformaUseCase(deps.finder); return new DeleteProformaUseCase(deps.finder);
} }
export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) {
return new IssueProformaUseCase(deps.finder);
}
export function buildChangeStatusProformaUseCase(deps: { export function buildChangeStatusProformaUseCase(deps: {
finder: IProformaFinder; finder: IProformaFinder;

View File

@ -2,7 +2,7 @@
export * from "./create-proforma"; export * from "./create-proforma";
//export * from "./delete-proforma.use-case"; //export * from "./delete-proforma.use-case";
export * from "./get-proforma-by-id.use-case"; export * from "./get-proforma-by-id.use-case";
//export * from "./issue-proforma.use-case"; export * from "./issue-proforma.use-case";
export * from "./list-proformas.use-case"; export * from "./list-proformas.use-case";
export * from "./report-proforma.use-case"; export * from "./report-proforma.use-case";
//export * from "./update-proforma"; //export * from "./update-proforma";

View File

@ -12,6 +12,7 @@ import {
import { import {
CreateProformaRequestSchema, CreateProformaRequestSchema,
GetProformaByIdRequestSchema, GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema,
ListProformasRequestSchema, ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema, ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema, ReportProformaByIdQueryRequestSchema,
@ -129,7 +130,7 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
const controller = new ChangeStatusProformaController(useCase); const controller = new ChangeStatusProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );*/
router.put( router.put(
"/:proforma_id/issue", "/:proforma_id/issue",
@ -142,7 +143,7 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
const controller = new IssuedProformaController(useCase); const controller = new IssuedProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
);*/ );
app.use(`${config.server.apiBasePath}/proformas`, router); app.use(`${config.server.apiBasePath}/proformas`, router);
}; };

View File

@ -17,7 +17,7 @@ export class SequelizeProformaNumberGenerator implements IProformaNumberGenerato
): Promise<Result<InvoiceNumber, Error>> { ): Promise<Result<InvoiceNumber, Error>> {
const where: WhereOptions = { const where: WhereOptions = {
company_id: companyId.toString(), company_id: companyId.toString(),
is_proforma: false, is_proforma: true,
}; };
series.match( series.match(
@ -41,12 +41,12 @@ export class SequelizeProformaNumberGenerator implements IProformaNumberGenerato
lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta
}); });
let nextValue = "001"; // valor inicial por defecto let nextValue = "0001"; // valor inicial por defecto
if (lastInvoice) { if (lastInvoice) {
const current = Number(lastInvoice.invoice_number); const current = Number(lastInvoice.invoice_number);
const next = Number.isFinite(current) && current > 0 ? current + 1 : 1; const next = Number.isFinite(current) && current > 0 ? current + 1 : 1;
nextValue = String(next).padStart(3, "0"); nextValue = String(next).padStart(4, "0");
} }
const numberResult = InvoiceNumber.create(nextValue); const numberResult = InvoiceNumber.create(nextValue);

View File

@ -226,6 +226,8 @@ export function useProformasGridColumns(
const availableTransitions = const availableTransitions =
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? []; PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
console.log(availableTransitions);
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{!isIssued && actionHandlers.onEditClick && ( {!isIssued && actionHandlers.onEditClick && (

View File

@ -36,10 +36,10 @@ export const ProformaListPage = () => {
} = useProformaListPageController(); } = useProformaListPageController();
const columns = useProformasGridColumns({ const columns = useProformasGridColumns({
//onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`), onEditClick: (proforma) => navigate(`/proformas/${proforma.id}/edit`),
//onIssueClick: handleIssueProforma, onIssueClick: handleIssueProforma,
//onDeleteClick: handleDeleteProforma, onDeleteClick: handleDeleteProforma,
//onChangeStatusClick: handleChangeStatusProforma, onChangeStatusClick: handleChangeStatusProforma,
}); });
if (listCtrl.isError || !listCtrl.data) { if (listCtrl.isError || !listCtrl.data) {

View File

@ -80,8 +80,8 @@
"description": "The street address of the customer" "description": "The street address of the customer"
}, },
"street2": { "street2": {
"label": "Street", "label": "Street 2",
"placeholder": "Enter street", "placeholder": "Enter street 2",
"description": "The street address of the customer" "description": "The street address of the customer"
}, },
"city": { "city": {
@ -114,7 +114,6 @@
"placeholder": "Enter secondary email", "placeholder": "Enter secondary email",
"description": "The secondary email address of the customer" "description": "The secondary email address of the customer"
}, },
"phone_primary": { "phone_primary": {
"label": "Primary phone", "label": "Primary phone",
"placeholder": "Enter primary phone number", "placeholder": "Enter primary phone number",
@ -125,7 +124,6 @@
"placeholder": "Enter secondary phone number ", "placeholder": "Enter secondary phone number ",
"description": "The secondary phone number of the customer" "description": "The secondary phone number of the customer"
}, },
"mobile_primary": { "mobile_primary": {
"label": "Primary mobile", "label": "Primary mobile",
"placeholder": "Enter primary mobile number", "placeholder": "Enter primary mobile number",
@ -196,4 +194,4 @@
"create_label": "Create new item" "create_label": "Create new item"
} }
} }
} }

View File

@ -82,7 +82,7 @@
"description": "La dirección de la calle del cliente" "description": "La dirección de la calle del cliente"
}, },
"street2": { "street2": {
"label": "Calle", "label": "Calle (ampliación)",
"placeholder": "Ingrese la calle", "placeholder": "Ingrese la calle",
"description": "La dirección de la calle del cliente" "description": "La dirección de la calle del cliente"
}, },
@ -106,67 +106,56 @@
"placeholder": "Seleccione el país", "placeholder": "Seleccione el país",
"description": "El país del cliente" "description": "El país del cliente"
}, },
"email_primary": { "email_primary": {
"label": "Email principal", "label": "Email principal",
"placeholder": "Ingrese el correo electrónico", "placeholder": "Ingrese el correo electrónico",
"description": "La dirección de correo electrónico principal del cliente" "description": "La dirección de correo electrónico principal del cliente"
}, },
"email_secondary": { "email_secondary": {
"label": "Email secundario", "label": "Email secundario",
"placeholder": "Ingrese el correo electrónico", "placeholder": "Ingrese el correo electrónico",
"description": "La dirección de correo electrónico secundario del clientºe" "description": "La dirección de correo electrónico secundario del clientºe"
}, },
"phone_primary": { "phone_primary": {
"label": "Teléfono", "label": "Teléfono",
"placeholder": "Ingrese el número de teléfono", "placeholder": "Ingrese el número de teléfono",
"description": "El número de teléfono del cliente" "description": "El número de teléfono del cliente"
}, },
"phone_secondary": { "phone_secondary": {
"label": "Teléfono secundario", "label": "Teléfono secundario",
"placeholder": "Ingrese el número de teléfono secundario", "placeholder": "Ingrese el número de teléfono secundario",
"description": "El número de teléfono secundario del cliente" "description": "El número de teléfono secundario del cliente"
}, },
"mobile_primary": { "mobile_primary": {
"label": "Teléfono", "label": "Teléfono",
"placeholder": "Ingrese el número de teléfono", "placeholder": "Ingrese el número de teléfono",
"description": "El número de teléfono del cliente" "description": "El número de teléfono del cliente"
}, },
"mobile_secondary": { "mobile_secondary": {
"label": "Teléfono secundario", "label": "Teléfono secundario",
"placeholder": "Ingrese el número de teléfono secundario", "placeholder": "Ingrese el número de teléfono secundario",
"description": "El número de teléfono secundario del cliente" "description": "El número de teléfono secundario del cliente"
}, },
"fax": { "fax": {
"label": "Fax", "label": "Fax",
"placeholder": "Ingrese el número de fax", "placeholder": "Ingrese el número de fax",
"description": "El número de fax del cliente" "description": "El número de fax del cliente"
}, },
"website": { "website": {
"label": "Sitio web", "label": "Sitio web",
"placeholder": "Ingrese la URL del sitio web", "placeholder": "Ingrese la URL del sitio web",
"description": "El sitio web del cliente" "description": "El sitio web del cliente"
}, },
"default_taxes": { "default_taxes": {
"label": "Impuesto por defecto", "label": "Impuesto por defecto",
"placeholder": "Seleccione el impuesto por defecto", "placeholder": "Seleccione el impuesto por defecto",
"description": "La tasa de impuesto por defecto para el cliente" "description": "La tasa de impuesto por defecto para el cliente"
}, },
"language_code": { "language_code": {
"label": "Idioma", "label": "Idioma",
"placeholder": "Seleccione el idioma", "placeholder": "Seleccione el idioma",
"description": "El idioma preferido del cliente" "description": "El idioma preferido del cliente"
}, },
"currency_code": { "currency_code": {
"label": "Moneda", "label": "Moneda",
"placeholder": "Seleccione la moneda", "placeholder": "Seleccione la moneda",
@ -207,4 +196,4 @@
"create_label": "Crear nuevo elemento" "create_label": "Crear nuevo elemento"
} }
} }
} }

View File

@ -55,7 +55,8 @@ export const CustomerBasicInfoFields = ({
required required
/> />
</Field> </Field>
<Field className="lg:col-span-2">
<Field className="lg:col-span-1 lg:col-start-1">
<FormField <FormField
control={control} control={control}
name="is_company" name="is_company"
@ -94,6 +95,17 @@ export const CustomerBasicInfoFields = ({
/> />
</Field> </Field>
<Field className="lg:col-span-1 " ref={focusRef}>
<TextField
control={control}
description={t("form_fields.tin.description")}
label={t("form_fields.tin.label")}
name="tin"
placeholder={t("form_fields.tin.placeholder")}
required
/>
</Field>
<TextField <TextField
className="lg:col-span-full" className="lg:col-span-full"
control={control} control={control}
@ -118,10 +130,7 @@ export const CustomerBasicInfoFields = ({
name="default_taxes" name="default_taxes"
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<Field className={"gap-1"} data-invalid={fieldState.invalid}> <Field className={"gap-1"} data-invalid={fieldState.invalid}>
<FieldLabel <FieldLabel htmlFor={"default_taxes"}>
className="text-xs text-muted-foreground text-nowrap"
htmlFor={"default_taxes"}
>
{t("form_fields.default_taxes.label")} {t("form_fields.default_taxes.label")}
</FieldLabel> </FieldLabel>
<CustomerTaxesMultiSelect <CustomerTaxesMultiSelect

View File

@ -1,3 +1,4 @@
import { FormDebug } from "@erp/core/components";
import { cn } from "@repo/shadcn-ui/lib/utils"; import { cn } from "@repo/shadcn-ui/lib/utils";
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields"; import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
@ -15,6 +16,7 @@ type CustomerFormProps = {
export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => { export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => {
return ( return (
<form id={formId} noValidate onSubmit={onSubmit}> <form id={formId} noValidate onSubmit={onSubmit}>
<FormDebug enabled />
<section className={cn("space-y-12 p-6", className)}> <section className={cn("space-y-12 p-6", className)}>
<CustomerBasicInfoFields focusRef={focusRef} /> <CustomerBasicInfoFields focusRef={focusRef} />
<CustomerAddressFields /> <CustomerAddressFields />

View File

@ -2,6 +2,7 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components";
import { import {
Avatar, Avatar,
AvatarFallback, AvatarFallback,
Badge,
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -11,15 +12,12 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { EyeIcon, MoreHorizontalIcon } from "lucide-react"; import { Building2Icon, EyeIcon, MoreHorizontalIcon, UserIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import { CustomerStatusBadge } from "../../../../ui";
import type { CustomerSummaryData } from "../../../types"; import type { CustomerSummaryData } from "../../../types";
import { AddressCell, ContactCell, Initials } from "../../components"; import { AddressCell, ContactCell, Initials } from "../../components";
import { KindBadge } from "../../components/kind-badge";
import { Soft } from "../../components/soft";
type GridActionHandlers = { type GridActionHandlers = {
onEditClick?: (customer: CustomerSummaryData) => void; onEditClick?: (customer: CustomerSummaryData) => void;
@ -81,8 +79,48 @@ export function useCustomersGridColumns(
const customer = row.original; const customer = row.original;
const isCompany = String(customer.is_company).toLowerCase() === "true"; const isCompany = String(customer.is_company).toLowerCase() === "true";
return ( return (
<div className="flex items-start gap-3">
<Avatar className="size-10 border-2 border-background shadow-sm">
<AvatarFallback
className={
customer.status === "active"
? "bg-blue-100 text-blue-700"
: "bg-muted text-muted-foreground"
}
>
<Initials name={customer.name} />
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{customer.name}</span>
{customer.trade_name && (
<span className="text-muted-foreground">({customer.trade_name})</span>
)}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-mono">{customer.tin}</span>
<Badge className="gap-1 px-1.5 py-0 text-xs font-normal" variant="outline">
{customer.is_company ? (
<>
<Building2Icon className="size-3" />
Empresa
</>
) : (
<>
<UserIcon className="size-3" />
Particular
</>
)}
</Badge>
</div>
</div>
</div>
);
/*return (
<div className="flex items-start gap-1 my-1.5"> <div className="flex items-start gap-1 my-1.5">
<Avatar className="size-10 hidden"> <Avatar className="size-10">
<AvatarFallback aria-label={customer.name}> <AvatarFallback aria-label={customer.name}>
<Initials name={customer.name} /> <Initials name={customer.name} />
</AvatarFallback> </AvatarFallback>
@ -99,7 +137,7 @@ export function useCustomersGridColumns(
</div> </div>
</div> </div>
</div> </div>
); );*/
}, },
}, },

View File

@ -1,15 +1,30 @@
import type { CustomerSummaryData } from "../../types"; import { MapPinIcon } from "lucide-react";
import { Soft } from "./soft"; import type { CustomerSummaryData } from "../../types";
export const AddressCell = ({ customer }: { customer: CustomerSummaryData }) => { export const AddressCell = ({ customer }: { customer: CustomerSummaryData }) => {
const line1 = [customer.street, customer.street2].filter(Boolean).join(", "); const line1 = [customer.street, customer.street2].filter(Boolean).join(", ");
const line2 = [customer.postal_code, customer.city].filter(Boolean).join(" "); const line2 = [customer.postal_code, customer.city].filter(Boolean).join(" ");
const line3 = [customer.province, customer.country].filter(Boolean).join(", "); const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
return ( return (
<address className="not-italic flex items-start gap-2">
<MapPinIcon className="mt-0.5 size-3.5" />
<div className="text-sm text-muted-foreground">
<p>{line1}</p>
<p>
{line2} · {line3}
</p>
</div>
</address>
);
};
/*
<address className="not-italic grid gap-1 text-foreground text-sm"> <address className="not-italic grid gap-1 text-foreground text-sm">
<div>{line1 || <Soft>-</Soft>}</div> <div>{line1 || <Soft>-</Soft>}</div>
<div>{[line2, line3].filter(Boolean).join(" • ")}</div> <div>{[line2, line3].filter(Boolean).join(" • ")}</div>
</address> </address>
);
};
*/

View File

@ -2,9 +2,42 @@ import { MailIcon, PhoneIcon } from "lucide-react";
import type { CustomerSummaryData } from "../../types"; import type { CustomerSummaryData } from "../../types";
import { Soft } from "./soft";
export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) => ( export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) => (
<div className="flex flex-col gap-1.5">
<a
className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
href={`mailto:${customer.email_primary}`}
>
<MailIcon className="size-3.5" />
{customer.email_primary}
</a>
{customer.email_secondary && (
<a
className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
href={`mailto:${customer.email_secondary}`}
>
<MailIcon className="size-3.5" />
{customer.email_secondary}
</a>
)}
{customer.phone_primary ? (
<a
className="flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
href={`tel:${customer.phone_primary}`}
>
<PhoneIcon className="size-3.5" />
{customer.phone_primary}
</a>
) : (
<span className="flex items-center gap-2 text-sm text-muted-foreground/50">
<PhoneIcon className="size-3.5" />-
</span>
)}
</div>
);
/*
<div className="grid gap-1 text-foreground text-sm my-1.5"> <div className="grid gap-1 text-foreground text-sm my-1.5">
{customer.email_primary && ( {customer.email_primary && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -31,4 +64,6 @@ export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) =>
</div> </div>
{false} {false}
</div> </div>
);
*/

View File

@ -40,7 +40,7 @@
"typescript": "^5.6.0" "typescript": "^5.6.0"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
"add": "^2.0.6", "add": "^2.0.6",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@ -1,6 +1,6 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "@fontsource-variable/geist"; @import "@fontsource-variable/inter";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@ -98,7 +98,7 @@
@layer utilities { @layer utilities {
body { body {
font-family: "Geist Variable", ui-sans-serif, sans-serif, system-ui; font-family: "Inter", ui-sans-serif, sans-serif, system-ui;
} }
} }
@ -111,7 +111,7 @@
**/ **/
:root { :root {
--font-sans: "Geist Variable", ui-sans-serif, sans-serif, system-ui; --font-sans: "Inter", ui-sans-serif, sans-serif, system-ui;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);

View File

@ -912,7 +912,7 @@ importers:
packages/shadcn-ui: packages/shadcn-ui:
dependencies: dependencies:
'@fontsource-variable/geist': '@fontsource-variable/inter':
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.2.8 version: 5.2.8
'@hookform/resolvers': '@hookform/resolvers':
@ -1597,8 +1597,8 @@ packages:
'@floating-ui/utils@0.2.10': '@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@fontsource-variable/geist@5.2.8': '@fontsource-variable/inter@5.2.8':
resolution: {integrity: sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==} resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
'@hapi/hoek@9.3.0': '@hapi/hoek@9.3.0':
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
@ -7635,7 +7635,7 @@ snapshots:
'@floating-ui/utils@0.2.10': {} '@floating-ui/utils@0.2.10': {}
'@fontsource-variable/geist@5.2.8': {} '@fontsource-variable/inter@5.2.8': {}
'@hapi/hoek@9.3.0': {} '@hapi/hoek@9.3.0': {}