This commit is contained in:
David Arranz 2024-07-17 20:10:07 +02:00
parent a845edb9c7
commit 47802ab932
31 changed files with 259 additions and 227 deletions

View File

@ -57,7 +57,7 @@
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.8.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.51.5", "react-hook-form": "^7.52.1",
"react-hook-form-persist": "^2.1.0", "react-hook-form-persist": "^2.1.0",
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-resizable-panels": "^2.0.19", "react-resizable-panels": "^2.0.19",

View File

@ -42,26 +42,27 @@ import { useCallback, useMemo, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { FieldValues, UseFieldArrayReturn } from "react-hook-form"; import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
import { AddNewRowButton } from "./AddNewRowButton"; import { AddNewRowButton } from "./AddNewRowButton";
import { SortableDataTableToolbar } from "./SortableDataTableToolbar"; import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar";
import { SortableTableRow } from "./SortableTableRow"; import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow";
declare module "@tanstack/react-table" { declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> { interface TableMeta<TData extends RowData> {
insertItem: (rowIndex: number, data: TData) => void; insertItem: (rowIndex: number, data?: unknown) => void;
appendItem: (data: TData) => void; appendItem: (data?: unknown) => void;
duplicateItems: (rowIndex?: number) => void; duplicateItems: (rowIndex?: number) => void;
deleteItems: (rowIndex?: number | number[]) => void; deleteItems: (rowIndex?: number | number[]) => void;
updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void; updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void;
} }
} }
export interface SortableProps { export interface QuoteItemsSortableProps {
id: UniqueIdentifier; id: UniqueIdentifier;
} }
export type SortableDataTableProps = { export type QuoteItemsSortableDataTableProps = {
columns: ColumnDef<unknown, unknown>[]; columns: ColumnDef<unknown, unknown>[];
data: Record<"id", string>[]; data: Record<"id", string>[];
defaultValues: Readonly<{ [x: string]: any }> | undefined;
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields">; actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields">;
}; };
@ -94,34 +95,12 @@ const dropAnimationConfig: DropAnimation = {
}, },
}; };
/*const defaultColumn: Partial<ColumnDef<unknown>> = { export function QuoteItemsSortableDataTable({
cell: ({ table, row: { index, original }, column, getValue }) => { columns,
const initialValue = getValue(); data,
defaultValues,
// We need to keep and update the state of the cell normally actions,
// eslint-disable-next-line react-hooks/rules-of-hooks }: QuoteItemsSortableDataTableProps) {
const [value, setValue] = useState(initialValue);
// If the initialValue is changed external, sync it up with our state
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
<input
value={value as string}
onChange={(e) => setValue(e.target.value)}
onBlur={() => {
console.log(column.id, value);
table.options.meta?.updateItem(index, original, column.id, value);
}}
/>
);
},
};*/
export function SortableDataTable({ columns, data, actions }: SortableDataTableProps) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(); const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
@ -154,11 +133,11 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
maxSize: 96, //enforced during column resizing maxSize: 96, //enforced during column resizing
}, },
meta: { meta: {
insertItem: (rowIndex: number, data: object = {}) => { insertItem: (rowIndex: number, data?: unknown) => {
actions.insert(rowIndex, data, { shouldFocus: true }); actions.insert(rowIndex, data || defaultValues?.items[0], { shouldFocus: true });
}, },
appendItem: (data: object = {}) => { appendItem: (data?: unknown) => {
actions.append(data, { shouldFocus: true }); actions.append(data || defaultValues?.items[0], { shouldFocus: true });
}, },
duplicateItems: (rowIndex?: number) => { duplicateItems: (rowIndex?: number) => {
if (rowIndex != undefined) { if (rowIndex != undefined) {
@ -323,7 +302,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
onDragCancel={handleDragCancel} onDragCancel={handleDragCancel}
collisionDetection={closestCenter} collisionDetection={closestCenter}
> >
<SortableDataTableToolbar table={table} /> <QuoteItemsSortableDataTableToolbar table={table} />
<Table className='table-fixed'> <Table className='table-fixed'>
<TableHeader> <TableHeader>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
@ -346,13 +325,13 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
{filterItems(table.getRowModel().rows).map((row) => ( {filterItems(table.getRowModel().rows).map((row) => (
<SortableTableRow key={row.id} id={row.id}> <QuoteItemsSortableTableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className='px-2 py-1 align-top'> <TableCell key={cell.id} className='px-2 py-1 align-top'>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
</SortableTableRow> </QuoteItemsSortableTableRow>
))} ))}
</SortableContext> </SortableContext>
</TableBody> </TableBody>

View File

@ -27,7 +27,7 @@ import {
Trash2Icon, Trash2Icon,
} from "lucide-react"; } from "lucide-react";
export const SortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => { export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table<unknown> }) => {
const selectedRowsCount = table.getSelectedRowModel().rows.length; const selectedRowsCount = table.getSelectedRowModel().rows.length;
if (selectedRowsCount) { if (selectedRowsCount) {

View File

@ -4,20 +4,20 @@ import { DraggableSyntheticListeners } from "@dnd-kit/core";
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react"; import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react";
import { SortableProps } from "./SortableDataTable"; import { QuoteItemsSortableProps } from "./QuoteItemsSortableDataTable";
interface Context { interface Context {
attributes: Record<string, any>; attributes: Record<string, any>;
listeners: DraggableSyntheticListeners; listeners: DraggableSyntheticListeners;
ref(node: HTMLElement | null): void; ref(node: HTMLElement | null): void;
} }
export const SortableTableRowContext = createContext<Context>({ export const QuoteItemsSortableTableRowContext = createContext<Context>({
attributes: {}, attributes: {},
listeners: undefined, listeners: undefined,
ref() {}, ref() {},
}); });
function animateLayoutChanges(args) { function animateLayoutChanges(args: any) {
if (args.isSorting || args.wasDragging) { if (args.isSorting || args.wasDragging) {
return defaultAnimateLayoutChanges(args); return defaultAnimateLayoutChanges(args);
} }
@ -25,7 +25,10 @@ function animateLayoutChanges(args) {
return true; return true;
} }
export function SortableTableRow({ id, children }: PropsWithChildren<SortableProps>) { export function QuoteItemsSortableTableRow({
id,
children,
}: PropsWithChildren<QuoteItemsSortableProps>) {
const { const {
attributes, attributes,
isDragging, isDragging,
@ -54,7 +57,7 @@ export function SortableTableRow({ id, children }: PropsWithChildren<SortablePro
); );
return ( return (
<SortableTableRowContext.Provider value={context}> <QuoteItemsSortableTableRowContext.Provider value={context}>
<TableRow <TableRow
key={id} key={id}
id={String(id)} id={String(id)}
@ -67,6 +70,6 @@ export function SortableTableRow({ id, children }: PropsWithChildren<SortablePro
> >
{children} {children}
</TableRow> </TableRow>
</SortableTableRowContext.Provider> </QuoteItemsSortableTableRowContext.Provider>
); );
} }

View File

@ -1,8 +1,7 @@
import { Badge, Button, Card, CardContent } from "@/ui"; import { Badge, Button, Card, CardContent } from "@/ui";
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
import { DataTable } from "@/components";
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar"; import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
import { useDataTable, useDataTableContext } from "@/lib/hooks"; import { useDataTable, useDataTableContext } from "@/lib/hooks";
import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts"; import { IListQuotes_Response_DTO, MoneyValue, UTCDateValue } from "@shared/contexts";

View File

@ -12,16 +12,18 @@ import { t } from "i18next";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useFieldArray, useFormContext } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import { useDetailColumns } from "../../hooks"; import { useDetailColumns } from "../../hooks";
import { SortableDataTable } from "../SortableDataTable"; import { QuoteItemsSortableDataTable } from "../QuoteItemsSortableDataTable";
export const QuoteDetailsCardEditor = ({ export const QuoteDetailsCardEditor = ({
currency, currency,
language, language,
defaultValues,
}: { }: {
currency: CurrencyData; currency: CurrencyData;
language: Language; language: Language;
defaultValues: Readonly<{ [x: string]: any }> | undefined;
}) => { }) => {
const { control, register, getValues } = useFormContext(); const { control, register } = useFormContext();
const { fields, ...fieldActions } = useFieldArray({ const { fields, ...fieldActions } = useFieldArray({
control, control,
@ -186,7 +188,7 @@ export const QuoteDetailsCardEditor = ({
); );
const handleInsertArticle = useCallback( const handleInsertArticle = useCallback(
(newArticle) => { (newArticle: any) => {
fieldActions.append({ fieldActions.append({
...newArticle, ...newArticle,
quantity: { quantity: {
@ -204,7 +206,14 @@ export const QuoteDetailsCardEditor = ({
const defaultLayout = [265, 440, 655]; const defaultLayout = [265, 440, 655];
const navCollapsedSize = 4; const navCollapsedSize = 4;
return <SortableDataTable actions={fieldActions} columns={columns} data={fields} />; return (
<QuoteItemsSortableDataTable
actions={fieldActions}
columns={columns}
data={fields}
defaultValues={defaultValues}
/>
);
return ( return (
<ResizablePanelGroup <ResizablePanelGroup
@ -226,7 +235,7 @@ export const QuoteDetailsCardEditor = ({
}} }}
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")} className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
> >
<SortableDataTable actions={fieldActions} columns={columns} data={fields} /> <QuoteItemsSortableDataTable actions={fieldActions} columns={columns} data={fields} />
</ResizablePanel> </ResizablePanel>
<ResizableHandle withHandle className='mx-3' /> <ResizableHandle withHandle className='mx-3' />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}> <ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>

View File

@ -6,6 +6,12 @@ import { useFormContext } from "react-hook-form";
export const QuoteGeneralCardEditor = () => { export const QuoteGeneralCardEditor = () => {
const { register, formState } = useFormContext(); const { register, formState } = useFormContext();
console.log({
...register("customer_information", {
required: true,
}),
});
return ( return (
<div className='grid gap-6 md:grid-cols-6'> <div className='grid gap-6 md:grid-cols-6'>
<FormGroup <FormGroup

View File

@ -48,7 +48,7 @@ export const QuoteCreate = () => {
setWarnWhen(false); setWarnWhen(false);
mutate(formData, { mutate(formData, {
onSuccess: (data) => { onSuccess: (data) => {
navigate(`/quotes/edit/${data.id}`, { relative: "path" }); navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
}, },
}); });
} finally { } finally {

View File

@ -8,16 +8,20 @@ import {
import { calculateItemTotals } from "@/lib/calc"; import { calculateItemTotals } from "@/lib/calc";
import { useUrlId } from "@/lib/hooks/useUrlId"; import { useUrlId } from "@/lib/hooks/useUrlId";
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui"; import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
import { CurrencyData, IUpdateQuote_Request_DTO, Language, MoneyValue } from "@shared/contexts"; import { CurrencyData, IGetQuote_Response_DTO, Language, MoneyValue } from "@shared/contexts";
import { t } from "i18next"; import { t } from "i18next";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors"; import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
import { useQuotes } from "./hooks"; import { useQuotes } from "./hooks";
interface QuoteDataForm extends IUpdateQuote_Request_DTO {} /*type QuoteDataForm = Omit<IGetQuote_Response_DTO, "items"> & {
items: IGetQuote_QuoteItem_Response_DTO;
};*/
type QuoteDataForm = IGetQuote_Response_DTO;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const QuoteEdit = () => { export const QuoteEdit = () => {
@ -42,12 +46,8 @@ export const QuoteEdit = () => {
const { data, status, error: queryError } = useOne(quoteId); const { data, status, error: queryError } = useOne(quoteId);
const { mutate } = useUpdate(String(quoteId)); const defaultValues = useMemo(
() => ({
const form = useForm<QuoteDataForm>({
mode: "onBlur",
values: data,
defaultValues: {
date: "", date: "",
reference: "", reference: "",
customer_information: "", customer_information: "",
@ -59,7 +59,7 @@ export const QuoteEdit = () => {
subtotal_price: { subtotal_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code, currency_code: data?.currency_code ?? quoteCurrency.code,
}, },
discount: { discount: {
amount: undefined, amount: undefined,
@ -68,32 +68,46 @@ export const QuoteEdit = () => {
total_price: { total_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code, currency_code: data?.currency_code ?? quoteCurrency.code,
}, },
items: [ items: [
{ {
description: "", description: "",
quantity: { quantity: {
amount: undefined, amount: 1,
scale: 0, scale: 0,
}, },
subtotal_price: { unit_price: {
amount: undefined, amount: null,
scale: 4, scale: 4,
currency_code: data?.currency_code, currency_code: data?.currency_code ?? quoteCurrency.code,
},
subtotal_price: {
amount: null,
scale: 4,
currency_code: data?.currency_code ?? quoteCurrency.code,
}, },
discount: { discount: {
amount: undefined, amount: null,
scale: 2, scale: 2,
}, },
total_price: { total_price: {
amount: undefined, amount: null,
scale: 4, scale: 4,
currency_code: data?.currency_code, currency_code: data?.currency_code ?? quoteCurrency.code,
}, },
}, },
], ],
}, }),
[data, quoteCurrency]
);
const { mutate } = useUpdate(String(quoteId));
const form = useForm<QuoteDataForm>({
mode: "onBlur",
values: data,
defaultValues,
}); });
const { watch, getValues, setValue, formState } = form; const { watch, getValues, setValue, formState } = form;
@ -117,6 +131,7 @@ export const QuoteEdit = () => {
mutate(data, { mutate(data, {
onError: (error) => { onError: (error) => {
console.debug(error); console.debug(error);
toast.error(error.message);
//alert(error.message); //alert(error.message);
}, },
//onSettled: () => {}, //onSettled: () => {},
@ -132,9 +147,6 @@ export const QuoteEdit = () => {
const { unsubscribe } = watch((_, { name, type }) => { const { unsubscribe } = watch((_, { name, type }) => {
const value = getValues(); const value = getValues();
console.log("USEEFFECT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
console.log(name);
if (name) { if (name) {
if (name === "currency_code") { if (name === "currency_code") {
setQuoteCurrency( setQuoteCurrency(
@ -241,7 +253,11 @@ export const QuoteEdit = () => {
<QuoteGeneralCardEditor /> <QuoteGeneralCardEditor />
</TabsContent> </TabsContent>
<TabsContent value='items'> <TabsContent value='items'>
<QuoteDetailsCardEditor currency={quoteCurrency} language={quoteLanguage} /> <QuoteDetailsCardEditor
currency={quoteCurrency}
language={quoteLanguage}
defaultValues={defaultValues}
/>
</TabsContent> </TabsContent>
<TabsContent value='history'></TabsContent> <TabsContent value='history'></TabsContent>

View File

@ -110,6 +110,9 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
)} )}
<FormControl> <FormControl>
<CurrencyInput <CurrencyInput
intlConfig={{
locale: language.code,
}}
name={field.name} name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras //ref={field.ref} <-- no activar que hace cosas raras
onBlur={field.onBlur} onBlur={field.onBlur}

View File

@ -48,7 +48,7 @@ import { SortableTableRow } from "./SortableTableRow";
declare module "@tanstack/react-table" { declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> { interface TableMeta<TData extends RowData> {
insertItem: (rowIndex: number, data: TData) => void; insertItem: (rowIndex: number, data: TData) => void;
appendItem: (data: TData) => void; appendItem: (data?: TData) => void;
duplicateItems: (rowIndex?: number) => void; duplicateItems: (rowIndex?: number) => void;
deleteItems: (rowIndex?: number | number[]) => void; deleteItems: (rowIndex?: number | number[]) => void;
updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void; updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void;

View File

@ -11,5 +11,5 @@ export * from "./Layout";
export * from "./LoadingIndicator"; export * from "./LoadingIndicator";
export * from "./LoadingOverlay"; export * from "./LoadingOverlay";
export * from "./ProtectedRoute"; export * from "./ProtectedRoute";
export * from "./SorteableDataTable"; //export * from "./SorteableDataTable";
export * from "./TailwindIndicator"; export * from "./TailwindIndicator";

View File

@ -1,24 +1,13 @@
import { MoneyValue, Percentage, Quantity } from "@shared/contexts"; import { MoneyValue, Percentage, Quantity } from "@shared/contexts";
import { IMoney, IPercentage, IQuantity } from "./types";
export const calculateItemTotals = (item: { export const calculateItemTotals = (item: any) => {
quantity?: IQuantity;
unit_price?: IMoney;
discount?: IPercentage;
}): {
quantity: Quantity;
unitPrice: MoneyValue;
subtotalPrice: MoneyValue;
discount: Percentage;
totalPrice: MoneyValue;
} | null => {
console.log(item); console.log(item);
const { quantity: quantity_dto, unit_price: unit_price_dto, discount: discount_dto } = item; const { quantity: quantity_dto, unit_price: unit_price_dto, discount: discount_dto } = item;
if (quantity_dto === "" || unit_price_dto === "") { /*if (quantity_dto.amount === null || unit_price_dto.amount === null) {
return null; return null;
} }*/
const quantityOrError = Quantity.create(quantity_dto); const quantityOrError = Quantity.create(quantity_dto);
if (quantityOrError.isFailure) { if (quantityOrError.isFailure) {

View File

@ -7,33 +7,29 @@ import {
import { NullOr } from "@shared/utilities"; import { NullOr } from "@shared/utilities";
import Joi from "joi"; import Joi from "joi";
export interface IArticleIdentifierOptions export interface IArticleIdentifierOptions extends INullableValueObjectOptions {}
extends INullableValueObjectOptions {}
export class ArticleIdentifier extends NullableValueObject<number> { export class ArticleIdentifier extends NullableValueObject<number> {
protected static validate( protected static validate(
value: NullOr<number | string>, value: NullOr<number | string>,
options: IArticleIdentifierOptions = {}, options: IArticleIdentifierOptions = {}
) { ) {
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(null); const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(null);
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label( const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
options.label ? options.label : "ArticleIdentifier", options.label ? options.label : "ArticleIdentifier"
); );
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex( const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
/^[-]?\d+$/, options.label ? options.label : "ArticleIdentifier"
).label(options.label ? options.label : "ArticleIdentifier"); );
const rules = Joi.alternatives(ruleNull, ruleNumber, ruleString); const rules = Joi.alternatives(ruleNull, ruleNumber, ruleString);
return RuleValidator.validate<NullOr<number>>(rules, value); return RuleValidator.validate<NullOr<number>>(rules, value);
} }
public static create( public static create(value: NullOr<number | string>, options: IArticleIdentifierOptions = {}) {
value: NullOr<number | string>,
options: IArticleIdentifierOptions = {},
) {
const _options = { const _options = {
label: "ArticleIdentifier", label: "ArticleIdentifier",
...options, ...options,
@ -64,8 +60,8 @@ export class ArticleIdentifier extends NullableValueObject<number> {
return this.isNull() ? "" : String(this.value); return this.isNull() ? "" : String(this.value);
} }
public toPrimitive(): number { public toPrimitive(): number | null {
return this.toNumber(); return this.isNull() ? null : this.toNumber();
} }
public increment(amount: number = 1) { public increment(amount: number = 1) {

View File

@ -190,7 +190,10 @@ export class CreateQuoteUseCase
QuoteItem.create({ QuoteItem.create({
articleId: item.article_id, articleId: item.article_id,
description: Description.create(item.description).object, description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object, quantity: Quantity.create({
amount: item.quantity.amount,
scale: item.quantity.scale,
}).object,
unitPrice: UnitPrice.create({ unitPrice: UnitPrice.create({
amount: item.unit_price?.amount, amount: item.unit_price?.amount,
currencyCode: item.unit_price?.currency_code, currencyCode: item.unit_price?.currency_code,

View File

@ -24,6 +24,7 @@ import {
UnitPrice, UnitPrice,
} from "@shared/contexts"; } from "@shared/contexts";
import { ArticleIdentifier } from "@/contexts/catalog/domain";
import { IUpdateQuote_Request_DTO } from "@shared/contexts"; import { IUpdateQuote_Request_DTO } from "@shared/contexts";
import { import {
Dealer, Dealer,
@ -183,28 +184,73 @@ export class UpdateQuoteUseCase
return Result.fail(discountOrError.error); return Result.fail(discountOrError.error);
} }
const items = new Collection<QuoteItem>( let items: Collection<QuoteItem>;
quoteDTO.items?.map(
(item) => try {
QuoteItem.create({ items = new Collection<QuoteItem>(
articleId: item.article_id, quoteDTO.items?.map((item) => {
description: Description.create(item.description).object, const articleIdOrError = ArticleIdentifier.create(item.article_id);
quantity: Quantity.create({ if (articleIdOrError.isFailure) {
amount: item.quantity.amount, throw articleIdOrError.error;
scale: item.quantity.scale, }
}).object,
unitPrice: UnitPrice.create({ const descriptionOrError = Description.create(item.description);
amount: item.unit_price?.amount, if (descriptionOrError.isFailure) {
currencyCode: item.unit_price?.currency_code, throw descriptionOrError.error;
scale: item.unit_price?.scale, }
}).object,
discount: Percentage.create({ const quantityOrError = Quantity.create({
amount: item.discount?.amount, amount: item.quantity.amount,
scale: item.discount?.scale, scale: item.quantity.scale,
}).object, });
}).object if (quantityOrError.isFailure) {
) throw quantityOrError.error;
); }
const unitPriceOrError = UnitPrice.create({
amount: item.unit_price?.amount,
currencyCode: item.unit_price?.currency_code,
scale: item.unit_price?.scale,
});
if (unitPriceOrError.isFailure) {
throw unitPriceOrError.error;
}
const percentageOrError = Percentage.create({
amount: item.discount?.amount,
scale: item.discount?.scale,
});
if (percentageOrError.isFailure) {
throw percentageOrError.error;
}
const quoteItemOrError = QuoteItem.create({
articleId: articleIdOrError.object,
description: descriptionOrError.object,
quantity: quantityOrError.object,
unitPrice: unitPriceOrError.object,
discount: percentageOrError.object,
});
if (quoteItemOrError.isFailure) {
throw quoteItemOrError.error;
}
return quoteItemOrError.object;
})
);
} catch (e: unknown) {
//let error = e as Error;
/*if (error.name === "ValidationError") {
error = e as ValidationError;
}
if (error.name === "DomainError") {
error = e as DomainError;
}*/
return Result.fail(e as IDomainError);
}
return Quote.create( return Quote.create(
{ {

View File

@ -1,3 +1,4 @@
import { ArticleIdentifier } from "@/contexts/catalog/domain";
import { import {
Description, Description,
Entity, Entity,
@ -11,7 +12,7 @@ import {
} from "@shared/contexts"; } from "@shared/contexts";
export interface IQuoteItemProps extends IEntityProps { export interface IQuoteItemProps extends IEntityProps {
articleId: string | null; articleId: ArticleIdentifier;
description: Description; // Descripción del artículo o servicio description: Description; // Descripción del artículo o servicio
quantity: Quantity; // Cantidad de unidades quantity: Quantity; // Cantidad de unidades
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
@ -21,7 +22,7 @@ export interface IQuoteItemProps extends IEntityProps {
} }
export interface IQuoteItem { export interface IQuoteItem {
articleId: string | null; articleId: ArticleIdentifier;
description: Description; description: Description;
quantity: Quantity; quantity: Quantity;
unitPrice: MoneyValue; unitPrice: MoneyValue;
@ -35,7 +36,7 @@ export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
return Result.ok(new QuoteItem(props, id)); return Result.ok(new QuoteItem(props, id));
} }
get articleId(): string | null { get articleId(): ArticleIdentifier {
return this.props.articleId; return this.props.articleId;
} }

View File

@ -1,3 +1,4 @@
import { ArticleIdentifier } from "@/contexts/catalog/domain";
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure"; import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { Description, MoneyValue, Percentage, Quantity, UniqueID } from "@shared/contexts"; import { Description, MoneyValue, Percentage, Quantity, UniqueID } from "@shared/contexts";
import { IQuoteItemProps, Quote, QuoteItem } from "../../domain"; import { IQuoteItemProps, Quote, QuoteItem } from "../../domain";
@ -22,8 +23,10 @@ class QuoteItemMapper
const { sourceParent } = params; const { sourceParent } = params;
const id = this.mapsValue(source, "item_id", UniqueID.create); const id = this.mapsValue(source, "item_id", UniqueID.create);
const articleId = this.mapsValue(source, "id_article", ArticleIdentifier.create);
const props: IQuoteItemProps = { const props: IQuoteItemProps = {
articleId: source.id_article === "" ? null : source.id_article, articleId,
description: this.mapsValue(source, "description", Description.create), description: this.mapsValue(source, "description", Description.create),
quantity: this.mapsValue(source, "quantity", (quantity) => quantity: this.mapsValue(source, "quantity", (quantity) =>
Quantity.create({ Quantity.create({
@ -83,7 +86,7 @@ class QuoteItemMapper
item_id: source.id.toString(), item_id: source.id.toString(),
quote_id: sourceParent.id.toPrimitive(), quote_id: sourceParent.id.toPrimitive(),
position: index, position: index,
id_article: source.articleId, id_article: source.articleId.toPrimitive(),
description: source.description.toPrimitive(), description: source.description.toPrimitive(),
quantity: source.quantity.convertScale(2).toPrimitive(), quantity: source.quantity.convertScale(2).toPrimitive(),
unit_price: source.unitPrice.convertScale(4).toPrimitive(), unit_price: source.unitPrice.convertScale(4).toPrimitive(),

View File

@ -30,7 +30,7 @@ export class QuoteItem_Model extends Model<
declare quote_id: string; declare quote_id: string;
declare item_id: string; declare item_id: string;
declare id_article: CreationOptional<string | null>; declare id_article: CreationOptional<number | null>;
declare position: number; declare position: number;
declare description: CreationOptional<string | null>; declare description: CreationOptional<string | null>;
declare quantity: CreationOptional<number | null>; declare quantity: CreationOptional<number | null>;

View File

@ -2,7 +2,7 @@ import Joi from "joi";
import { Result, RuleValidator } from "../../domain"; import { Result, RuleValidator } from "../../domain";
export interface IMoney_DTO { export interface IMoney_DTO {
amount: number; amount: number | null;
scale: number; scale: number;
currency_code: string; currency_code: string;
} }

View File

@ -1,5 +1,5 @@
export interface IPercentage_DTO { export interface IPercentage_DTO {
amount: number; amount: number | null;
scale: number; scale: number;
} }

View File

@ -1,8 +1,8 @@
import Joi from "joi"; import Joi from "joi";
import { Result, RuleValidator } from "../../domain"; import { Result, RuleValidator } from "../../domain";
export interface IQuantity_Request_DTO { export interface IQuantity_DTO {
amount: number; amount: number | null;
scale: number; scale: number;
} }
@ -21,4 +21,5 @@ export function ensureQuantity_DTOIsValid(quantity: IQuantity_Request_DTO) {
return Result.ok(true); return Result.ok(true);
} }
export interface IQuantity_Response_DTO extends IQuantity_Request_DTO {} export interface IQuantity_Request_DTO extends IQuantity_DTO {}
export interface IQuantity_Response_DTO extends IQuantity_DTO {}

View File

@ -1,29 +1,23 @@
import Joi from "joi"; import Joi from "joi";
import { UndefinedOr } from "../../../../utilities"; import { UndefinedOr } from "../../../../utilities";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator"; import { RuleValidator } from "../RuleValidator";
import { Result } from "./Result"; import { Result } from "./Result";
import { import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject";
IStringValueObjectOptions,
StringValueObject,
} from "./StringValueObject";
export class Description extends StringValueObject { export class Description extends StringValueObject {
protected static validate( protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
value: UndefinedOr<string>,
options: IStringValueObjectOptions
) {
const ruleIsEmpty = Joi.string() const ruleIsEmpty = Joi.string()
.optional() .optional()
.allow(null)
.allow("")
.default("") .default("")
.label(String(options.label)); .label(String(options.label));
return RuleValidator.validate<string>(ruleIsEmpty, value); return RuleValidator.validate<string>(ruleIsEmpty, value);
} }
public static create( public static create(value: UndefinedOr<string>, options: IStringValueObjectOptions = {}) {
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {}
) {
const _options = { const _options = {
label: "description", label: "description",
...options, ...options,
@ -32,7 +26,9 @@ export class Description extends StringValueObject {
const validationResult = Description.validate(value, _options); const validationResult = Description.validate(value, _options);
if (validationResult.isFailure) { if (validationResult.isFailure) {
return Result.fail(validationResult.error); return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
} }
return Result.ok(new Description(validationResult.object)); return Result.ok(new Description(validationResult.object));

View File

@ -1,27 +1,17 @@
import { UndefinedOr } from "../../../../utilities"; import { UndefinedOr } from "../../../../utilities";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator"; import { RuleValidator } from "../RuleValidator";
import { Result } from "./Result"; import { Result } from "./Result";
import { import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject";
IStringValueObjectOptions,
StringValueObject,
} from "./StringValueObject";
export class Measure extends StringValueObject { export class Measure extends StringValueObject {
protected static validate( protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
value: UndefinedOr<string>, const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("").label(String(options.label));
options: IStringValueObjectOptions,
) {
const ruleIsEmpty = RuleValidator.RULE_ALLOW_EMPTY.default("").label(
String(options.label),
);
return RuleValidator.validate<string>(ruleIsEmpty, value); return RuleValidator.validate<string>(ruleIsEmpty, value);
} }
public static create( public static create(value: UndefinedOr<string>, options: IStringValueObjectOptions = {}) {
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {},
) {
const _options = { const _options = {
label: "description", label: "description",
...options, ...options,
@ -30,7 +20,9 @@ export class Measure extends StringValueObject {
const validationResult = Measure.validate(value, _options); const validationResult = Measure.validate(value, _options);
if (validationResult.isFailure) { if (validationResult.isFailure) {
return Result.fail(validationResult.error); return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
} }
return Result.ok(new Measure(validationResult.object)); return Result.ok(new Measure(validationResult.object));

View File

@ -4,6 +4,7 @@ import DineroFactory, { Currency, Dinero } from "dinero.js";
import Joi from "joi"; import Joi from "joi";
import { isNull } from "lodash"; import { isNull } from "lodash";
import { NullOr, UndefinedOr } from "../../../../utilities"; import { NullOr, UndefinedOr } from "../../../../utilities";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator"; import { RuleValidator } from "../RuleValidator";
import { CurrencyData } from "./CurrencyData"; import { CurrencyData } from "./CurrencyData";
import { Result } from "./Result"; import { Result } from "./Result";
@ -144,7 +145,9 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
const validationResult = MoneyValue.validate(amount, options); const validationResult = MoneyValue.validate(amount, options);
if (validationResult.isFailure) { if (validationResult.isFailure) {
return Result.fail(validationResult.error); return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, options)
);
} }
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object); const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);

View File

@ -218,7 +218,7 @@ export class Percentage extends NullableValueObject<IPercentage> {
} }
get scale(): number { get scale(): number {
return this.isNull() ? 0 : Number(this.props?.scale); return Number(this.props?.scale);
} }
public getAmount(): NullOr<number> { public getAmount(): NullOr<number> {

View File

@ -1,5 +1,6 @@
import Joi from "joi"; import Joi from "joi";
import { NullOr } from "../../../../utilities"; import { NullOr } from "../../../../utilities";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator"; import { RuleValidator } from "../RuleValidator";
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject"; import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
import { Result } from "./Result"; import { Result } from "./Result";
@ -98,7 +99,9 @@ export class Quantity extends NullableValueObject<IQuantity> {
const validationResult = Quantity.validate(amount, scale, _options); const validationResult = Quantity.validate(amount, scale, _options);
if (validationResult.isFailure) { if (validationResult.isFailure) {
return Result.fail(validationResult.error); return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
} }
let _amount: NullOr<number> = Quantity._sanitize(amount); let _amount: NullOr<number> = Quantity._sanitize(amount);

View File

@ -22,14 +22,8 @@ export class FieldCriteria extends StringValueObject implements IFieldCriteria {
} }
protected static validate(value: UndefinedOr<string>) { protected static validate(value: UndefinedOr<string>) {
if ( if (RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, value).isSuccess) {
RuleValidator.validate(RuleValidator.RULE_NOT_NULL_OR_UNDEFINED, value) const stringOrError = RuleValidator.validate(RuleValidator.RULE_IS_TYPE_STRING, value);
.isSuccess
) {
const stringOrError = RuleValidator.validate(
RuleValidator.RULE_IS_TYPE_STRING,
value
);
if (stringOrError.isFailure) { if (stringOrError.isFailure) {
return stringOrError; return stringOrError;

View File

@ -1,21 +1,16 @@
import Joi from "joi"; import Joi from "joi";
import { UndefinedOr } from "../../../../utilities"; import { UndefinedOr } from "../../../../utilities";
import { DomainError, handleDomainError } from "../errors";
import { RuleValidator } from "../RuleValidator"; import { RuleValidator } from "../RuleValidator";
import { Result } from "./Result"; import { Result } from "./Result";
import { import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject";
IStringValueObjectOptions,
StringValueObject,
} from "./StringValueObject";
export class Slug extends StringValueObject { export class Slug extends StringValueObject {
protected static readonly MIN_LENGTH = 2; protected static readonly MIN_LENGTH = 2;
protected static readonly MAX_LENGTH = 100; protected static readonly MAX_LENGTH = 100;
protected static validate( protected static validate(value: UndefinedOr<string>, options: IStringValueObjectOptions) {
value: UndefinedOr<string>,
options: IStringValueObjectOptions
) {
const rule = Joi.string() const rule = Joi.string()
.allow(null) .allow(null)
.allow("") .allow("")
@ -66,10 +61,7 @@ export class Slug extends StringValueObject {
return slug ? slug.trim() : ""; return slug ? slug.trim() : "";
} }
public static create( public static create(value: UndefinedOr<string>, options: IStringValueObjectOptions = {}) {
value: UndefinedOr<string>,
options: IStringValueObjectOptions = {}
) {
const _options = { const _options = {
label: "slug", label: "slug",
...options, ...options,
@ -78,7 +70,9 @@ export class Slug extends StringValueObject {
const validationResult = Slug.validate(value, _options); const validationResult = Slug.validate(value, _options);
if (validationResult.isFailure) { if (validationResult.isFailure) {
return Result.fail(validationResult.error); return Result.fail(
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
} }
const slugValue = Slug.sanitize(validationResult.object); const slugValue = Slug.sanitize(validationResult.object);

View File

@ -1,8 +1,4 @@
import { import { IMoney_DTO, IPercentage_DTO, IQuantity_DTO } from "../../../../../common";
IMoney_Response_DTO,
IPercentage_Response_DTO,
IQuantity_Response_DTO,
} from "../../../../../common";
export interface IGetQuote_Response_DTO { export interface IGetQuote_Response_DTO {
id: string; id: string;
@ -17,9 +13,9 @@ export interface IGetQuote_Response_DTO {
notes: string; notes: string;
validity: string; validity: string;
subtotal_price: IMoney_Response_DTO; subtotal_price: IMoney_DTO;
discount: IPercentage_Response_DTO; discount: IPercentage_DTO;
total_price: IMoney_Response_DTO; total_price: IMoney_DTO;
items: IGetQuote_QuoteItem_Response_DTO[]; items: IGetQuote_QuoteItem_Response_DTO[];
@ -28,10 +24,10 @@ export interface IGetQuote_Response_DTO {
export interface IGetQuote_QuoteItem_Response_DTO { export interface IGetQuote_QuoteItem_Response_DTO {
article_id: string; article_id: string;
quantity: IQuantity_Response_DTO; quantity: IQuantity_DTO;
description: string; description: string;
unit_price: IMoney_Response_DTO; unit_price: IMoney_DTO;
subtotal_price: IMoney_Response_DTO; subtotal_price: IMoney_DTO;
discount: IPercentage_Response_DTO; discount: IPercentage_DTO;
total_price: IMoney_Response_DTO; total_price: IMoney_DTO;
} }

View File

@ -50,49 +50,49 @@ export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Requ
validity: Joi.string().optional().allow(null).allow("").default(""), validity: Joi.string().optional().allow(null).allow("").default(""),
subtotal_price: Joi.object({ subtotal_price: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
currency_code: Joi.string(), currency_code: Joi.string(),
}).unknown(), }).optional(),
discount: Joi.object({ discount: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
}).unknown(), }).optional(),
total_price: Joi.object({ total_price: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
currency_code: Joi.string(), currency_code: Joi.string(),
}).unknown(), }).optional(),
items: Joi.array().items( items: Joi.array().items(
Joi.object({ Joi.object({
article_id: Joi.string().optional().allow(null).allow("").default(""), article_id: Joi.number().optional().allow(null).allow("").default(""),
quantity: Joi.object({ quantity: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
}).unknown(), }).optional(),
description: Joi.string(), description: Joi.string().optional().allow(null).allow("").default(""),
unit_price: Joi.object({ unit_price: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
currency_code: Joi.string(), currency_code: Joi.string(),
}).unknown(), }).optional(),
subtotal_price: Joi.object({ subtotal_price: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
currency_code: Joi.string(), currency_code: Joi.string(),
}).unknown(), }).optional(),
discount: Joi.object({ discount: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
}).unknown(), }).optional(),
total_price: Joi.object({ total_price: Joi.object({
amount: Joi.number(), amount: Joi.number().allow(null),
scale: Joi.number(), scale: Joi.number(),
currency_code: Joi.string(), currency_code: Joi.string(),
}).unknown(), }).optional(),
}).unknown(true) }).unknown(true)
), ),
}).unknown(true); }).unknown(true);