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 { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
import { CreateProformaUseCase } from "../use-cases/create-proforma";
import { IssueProformaUseCase } from "../use-cases/issue-proforma.use-case";
export function buildGetProformaByIdUseCase(deps: {
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: {
finder: IProformaFinder;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
@ -75,9 +80,6 @@ export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
return new DeleteProformaUseCase(deps.finder);
}
export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) {
return new IssueProformaUseCase(deps.finder);
}
export function buildChangeStatusProformaUseCase(deps: {
finder: IProformaFinder;

View File

@ -2,7 +2,7 @@
export * from "./create-proforma";
//export * from "./delete-proforma.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 "./report-proforma.use-case";
//export * from "./update-proforma";

View File

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

View File

@ -17,7 +17,7 @@ export class SequelizeProformaNumberGenerator implements IProformaNumberGenerato
): Promise<Result<InvoiceNumber, Error>> {
const where: WhereOptions = {
company_id: companyId.toString(),
is_proforma: false,
is_proforma: true,
};
series.match(
@ -41,12 +41,12 @@ export class SequelizeProformaNumberGenerator implements IProformaNumberGenerato
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) {
const current = Number(lastInvoice.invoice_number);
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components";
import {
Avatar,
AvatarFallback,
Badge,
Button,
DropdownMenu,
DropdownMenuContent,
@ -11,15 +12,12 @@ import {
DropdownMenuTrigger,
} from "@repo/shadcn-ui/components";
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 { useTranslation } from "../../../../i18n";
import { CustomerStatusBadge } from "../../../../ui";
import type { CustomerSummaryData } from "../../../types";
import { AddressCell, ContactCell, Initials } from "../../components";
import { KindBadge } from "../../components/kind-badge";
import { Soft } from "../../components/soft";
type GridActionHandlers = {
onEditClick?: (customer: CustomerSummaryData) => void;
@ -81,8 +79,48 @@ export function useCustomersGridColumns(
const customer = row.original;
const isCompany = String(customer.is_company).toLowerCase() === "true";
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">
<Avatar className="size-10 hidden">
<Avatar className="size-10">
<AvatarFallback aria-label={customer.name}>
<Initials name={customer.name} />
</AvatarFallback>
@ -99,7 +137,7 @@ export function useCustomersGridColumns(
</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 }) => {
const line1 = [customer.street, customer.street2].filter(Boolean).join(", ");
const line2 = [customer.postal_code, customer.city].filter(Boolean).join(" ");
const line3 = [customer.province, customer.country].filter(Boolean).join(", ");
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">
<div>{line1 || <Soft>-</Soft>}</div>
<div>{[line2, line3].filter(Boolean).join(" • ")}</div>
</address>
);
};
*/

View File

@ -2,9 +2,42 @@ import { MailIcon, PhoneIcon } from "lucide-react";
import type { CustomerSummaryData } from "../../types";
import { Soft } from "./soft";
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">
{customer.email_primary && (
<div className="flex items-center gap-2">
@ -31,4 +64,6 @@ export const ContactCell = ({ customer }: { customer: CustomerSummaryData }) =>
</div>
{false}
</div>
);
*/

View File

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

View File

@ -1,6 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "@fontsource-variable/geist";
@import "@fontsource-variable/inter";
@custom-variant dark (&:is(.dark *));
@ -98,7 +98,7 @@
@layer utilities {
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 {
--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);
--foreground: oklch(0.145 0 0);

View File

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