This commit is contained in:
David Arranz 2026-08-03 17:42:23 +02:00
parent 10a5c25fc1
commit ddf68c32c1
36 changed files with 95 additions and 170 deletions

3
.gitignore vendored
View File

@ -10,6 +10,3 @@ dist-ssr
server/dist
public/dist
out
apps/**/false/*
false

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges-server",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"scripts": {
"build": "tsup src/index.ts --config tsup.config.ts",

View File

@ -1,7 +1,7 @@
{
"name": "@erp/factuges-web",
"private": true,
"version": "0.10.1",
"version": "0.10.2",
"type": "module",
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",

View File

@ -1,6 +1,6 @@
{
"name": "@erp/auth",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/catalogs",
"description": "Catalogs module",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/companies",
"description": "Companies module",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/core",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -5,7 +5,7 @@ import { ArrowLeftIcon } from "lucide-react";
import type { ReactNode } from "react";
export interface PageFormHeaderProps {
title: string;
title: string | ReactNode;
backLabel: string;
onBack?: () => void;

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customer-invoices",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,5 +1,5 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceNumber, InvoiceSerie } from "../../../domain";

View File

@ -4,13 +4,13 @@ import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api"
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IInvoiceSeriesNumberAssigner } from "../../../application/invoice-series";
import {
type IIssuedInvoiceCreatorParams,
type IIssuedInvoicePublicServices,
type IIssuedInvoiceServicesContext,
buildIssuedInvoiceCreator,
} from "../../../application/issued-invoices";
import type { IInvoiceSeriesNumberAssigner } from "../../../application/invoice-series";
import { InvoiceNumber, InvoiceSerie } from "../../../domain";
import { buildV2IssuedInvoicePersistence } from "./issued-invoice-persistence.di";
@ -29,11 +29,13 @@ export function buildIssuedInvoicePublicServices(
// Fase 1C: la emision ya persiste en `issued_invoices` y relaciona Verifactu por `issued_invoice_id`.
const persistence = buildV2IssuedInvoicePersistence({ catalogs, database });
const { repository } = persistence;
const invoiceSeriesNumberAssigner: IInvoiceSeriesNumberAssigner = {
assignNextNumber: async ({ companyId, invoiceSeriesCode, transaction }) => {
const requestedSeriesCode = invoiceSeriesCode
.map((series) => series.toPrimitive())
.unwrapOr(null);
const requestedSeriesCode = invoiceSeriesCode.match(
(serie) => serie.toString(),
() => null
);
const assignResult = await documentSeriesServices.assignNextNumber({
companyId: companyId.toString(),
@ -56,12 +58,12 @@ export function buildIssuedInvoicePublicServices(
return Result.fail(assignedSeriesResult.error);
}
return Result.ok({
invoiceNumber: invoiceNumberResult.data,
formattedNumber: assignResult.data.reference,
invoiceSeriesId: assignResult.data.series_id,
invoiceSeriesCode: assignedSeriesResult.data,
});
return Result.ok({
invoiceNumber: invoiceNumberResult.data,
formattedNumber: assignResult.data.reference,
invoiceSeriesId: assignResult.data.series_id,
invoiceSeriesCode: assignedSeriesResult.data,
});
},
};

View File

@ -19,7 +19,7 @@ export const ProformaDetailHeaderMeta = ({ proforma }: ProformaDetailHeaderMetaP
);
return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted-foreground">
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-base text-foreground font-semibold">
{items.map((item, index) => (
<div className="flex items-center gap-3" key={`${item}-${index}`}>
{index > 0 ? <span className="hidden text-border sm:inline">|</span> : null}

View File

@ -68,7 +68,7 @@ export const ProformaDetailPage = () => {
}
backLabel="Volver al listado"
onBack={ctrl.navigateBack}
title={`Proforma ${ctrl.proforma.proformaReference}`}
title={<>{`Proforma ${ctrl.proforma.proformaReference}`}</>}
>
<div className="border-b bg-background px-4 py-4 sm:px-6">
<div className="flex flex-wrap items-center gap-3">

View File

@ -20,8 +20,8 @@ import {
import type { ColumnDef } from "@tanstack/react-table";
import {
CopyIcon,
EyeIcon,
ExternalLinkIcon,
EyeIcon,
FileDownIcon,
FileTextIcon,
FolderArchiveIcon,
@ -80,9 +80,7 @@ const stopRowPropagation = (event: React.MouseEvent | React.KeyboardEvent) => {
event.stopPropagation();
};
const stopRowPropagationAndPreventDefault = (
event: React.MouseEvent | React.KeyboardEvent
) => {
const stopRowPropagationAndPreventDefault = (event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
event.stopPropagation();
};
@ -367,12 +365,12 @@ export function useProformasGridColumns(
key: "edit",
label: t("pages.proformas.list.actions.edit"),
icon: PencilIcon,
disabled: !actionHandlers.onEditClick || !isNormalScope || isIssued,
disabledReason: !isNormalScope
? getScopeReason()
: isIssued
disabled: !(actionHandlers.onEditClick && isNormalScope) || isIssued,
disabledReason: isNormalScope
? isIssued
? t("pages.proformas.list.actions.disabled_reasons.edit_issued")
: undefined,
: undefined
: getScopeReason(),
onClick: handleEdit,
},
];
@ -382,11 +380,11 @@ export function useProformasGridColumns(
key: "linked-invoice",
label: t("pages.proformas.list.actions.view_linked_invoice"),
icon: ExternalLinkIcon,
disabled: !isIssued || !proforma.linkedInvoiceId,
disabled: !(isIssued && proforma.linkedInvoiceId),
disabledReason:
!isIssued || !proforma.linkedInvoiceId
? t("pages.proformas.list.actions.disabled_reasons.linked_invoice_unavailable")
: undefined,
isIssued && proforma.linkedInvoiceId
? undefined
: t("pages.proformas.list.actions.disabled_reasons.linked_invoice_unavailable"),
href: proforma.linkedInvoiceId
? `/issed-invoices/${proforma.linkedInvoiceId}`
: undefined,
@ -396,26 +394,27 @@ export function useProformasGridColumns(
label: t("pages.proformas.list.actions.change_status"),
icon: RefreshCwIcon,
disabled:
!actionHandlers.onChangeStatusClick || !isNormalScope || !nextTransition || isIssued,
disabledReason: !isNormalScope
? getScopeReason()
: isIssued
!(actionHandlers.onChangeStatusClick && isNormalScope && nextTransition) ||
isIssued,
disabledReason: isNormalScope
? isIssued
? t("pages.proformas.list.actions.disabled_reasons.status_issued")
: !nextTransition
? t("proformas.change_proforma_status_dialog.empty_available_statuses")
: undefined,
: nextTransition
? undefined
: t("proformas.change_proforma_status_dialog.empty_available_statuses")
: getScopeReason(),
onClick: handleChangeStatus,
},
{
key: "issue",
label: t("pages.proformas.list.actions.issue"),
icon: FileTextIcon,
disabled: !actionHandlers.onIssueClick || !isNormalScope || !isApproved || isIssued,
disabledReason: !isNormalScope
? getScopeReason()
: !isApproved
? t("proformas.issue_proforma_dialog.not_allowed_description")
: undefined,
disabled: !(actionHandlers.onIssueClick && isNormalScope && isApproved) || isIssued,
disabledReason: isNormalScope
? isApproved
? undefined
: t("proformas.issue_proforma_dialog.not_allowed_description")
: getScopeReason(),
onClick: handleIssue,
},
{
@ -432,31 +431,31 @@ export function useProformasGridColumns(
key: "archive",
label: t("pages.proformas.list.actions.archive"),
icon: FolderArchiveIcon,
disabled: !actionHandlers.onArchiveClick || !isArchivable,
disabledReason: !actionHandlers.onArchiveClick
? undefined
: !isNormalScope
? getScopeReason()
: t("proformas.archive.error_conflict_message"),
disabled: !(actionHandlers.onArchiveClick && isArchivable),
disabledReason: actionHandlers.onArchiveClick
? isNormalScope
? t("proformas.archive.error_conflict_message")
: getScopeReason()
: undefined,
onClick: handleArchive,
},
{
key: "unarchive",
label: t("pages.proformas.list.actions.unarchive"),
icon: Undo2Icon,
disabled: !actionHandlers.onUnarchiveClick || !isUnarchivable,
disabledReason: !actionHandlers.onUnarchiveClick
? undefined
: !isArchivedScope
? t("pages.proformas.list.actions.disabled_reasons.archived_only")
: t("proformas.unarchive.error_conflict_message"),
disabled: !(actionHandlers.onUnarchiveClick && isUnarchivable),
disabledReason: actionHandlers.onUnarchiveClick
? isArchivedScope
? t("proformas.unarchive.error_conflict_message")
: t("pages.proformas.list.actions.disabled_reasons.archived_only")
: undefined,
onClick: handleUnarchive,
},
{
key: "download-pdf",
label: t("pages.proformas.list.actions.download_pdf"),
icon: FileDownIcon,
disabled: !actionHandlers.onDownloadPdf || isDeletedScope || isPDFLoading,
disabled: Boolean(!actionHandlers.onDownloadPdf || isDeletedScope || isPDFLoading),
disabledReason: isDeletedScope
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
: isPDFLoading
@ -473,9 +472,9 @@ export function useProformasGridColumns(
disabled: !actionHandlers.onDeleteClick || isDeletedScope || !isDraft,
disabledReason: isDeletedScope
? t("pages.proformas.list.actions.disabled_reasons.deleted_scope")
: !isDraft
? t("proformas.delete_proforma_dialog.error_conflict_message")
: undefined,
: isDraft
? undefined
: t("proformas.delete_proforma_dialog.error_conflict_message"),
onClick: handleDelete,
},
];
@ -543,7 +542,9 @@ export function useProformasGridColumns(
return (
<Tooltip key={action.key}>
<TooltipTrigger render={action.disabled ? <div className="w-full">{item}</div> : item} />
<TooltipTrigger
render={action.disabled ? <div className="w-full">{item}</div> : item}
/>
<TooltipContent side="left">{action.disabledReason ?? action.label}</TooltipContent>
</Tooltip>
);
@ -574,11 +575,7 @@ export function useProformasGridColumns(
/>
<TooltipContent>{t("common.more_actions")}</TooltipContent>
</Tooltip>
<DropdownMenuContent
align="end"
className="w-56"
onClick={stopRowPropagation}
>
<DropdownMenuContent align="end" className="w-56" onClick={stopRowPropagation}>
<DropdownMenuGroup>
<DropdownMenuLabel>{t("common.actions")}</DropdownMenuLabel>
<DropdownMenuSeparator />

View File

@ -3,7 +3,7 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
import type { ReactNode } from "react";
export interface ProformaPageHeaderProps {
title: string;
title: string | ReactNode;
backLabel?: string;
onBack?: () => void;
statusLabel?: string;

View File

@ -1,7 +1,7 @@
{
"name": "@erp/customers",
"description": "Customers",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/document-series",
"description": "Document series module",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/identity",
"description": "Identity module",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/supplier-invoices",
"description": "Supplier invoices",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/suppliers",
"description": "Suppliers",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "uecko-erp-2025",
"private": true,
"version": "0.10.1",
"version": "0.10.2",
"workspaces": [
"apps/*",
"modules/*",

View File

@ -1,6 +1,6 @@
{
"name": "@repo/i18next",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"scripts": {

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-criteria",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,20 +0,0 @@
import { describe, expect, test } from "vitest";
import { buildBooleanFullTextQuery } from "../build-boolean-full-text-query";
describe("buildBooleanFullTextQuery", () => {
test.each([
["pr", "+pr*"],
["pr-", "+pr*"],
["-pr", "+pr*"],
["cliente prueba", "+cliente* +prueba*"],
["cliente-prueba", "+cliente* +prueba*"],
["José Pérez", "+José* +Pérez*"],
["++ -- **", ""],
["", ""],
[" ", ""],
['+ - > < ( ) ~ * " @', ""],
])("normalizes %p into %p", (input, expected) => {
expect(buildBooleanFullTextQuery(input)).toBe(expected);
});
});

View File

@ -1,53 +0,0 @@
import { describe, expect, test } from "vitest";
import { Op, type Sequelize } from "sequelize";
import { Criteria } from "../critera";
import { CriteriaToSequelizeConverter } from "../criteria-to-sequelize-converter";
const createDatabaseStub = () =>
({
escape: (value: string) => `'${value.replaceAll("'", "''")}'`,
}) as Sequelize;
describe("CriteriaToSequelizeConverter quick search", () => {
test("builds a full-text filter and score for free text search", () => {
const converter = new CriteriaToSequelizeConverter();
const criteria = Criteria.fromPrimitives([], "name", "asc", 10, 0, "pr-");
const options = converter.convert(criteria, {
searchableFields: ["name", "trade_name"],
sortableFields: ["name"],
enableFullText: true,
database: createDatabaseStub(),
strictMode: true,
});
expect(options.attributes).toEqual({
include: [[expect.objectContaining({ val: expect.stringContaining("'+pr*'") }), "score"]],
});
expect(options.order).toEqual([
[expect.objectContaining({ val: "score" }), "DESC"],
["name", "ASC"],
]);
expect(options.where).toEqual({
[Op.and]: expect.any(Array),
});
});
test("omits MATCH, score and score ordering when the normalized search is empty", () => {
const converter = new CriteriaToSequelizeConverter();
const criteria = Criteria.fromPrimitives([], "name", "asc", 10, 0, "++ -- **");
const options = converter.convert(criteria, {
searchableFields: ["name", "trade_name"],
sortableFields: ["name"],
enableFullText: true,
database: createDatabaseStub(),
strictMode: true,
});
expect(options.attributes).toBeUndefined();
expect(options.order).toEqual([["name", "ASC"]]);
expect(options.where).toBeUndefined();
});
});

View File

@ -1,5 +1,6 @@
import { type FindOptions, Op, type OrderItem, Sequelize, type WhereOptions } from "sequelize";
import { buildBooleanFullTextQuery } from "./build-boolean-full-text-query";
import type { Criteria } from "./critera.js";
import type {
ConvertParams,
@ -7,7 +8,6 @@ import type {
CriteriaMappings,
ICriteriaToOrmConverter,
} from "./types.js";
import { buildBooleanFullTextQuery } from "./build-boolean-full-text-query.js";
import { appendOrder, normalizeOrder, prependOrder } from "./utils";
/**
@ -256,10 +256,7 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
throw new Error(`[CriteriaToSequelizeConverter] Unsupported field mapping: ${String(value)}`);
}
private buildNullWhereCondition(
mapping: CriteriaFieldMapping,
value: unknown
): WhereOptions {
private buildNullWhereCondition(mapping: CriteriaFieldMapping, value: unknown): WhereOptions {
switch (mapping.type) {
case "root":
return {
@ -267,11 +264,16 @@ export class CriteriaToSequelizeConverter implements ICriteriaToOrmConverter {
};
case "association":
return Sequelize.where(Sequelize.col(`${mapping.association}.${mapping.column}`), value) as
unknown as WhereOptions;
return Sequelize.where(
Sequelize.col(`${mapping.association}.${mapping.column}`),
value
) as unknown as WhereOptions;
case "literal":
return Sequelize.where(Sequelize.literal(mapping.expression), value) as unknown as WhereOptions;
return Sequelize.where(
Sequelize.literal(mapping.expression),
value
) as unknown as WhereOptions;
default:
return this.assertNever(mapping);

View File

@ -1,5 +1,5 @@
export * from "./critera";
export * from "./build-boolean-full-text-query";
export * from "./critera";
export * from "./criteria-from-url-converter";
export * from "./criteria-to-sequelize-converter";
export * from "./defaults";

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ddd",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-logger",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ui",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-utils",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/shadcn-ui",
"version": "0.10.1",
"version": "0.10.2",
"type": "module",
"private": true,
"exports": {

View File

@ -1,6 +1,6 @@
{
"name": "@repo/typescript-config",
"version": "0.10.1",
"version": "0.10.2",
"private": true,
"publishConfig": {
"access": "public"