Facturas de cliente
This commit is contained in:
parent
81fffc4a0e
commit
4a47e6f249
@ -9,6 +9,7 @@
|
||||
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
|
||||
<title>FactuGES 2025</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link href="/src/global.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
"@hookform/devtools": "^4.4.0",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.1.5",
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@tanstack/react-query-devtools": "^5.74.11",
|
||||
"@types/dinero.js": "^1.9.4",
|
||||
"@types/node": "^22.15.12",
|
||||
@ -33,9 +32,11 @@
|
||||
"@erp/auth": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@repo/rdx-criteria": "workspace:*",
|
||||
"@repo/rdx-ui": "workspace:*",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"axios": "^1.9.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
@ -50,7 +51,7 @@
|
||||
"react-secure-storage": "^1.3.2",
|
||||
"sequelize": "^6.37.5",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export { default } from "@repo/shadcn-ui/postcss.config.mjs";
|
||||
@ -1,3 +1,2 @@
|
||||
|
||||
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@import '@repo/shadcn-ui/globals.css';
|
||||
@import '@repo/rdx-ui/globals.css';
|
||||
@import "@repo/shadcn-ui/globals.css";
|
||||
@import "@repo/rdx-ui/globals.css";
|
||||
@import "@erp/customers/globals.css";
|
||||
@import "@erp/customer-invoices/globals.css";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Calibri", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
@theme {
|
||||
--font-sans: "Calibri", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tailwind CSS official document:
|
||||
* https://tailwindcss.com/docs/detecting-classes-in-source-files
|
||||
*
|
||||
* if you ever need to explicitly add a source that's excluded by default,
|
||||
* if you ever need to explicitly add a source that's excluded by default,
|
||||
* you can always add it with the @source directive.
|
||||
*/
|
||||
|
||||
@source '../node_modules/@repo/shadcn-ui';
|
||||
@source '../node_modules/@repo/rdx-ui';
|
||||
@source '../node_modules/@repo/rdx-ui';
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "@repo/shadcn-ui/tailwind.config.mjs";
|
||||
@ -25,7 +25,7 @@
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"noBannedTypes": "info",
|
||||
"useOptionalChain": "info"
|
||||
"useOptionalChain": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noImplicitAnyLet": "info",
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./dto";
|
||||
export * from "./schemas";
|
||||
export * from "./types";
|
||||
|
||||
29
modules/core/src/common/schemas/core.schemas.ts
Normal file
29
modules/core/src/common/schemas/core.schemas.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/***
|
||||
* Cantidad
|
||||
* – admite decimales hasta 2 cifras
|
||||
* – el campo `amount` puede ser null
|
||||
*/
|
||||
export const makeAmountSchema = ({ maxScale = 2, nullable = false } = {}) => {
|
||||
const amount = z.number().refine((v) => Number.isFinite(v), "Amount must be a finite number");
|
||||
|
||||
return z.object({
|
||||
amount: nullable ? amount.nullable() : amount,
|
||||
scale: z.number().int().nonnegative().max(maxScale, `Scale cannot exceed ${maxScale}`),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Porcentaje ≥ 0 ≤ 100
|
||||
* – admite decimales hasta 2 cifras
|
||||
* – el campo `amount` puede ser null
|
||||
*/
|
||||
export const makePercentageSchema = ({ maxScale = 2, min = 0, max = 100, nullable = true } = {}) =>
|
||||
makeAmountSchema({ maxScale, nullable }).extend({
|
||||
amount: z
|
||||
.number()
|
||||
.min(min, `El porcentaje debe ser ≥ ${min}`)
|
||||
.max(max, `El porcentaje no puede superar ${max}`)
|
||||
.nullable(),
|
||||
});
|
||||
1
modules/core/src/common/schemas/index.ts
Normal file
1
modules/core/src/common/schemas/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./core.schemas";
|
||||
@ -6,7 +6,8 @@
|
||||
"exports": {
|
||||
".": "./src/common/index.ts",
|
||||
"./api": "./src/api/index.ts",
|
||||
"./client": "./src/web/manifest.ts"
|
||||
"./client": "./src/web/manifest.ts",
|
||||
"./globals.css": "./src/web/globals.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"dinero.js": "^1.9.1"
|
||||
@ -22,7 +23,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-grid-community/locale": "34.0.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@repo/rdx-criteria": "workspace:*",
|
||||
"@repo/rdx-ddd": "workspace:*",
|
||||
@ -30,6 +35,7 @@
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"ag-grid-community": "^33.3.0",
|
||||
"ag-grid-react": "^33.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
@ -43,6 +49,9 @@
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"zod": "^3.25.67"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const CreateCustomerInvoiceCommandSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
status: z.string(),
|
||||
id: z.uuid(),
|
||||
invoice_status: z.string(),
|
||||
invoice_number: z.string().min(1, "Customer invoice number is required"),
|
||||
invoice_series: z.string().min(1, "Customer invoice series is required"),
|
||||
issue_date: z.string().datetime({ offset: true, message: "Invalid issue date format" }),
|
||||
operation_date: z.string().datetime({ offset: true, message: "Invalid operation date format" }),
|
||||
language_code: z.string().min(2, "Language code must be at least 2 characters long"),
|
||||
currency: z.string().min(3, "Currency code must be at least 3 characters long"),
|
||||
currency_code: z.string().min(3, "Currency code must be at least 3 characters long"),
|
||||
items: z.array(
|
||||
z.object({
|
||||
description: z.string().min(1, "Item description is required"),
|
||||
@ -16,10 +16,12 @@ export const CreateCustomerInvoiceCommandSchema = z.object({
|
||||
amount: z.number().positive("Quantity amount must be positive"),
|
||||
scale: z.number().int().nonnegative("Quantity scale must be a non-negative integer"),
|
||||
}),
|
||||
unitPrice: z.object({
|
||||
unit_price: z.object({
|
||||
amount: z.number().positive("Unit price amount must be positive"),
|
||||
scale: z.number().int().nonnegative("Unit price scale must be a non-negative integer"),
|
||||
currency: z.string().min(3, "Unit price currency code must be at least 3 characters long"),
|
||||
currency_code: z
|
||||
.string()
|
||||
.min(3, "Unit price currency code must be at least 3 characters long"),
|
||||
}),
|
||||
discount: z.object({
|
||||
amount: z.number().nonnegative("Discount amount cannot be negative"),
|
||||
|
||||
@ -1,14 +1,23 @@
|
||||
{
|
||||
"customerInvoices": {
|
||||
"common": {},
|
||||
"pages": {
|
||||
"title": "Customer invoices",
|
||||
"description": "Manage your customer invoices",
|
||||
"list": {
|
||||
"title": "Customer invoice list",
|
||||
"description": "List all customer invoices"
|
||||
"description": "List all customer invoices",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Inv. number",
|
||||
"invoice_series": "Serie",
|
||||
"invoice_status": "Status",
|
||||
"issue_date": "Date",
|
||||
"total_price": "Total price"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "Create customer invoice",
|
||||
"description": "Create a new customer invoice"
|
||||
"title": "New customer invoice",
|
||||
"description": "Create a new customer invoice",
|
||||
"back_to_list": "Back to the list"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit customer invoice",
|
||||
@ -22,5 +31,51 @@
|
||||
"title": "View customer invoice",
|
||||
"description": "View the details of the selected customer invoice"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
"emitted": "Emitted",
|
||||
"sent": "Sent",
|
||||
"received": "Received",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"form_fields": {
|
||||
"invoice_number": {
|
||||
"label": "Invoice number",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"items": {
|
||||
"quantity": {
|
||||
"label": "Quantity",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"label": "Description",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"unit_price": {
|
||||
"label": "Unit price",
|
||||
"placeholder": "",
|
||||
"description": "Item unit price"
|
||||
},
|
||||
"subtotal_price": {
|
||||
"label": "Subtotal",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"discount": {
|
||||
"label": "Dto (%)",
|
||||
"placeholder": "",
|
||||
"description": "Percentage discount"
|
||||
},
|
||||
"total_price": {
|
||||
"label": "Total price",
|
||||
"placeholder": "",
|
||||
"description": "Total price with percentage discount"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,23 @@
|
||||
{
|
||||
"customerInvoices": {
|
||||
"common": {},
|
||||
"pages": {
|
||||
"title": "Facturas",
|
||||
"description": "Gestiona tus facturas",
|
||||
"list": {
|
||||
"title": "Lista de facturas",
|
||||
"description": "Lista todas las facturas"
|
||||
"description": "Lista todas las facturas",
|
||||
"grid_columns": {
|
||||
"invoice_number": "Num. factura",
|
||||
"invoice_series": "Serie",
|
||||
"invoice_status": "Estado",
|
||||
"issue_date": "Fecha",
|
||||
"total_price": "Imp. total"
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"title": "Crear factura",
|
||||
"description": "Crear una nueva factura"
|
||||
"description": "Crear una nueva factura",
|
||||
"back_to_list": "Volver a la lista"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Editar factura",
|
||||
@ -22,5 +31,51 @@
|
||||
"title": "Ver factura",
|
||||
"description": "Ver los detalles de la factura seleccionada"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"draft": "Borrador",
|
||||
"emitted": "Emitida",
|
||||
"sent": "Enviada",
|
||||
"received": "Recibida",
|
||||
"rejected": "Rechazada"
|
||||
},
|
||||
"form_fields": {
|
||||
"invoice_number": {
|
||||
"label": "Num. factura",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"items": {
|
||||
"quantity": {
|
||||
"label": "Cantidad",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"label": "Descripción",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"unit_price": {
|
||||
"label": "Imp. unitario",
|
||||
"placeholder": "",
|
||||
"description": "Importe unitario del artículo"
|
||||
},
|
||||
"subtotal_price": {
|
||||
"label": "Subtotal",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"discount": {
|
||||
"label": "Dto (%)",
|
||||
"placeholder": "",
|
||||
"description": "Porcentaje de descuento"
|
||||
},
|
||||
"total_price": {
|
||||
"label": "Imp. total",
|
||||
"placeholder": "",
|
||||
"description": "Importe total con el descuento ya aplicado"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { t } from "i18next";
|
||||
import { PackagePlusIcon } from "lucide-react";
|
||||
import { JSX, forwardRef } from "react";
|
||||
|
||||
export interface AppendBlockRowButtonProps extends React.ComponentProps<typeof Button> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const AppendBlockRowButton = forwardRef<HTMLButtonElement, AppendBlockRowButtonProps>(
|
||||
(
|
||||
{ label = t("common.append_block"), className, ...props }: AppendBlockRowButtonProps,
|
||||
ref
|
||||
): JSX.Element => (
|
||||
<Button type='button' variant='outline' ref={ref} {...props}>
|
||||
{" "}
|
||||
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||
{label && <>{label}</>}
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
|
||||
AppendBlockRowButton.displayName = "AppendBlockRowButton";
|
||||
@ -0,0 +1,26 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { t } from "i18next";
|
||||
import { PackagePlusIcon } from "lucide-react";
|
||||
import { JSX, forwardRef } from "react";
|
||||
|
||||
export interface AppendCatalogArticleRowButtonProps extends React.ComponentProps<typeof Button> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const AppendCatalogArticleRowButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
AppendCatalogArticleRowButtonProps
|
||||
>(
|
||||
(
|
||||
{ label = t("common.append_article"), className, ...props }: AppendCatalogArticleRowButtonProps,
|
||||
ref
|
||||
): JSX.Element => (
|
||||
<Button type='button' variant='outline' ref={ref} {...props}>
|
||||
{" "}
|
||||
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||
{label && <>{label}</>}
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
|
||||
AppendCatalogArticleRowButton.displayName = "AppendCatalogArticleRowButton";
|
||||
@ -0,0 +1,23 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { t } from "i18next";
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import { JSX, forwardRef } from "react";
|
||||
|
||||
export interface AppendEmptyRowButtonProps extends React.ComponentProps<typeof Button> {
|
||||
label?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AppendEmptyRowButton = forwardRef<HTMLButtonElement, AppendEmptyRowButtonProps>(
|
||||
(
|
||||
{ label = t("common.append_empty_row"), className, ...props }: AppendEmptyRowButtonProps,
|
||||
ref
|
||||
): JSX.Element => (
|
||||
<Button type='button' variant='outline' ref={ref} {...props}>
|
||||
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||
{label && <>{label}</>}
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
|
||||
AppendEmptyRowButton.displayName = "AppendEmptyRowButton";
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./append-block-row-button";
|
||||
export * from "./append-catalog-article-row-button";
|
||||
export * from "./append-empty-row-button";
|
||||
@ -0,0 +1,66 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MODULE_NAME } from "../manifest";
|
||||
|
||||
export type CustomerInvoiceStatus = "draft" | "emitted" | "sent" | "received" | "rejected";
|
||||
|
||||
export type CustomerInvoiceStatusBadgeProps = {
|
||||
status: string; // permitir cualquier valor
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const statusColorConfig: Record<CustomerInvoiceStatus, { badge: string; dot: string }> = {
|
||||
draft: {
|
||||
badge:
|
||||
"bg-gray-600/10 dark:bg-gray-600/20 hover:bg-gray-600/10 text-gray-500 border-gray-600/60",
|
||||
dot: "bg-gray-500",
|
||||
},
|
||||
emitted: {
|
||||
badge:
|
||||
"bg-amber-600/10 dark:bg-amber-600/20 hover:bg-amber-600/10 text-amber-500 border-amber-600/60",
|
||||
dot: "bg-amber-500",
|
||||
},
|
||||
sent: {
|
||||
badge:
|
||||
"bg-cyan-600/10 dark:bg-cyan-600/20 hover:bg-cyan-600/10 text-cyan-500 border-cyan-600/60 shadow-none rounded-full",
|
||||
dot: "bg-cyan-500",
|
||||
},
|
||||
received: {
|
||||
badge:
|
||||
"bg-emerald-600/10 dark:bg-emerald-600/20 hover:bg-emerald-600/10 text-emerald-500 border-emerald-600/60",
|
||||
dot: "bg-emerald-500",
|
||||
},
|
||||
rejected: {
|
||||
badge: "bg-red-600/10 dark:bg-red-600/20 hover:bg-red-600/10 text-red-500 border-red-600/60",
|
||||
dot: "bg-red-500",
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomerInvoiceStatusBadge = forwardRef<
|
||||
HTMLDivElement,
|
||||
CustomerInvoiceStatusBadgeProps
|
||||
>(({ status, className, ...props }, ref) => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const normalizedStatus = status.toLowerCase() as CustomerInvoiceStatus;
|
||||
const config = statusColorConfig[normalizedStatus];
|
||||
const commonClassName = "transition-colors duration-200 cursor-pointer shadow-none rounded-full";
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<Badge ref={ref} className={cn(commonClassName, className)} {...props}>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={cn(commonClassName, config.badge, className)} {...props}>
|
||||
<div className={cn("h-1.5 w-1.5 rounded-full mr-2", config.dot)} />
|
||||
{t(`status.${status}`)}
|
||||
</Badge>
|
||||
);
|
||||
});
|
||||
|
||||
CustomerInvoiceStatusBadge.displayName = "CustomerInvoiceStatusBadge";
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale";
|
||||
// Grid
|
||||
@ -11,75 +11,40 @@ import { MoneyDTO } from "@erp/core";
|
||||
import { formatDate, formatMoney } from "@erp/core/client";
|
||||
// Core CSS
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCustomerInvoicesQuery } from "../hooks";
|
||||
|
||||
/**
|
||||
* Fetch example Json data
|
||||
* Not recommended for production use!
|
||||
*/
|
||||
export const useFetchJson = <T,>(url: string, limit?: number) => {
|
||||
const [data, setData] = useState<T[]>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
|
||||
// Note error handling is omitted here for brevity
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
const data = limit ? json.slice(0, limit) : json;
|
||||
setData(data);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchData();
|
||||
}, [url, limit]);
|
||||
return { data, loading };
|
||||
};
|
||||
|
||||
// Row Data Interface
|
||||
interface IRow {
|
||||
mission: string;
|
||||
company: string;
|
||||
location: string;
|
||||
date: string;
|
||||
time: string;
|
||||
rocket: string;
|
||||
price: number;
|
||||
successful: boolean;
|
||||
}
|
||||
import { MODULE_NAME } from "../manifest";
|
||||
import { CustomerInvoiceStatusBadge } from "./customer-invoice-status-badge";
|
||||
|
||||
// Create new GridExample component
|
||||
export const CustomerInvoicesGrid = () => {
|
||||
//const { useList } = useCustomerInvoices();
|
||||
|
||||
export const CustomerInvoicesListGrid = () => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
const { data, isLoading, isPending, isError, error } = useCustomerInvoicesQuery({});
|
||||
|
||||
// Column Definitions: Defines & controls grid columns.
|
||||
const [colDefs] = useState<ColDef[]>([
|
||||
{ field: "invoice_number", headerName: "Num. factura" },
|
||||
{ field: "invoice_series", headerName: "Serie" },
|
||||
{
|
||||
field: "invoice_status",
|
||||
filter: true,
|
||||
headerName: "Estado",
|
||||
headerName: t("pages.list.grid_columns.invoice_status"),
|
||||
cellRenderer: (params: ValueFormatterParams) => {
|
||||
return <CustomerInvoiceStatusBadge status={params.value} />;
|
||||
},
|
||||
},
|
||||
|
||||
{ field: "invoice_number", headerName: t("pages.list.grid_columns.invoice_number") },
|
||||
{ field: "invoice_series", headerName: t("pages.list.grid_columns.invoice_series") },
|
||||
|
||||
{
|
||||
field: "issue_date",
|
||||
headerName: "Fecha fact.",
|
||||
headerName: t("pages.list.grid_columns.issue_date"),
|
||||
valueFormatter: (params: ValueFormatterParams) => {
|
||||
return formatDate(params.value);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "subtotal_price",
|
||||
valueFormatter: (params: ValueFormatterParams) => {
|
||||
const rawValue: MoneyDTO = params.value;
|
||||
return formatMoney(rawValue);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "total_price",
|
||||
headerName: t("pages.list.grid_columns.total_price"),
|
||||
valueFormatter: (params: ValueFormatterParams) => {
|
||||
const rawValue: MoneyDTO = params.value;
|
||||
return formatMoney(rawValue);
|
||||
@ -97,18 +62,9 @@ export const CustomerInvoicesGrid = () => {
|
||||
sortable: false,
|
||||
resizable: true,
|
||||
},
|
||||
sideBar: true,
|
||||
statusBar: {
|
||||
statusPanels: [
|
||||
{ statusPanel: "agTotalAndFilteredRowCountComponent", align: "left" },
|
||||
{ statusPanel: "agAggregationComponent" },
|
||||
],
|
||||
},
|
||||
rowGroupPanelShow: "always",
|
||||
pagination: true,
|
||||
paginationPageSize: 10,
|
||||
paginationPageSizeSelector: [10, 20, 30, 50],
|
||||
enableCharts: true,
|
||||
localeText: AG_GRID_LOCALE_ES,
|
||||
rowSelection: { mode: "multiRow" },
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./customer-invoices-grid";
|
||||
export * from "./customer-invoice-status-badge";
|
||||
export * from "./customer-invoices-layout";
|
||||
export * from "./customer-invoices-list-grid";
|
||||
|
||||
@ -0,0 +1,343 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
Input,
|
||||
Textarea,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { formatCurrency } from "../../pages/create/utils";
|
||||
import {
|
||||
CustomerInvoiceItemsSortableDataTable,
|
||||
RowIdData,
|
||||
} from "./customer-invoice-items-sortable-datatable";
|
||||
|
||||
export const CustomerInvoiceItemsCardEditor = ({
|
||||
//currency,
|
||||
//language,
|
||||
defaultValues,
|
||||
}: {
|
||||
//currency: CurrencyData;
|
||||
//language: Language;
|
||||
defaultValues: Readonly<{ [x: string]: any }> | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
|
||||
const { control, watch, getValues } = useFormContext();
|
||||
|
||||
const watchedItems = watch("items");
|
||||
|
||||
//const [pickerMode] = useState<"dialog" | "panel">("dialog");
|
||||
|
||||
//const [articlePickerDialogOpen, setArticlePickerDialogOpen] = useState<boolean>(false);
|
||||
//const [blockPickerDialogOpen, setBlockPickerDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const { fields, ...fieldActions } = useFieldArray({
|
||||
control,
|
||||
name: "items",
|
||||
});
|
||||
|
||||
const columns: ColumnDef<RowIdData, unknown>[] = useDetailColumns(
|
||||
[
|
||||
/*{
|
||||
id: "row_id" as const,
|
||||
header: () => (
|
||||
<HashIcon aria-label='Orden de fila' className='items-center justify-center w-4 h-4' />
|
||||
),
|
||||
accessorFn: (_: unknown, index: number) => index + 1,
|
||||
size: 5,
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
},*/
|
||||
/*{
|
||||
id: "id_article" as const,
|
||||
accessorKey: "id_article",
|
||||
header: "artículo",
|
||||
cell: ({ row: { index, original } }) => {
|
||||
return (
|
||||
<FormTextAreaField
|
||||
readOnly={original?.id_article}
|
||||
autoSize
|
||||
{...register(`items.${index}.id_article`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
size: 500,
|
||||
},*/
|
||||
{
|
||||
id: "description" as const,
|
||||
accessorKey: "description",
|
||||
header: t("customer_invoices.form_fields.description.label"),
|
||||
cell: ({ row: { index, original } }) => (
|
||||
<FormField
|
||||
control={control}
|
||||
name={`items.${index}.description`}
|
||||
render={({ field }) => (
|
||||
<FormItem className='md:col-span-2'>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t("customer_invoices.form_fields.description.placeholder")}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
minSize: 200,
|
||||
size: 400,
|
||||
},
|
||||
{
|
||||
id: "quantity" as const,
|
||||
accessorKey: "quantity",
|
||||
header: () => (
|
||||
<div className='text-right'>{t("customer_invoices.form_fields.quantity.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => (
|
||||
<FormField
|
||||
control={control}
|
||||
name={`items.${index}.quantity.amount`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='number'
|
||||
step='0.01'
|
||||
min='0'
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
||||
value={field.value / 100}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
size: 75,
|
||||
},
|
||||
{
|
||||
id: "unit_price" as const,
|
||||
accessorKey: "unit_price",
|
||||
header: () => (
|
||||
<div className='text-right'>
|
||||
{t("customer_invoices.form_fields.items.unit_price.label")}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => (
|
||||
<FormField
|
||||
control={control}
|
||||
name={`items.${index}.unit_price.amount`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='number'
|
||||
step='0.01'
|
||||
min='0'
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
||||
value={field.value / 100}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
size: 125,
|
||||
},
|
||||
{
|
||||
id: "subtotal_price" as const,
|
||||
accessorKey: "subtotal_price",
|
||||
header: () => (
|
||||
<div className='text-right'>
|
||||
{t("customer_invoices.form_fields.items.subtotal_price.label")}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => {
|
||||
/*return (
|
||||
<FormCurrencyField
|
||||
variant='ghost'
|
||||
currency={currency}
|
||||
language={language}
|
||||
scale={2}
|
||||
readOnly
|
||||
className='text-right'
|
||||
{...register(`items.${index}.subtotal_price`)}
|
||||
/>
|
||||
);*/
|
||||
return null;
|
||||
},
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
id: "discount" as const,
|
||||
accessorKey: "discount",
|
||||
header: () => (
|
||||
<div className='text-right'>
|
||||
{t("customer_invoices.form_fields.items.discount.label")}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => (
|
||||
<FormField
|
||||
control={control}
|
||||
name={`items.${index}.discount.amount`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='number'
|
||||
step='0.01'
|
||||
min='0'
|
||||
max='100'
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value) * 100)}
|
||||
value={field.value / 100}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
id: "total_price" as const,
|
||||
accessorKey: "total_price",
|
||||
header: () => (
|
||||
<div className='text-right'>
|
||||
{t("customer_invoices.form_fields.items.total_price.label")}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row: { index } }) => (
|
||||
<>
|
||||
{formatCurrency(
|
||||
watchedItems[index]?.total_price?.amount || 0,
|
||||
2,
|
||||
getValues("currency")
|
||||
)}
|
||||
</>
|
||||
),
|
||||
size: 150,
|
||||
},
|
||||
],
|
||||
{
|
||||
enableDragHandleColumn: true,
|
||||
enableSelectionColumn: true,
|
||||
enableActionsColumn: true,
|
||||
rowActionFn: (props) => {
|
||||
const { table, row } = props;
|
||||
return [
|
||||
{
|
||||
label: t("common.duplicate_row"),
|
||||
icon: <CopyIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.duplicateItems(row.index),
|
||||
},
|
||||
{
|
||||
label: t("common.insert_row_above"),
|
||||
icon: <ChevronUpIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.insertItem(row.index),
|
||||
},
|
||||
{
|
||||
label: t("common.insert_row_below"),
|
||||
icon: <ChevronDownIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.insertItem(row.index + 1),
|
||||
},
|
||||
|
||||
{
|
||||
label: "-",
|
||||
},
|
||||
{
|
||||
label: t("common.remove_row"),
|
||||
//shortcut: "⌘⌫",
|
||||
icon: <Trash2Icon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => {
|
||||
table.options.meta?.deleteItems(row.index);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/*const handleAppendCatalogArticle = useCallback(
|
||||
(article: any, quantity = 1) => {
|
||||
fieldActions.append({
|
||||
...article,
|
||||
quantity: {
|
||||
amount: 100 * quantity,
|
||||
scale: Quantity.DEFAULT_SCALE,
|
||||
},
|
||||
unit_price: article.retail_price,
|
||||
discount: {
|
||||
amount: null,
|
||||
scale: 2,
|
||||
},
|
||||
});
|
||||
toast({
|
||||
title: t("quotes.catalog_picker_dialog.toast_article_added"),
|
||||
description: article.description,
|
||||
});
|
||||
},
|
||||
[fieldActions, toast]
|
||||
);
|
||||
|
||||
const handleAppendBlock = useCallback(
|
||||
(block: any) => {
|
||||
fieldActions.append({
|
||||
description: `${block.title}\n${block.body}`,
|
||||
quantity: {
|
||||
amount: null,
|
||||
scale: Quantity.DEFAULT_SCALE,
|
||||
},
|
||||
unit_price: {
|
||||
amount: null,
|
||||
scale: UnitPrice.DEFAULT_SCALE,
|
||||
},
|
||||
discount: {
|
||||
amount: null,
|
||||
scale: 2,
|
||||
},
|
||||
});
|
||||
toast({
|
||||
title: t("quotes.blocks_picker_dialog.toast_article_added"),
|
||||
description: block.title,
|
||||
});
|
||||
},
|
||||
[fieldActions]
|
||||
);*/
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
const defaultLayout = [265, 440, 655];
|
||||
const navCollapsedSize = 4;
|
||||
|
||||
console.log(columns);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<CustomerInvoiceItemsSortableDataTable
|
||||
actions={{
|
||||
...fieldActions,
|
||||
//pickCatalogArticle: () => setArticlePickerDialogOpen(true),
|
||||
//pickBlock: () => setBlockPickerDialogOpen(true),
|
||||
}}
|
||||
columns={columns}
|
||||
data={fields}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,117 @@
|
||||
import {
|
||||
Button,
|
||||
Separator,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { Table } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { CopyPlusIcon, ScanIcon, Trash2Icon } from "lucide-react";
|
||||
import {
|
||||
AppendBlockRowButton,
|
||||
AppendCatalogArticleRowButton,
|
||||
AppendEmptyRowButton,
|
||||
} from "../buttons";
|
||||
|
||||
export const CustomerInvoiceItemsSortableDataTableToolbar = ({ table }: { table: Table<any> }) => {
|
||||
const selectedRowsCount = table.getSelectedRowModel().rows.length;
|
||||
|
||||
if (selectedRowsCount) {
|
||||
return (
|
||||
<nav className='flex items-center h-12 p-1 rounded-md text-muted-foreground bg-muted '>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type='button'
|
||||
variant='link'
|
||||
disabled={!table.getSelectedRowModel().rows.length}
|
||||
onClick={() => table.options.meta?.duplicateItems()}
|
||||
>
|
||||
<CopyPlusIcon className='w-4 h-4 sm:mr-2' />
|
||||
<span className='sr-only sm:not-sr-only'>
|
||||
{t("common.duplicate_selected_rows")}
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.duplicate_selected_rows_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type='button'
|
||||
variant='link'
|
||||
disabled={!table.getSelectedRowModel().rows.length}
|
||||
onClick={() => table.options.meta?.deleteItems()}
|
||||
>
|
||||
<Trash2Icon className='w-4 h-4 sm:mr-2' />
|
||||
<span className='sr-only sm:not-sr-only'>{t("common.remove_selected_rows")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.remove_selected_rows_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type='button'
|
||||
variant='link'
|
||||
disabled={!table.getSelectedRowModel().rows.length}
|
||||
onClick={() => table.resetRowSelection()}
|
||||
>
|
||||
<ScanIcon className='w-4 h-4 sm:mr-2' />
|
||||
<span className='sr-only sm:not-sr-only'>{t("common.reset_selected_rows")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.reset_selected_rows_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation='vertical' className='h-6 ml-1 mr-4' />
|
||||
<p className='text-sm font-normal'>
|
||||
{t("common.rows_selected", { count: selectedRowsCount })}
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className='flex items-center h-12 p-1 rounded-md bg-accent/75 text-muted-foreground'>
|
||||
<div className='flex space-x-2'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AppendEmptyRowButton variant='link' onClick={() => table.options.meta?.appendItem()} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.append_empty_row_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AppendCatalogArticleRowButton
|
||||
variant='link'
|
||||
onClick={() => {
|
||||
if (table.options.meta && table.options.meta.pickCatalogArticle) {
|
||||
table.options.meta?.pickCatalogArticle();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<AppendBlockRowButton
|
||||
variant='link'
|
||||
onClick={() => {
|
||||
if (table.options.meta && table.options.meta.pickBlock) {
|
||||
table.options.meta?.pickBlock();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.append_block_tooltip")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 ml-auto' />
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,531 @@
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
DropAnimation,
|
||||
KeyboardSensor,
|
||||
MeasuringStrategy,
|
||||
MouseSensor,
|
||||
PointerSensor,
|
||||
TouchSensor,
|
||||
UniqueIdentifier,
|
||||
closestCenter,
|
||||
defaultDropAnimation,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { ButtonGroup, DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@repo/shadcn-ui/components/table";
|
||||
import {
|
||||
ColumnDef,
|
||||
InitialTableState,
|
||||
Row,
|
||||
RowData,
|
||||
RowSelectionState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
|
||||
import {
|
||||
AppendBlockRowButton,
|
||||
AppendCatalogArticleRowButton,
|
||||
AppendEmptyRowButton,
|
||||
} from "../buttons";
|
||||
import { CustomerInvoiceItemsSortableDataTableToolbar } from "./customer-invoice-items-sortable-datatable-toolbar";
|
||||
import { CustomerInvoiceItemsSortableTableRow } from "./customer-invoice-items-sortable-table-row";
|
||||
|
||||
export type RowIdData = { [x: string]: any };
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface TableMeta<TData extends RowData> {
|
||||
insertItem: (rowIndex: number, data?: unknown) => void;
|
||||
appendItem: (data?: unknown) => void;
|
||||
pickCatalogArticle?: () => void;
|
||||
pickBlock?: () => void;
|
||||
duplicateItems: (rowIndex?: number) => void;
|
||||
deleteItems: (rowIndex?: number | number[]) => void;
|
||||
updateItem: (
|
||||
rowIndex: number,
|
||||
rowData: TData & RowIdData,
|
||||
fieldName: string,
|
||||
value: unknown
|
||||
) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CustomerInvoiceItemsSortableProps {
|
||||
id: UniqueIdentifier;
|
||||
}
|
||||
|
||||
export type CustomerInvoiceItemsSortableDataTableProps<
|
||||
TData extends RowData & RowIdData,
|
||||
TValue = unknown,
|
||||
> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
defaultValues: Readonly<{ [x: string]: any }> | undefined;
|
||||
initialState?: InitialTableState;
|
||||
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & {
|
||||
pickCatalogArticle?: () => void;
|
||||
pickBlock?: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
const measuringConfig = {
|
||||
droppable: {
|
||||
strategy: MeasuringStrategy.Always,
|
||||
},
|
||||
};
|
||||
|
||||
const dropAnimationConfig: DropAnimation = {
|
||||
keyframes({ transform }) {
|
||||
return [
|
||||
{ opacity: 1, transform: CSS.Transform.toString(transform.initial) },
|
||||
{
|
||||
opacity: 0,
|
||||
transform: CSS.Transform.toString({
|
||||
...transform.final,
|
||||
x: transform.final.x + 5,
|
||||
y: transform.final.y + 5,
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
easing: "ease-out",
|
||||
sideEffects({ active }) {
|
||||
active.node.animate([{ opacity: 0 }, { opacity: 1 }], {
|
||||
duration: defaultDropAnimation.duration,
|
||||
easing: defaultDropAnimation.easing,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export function CustomerInvoiceItemsSortableDataTable<
|
||||
TData extends RowData & RowIdData,
|
||||
TValue = unknown,
|
||||
>({
|
||||
columns,
|
||||
data,
|
||||
defaultValues,
|
||||
initialState,
|
||||
actions,
|
||||
}: CustomerInvoiceItemsSortableDataTableProps<TData, TValue>) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||
|
||||
const [columnVisibility, _setColumnVisibility] = useState<VisibilityState>(
|
||||
initialState?.columnVisibility || {}
|
||||
);
|
||||
|
||||
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||
|
||||
const table = useReactTable<TData>({
|
||||
data,
|
||||
columns,
|
||||
enableColumnResizing: false,
|
||||
columnResizeMode: "onChange",
|
||||
//defaultColumn,
|
||||
|
||||
autoResetAll: false, // <-- añadido nuevo
|
||||
initialState,
|
||||
state: {
|
||||
rowSelection,
|
||||
columnVisibility,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
enableSorting: false,
|
||||
enableHiding: true,
|
||||
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (originalRow) => originalRow?.id,
|
||||
|
||||
debugTable: false,
|
||||
debugHeaders: false,
|
||||
debugColumns: false,
|
||||
|
||||
defaultColumn: {
|
||||
minSize: 0, //starting column size
|
||||
size: Number.MAX_SAFE_INTEGER, //enforced during column resizing
|
||||
maxSize: Number.MAX_SAFE_INTEGER, //enforced during column resizing
|
||||
},
|
||||
|
||||
meta: {
|
||||
insertItem: (rowIndex: number, data?: unknown) => {
|
||||
actions.insert(rowIndex, data || defaultValues?.items[0], { shouldFocus: true });
|
||||
},
|
||||
appendItem: (data?: unknown) => {
|
||||
actions.append(data || defaultValues?.items[0], { shouldFocus: true });
|
||||
},
|
||||
pickCatalogArticle: () => {
|
||||
if (actions.pickCatalogArticle) {
|
||||
actions?.pickCatalogArticle();
|
||||
}
|
||||
},
|
||||
pickBlock: () => {
|
||||
if (actions.pickBlock) {
|
||||
actions?.pickBlock();
|
||||
}
|
||||
},
|
||||
duplicateItems: (rowIndex?: number) => {
|
||||
if (rowIndex !== undefined) {
|
||||
const originalData = table.getRowModel().rows[rowIndex].original;
|
||||
actions.insert(rowIndex + 1, originalData, { shouldFocus: true });
|
||||
} else if (table.getSelectedRowModel().rows.length) {
|
||||
const lastIndex =
|
||||
table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index;
|
||||
|
||||
const data = table
|
||||
.getSelectedRowModel()
|
||||
.rows.map((row: Row<any>) => ({ ...row.original, id: undefined }));
|
||||
|
||||
if (table.getRowModel().rows.length < lastIndex + 1) {
|
||||
actions.append(data);
|
||||
} else {
|
||||
actions.insert(lastIndex + 1, data, { shouldFocus: true });
|
||||
}
|
||||
table.resetRowSelection();
|
||||
}
|
||||
},
|
||||
deleteItems: (rowIndex?: number | number[]) => {
|
||||
if (rowIndex !== undefined) {
|
||||
actions.remove(rowIndex);
|
||||
} else if (table.getSelectedRowModel().rows.length > 0) {
|
||||
let start = table.getSelectedRowModel().rows.length - 1;
|
||||
for (; start >= 0; start--) {
|
||||
const oldIndex = sorteableRowIds.indexOf(
|
||||
String(table.getSelectedRowModel().rows[start].id)
|
||||
);
|
||||
actions.remove(oldIndex);
|
||||
sorteableRowIds.splice(oldIndex, 1);
|
||||
}
|
||||
|
||||
/*table.getSelectedRowModel().rows.forEach((row) => {
|
||||
});*/
|
||||
table.resetRowSelection();
|
||||
} else {
|
||||
actions.remove();
|
||||
}
|
||||
},
|
||||
updateItem: (rowIndex: number, rowData: any, fieldName: string, value: unknown) => {
|
||||
// Skip page index reset until after next rerender
|
||||
// skipAutoResetPageIndex();
|
||||
actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {}),
|
||||
useSensor(PointerSensor, {})
|
||||
);
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
let activeId = event.active.id;
|
||||
let overId = event.over?.id;
|
||||
|
||||
if (overId !== undefined && activeId !== overId) {
|
||||
let newIndex = sorteableRowIds.indexOf(String(overId));
|
||||
|
||||
if (table.getSelectedRowModel().rows.length > 1) {
|
||||
table.getSelectedRowModel().rows.forEach((row, index) => {
|
||||
const oldIndex = sorteableRowIds.indexOf(String(row.id));
|
||||
|
||||
if (index > 0) {
|
||||
activeId = row.id;
|
||||
newIndex = sorteableRowIds.indexOf(String(overId));
|
||||
if (newIndex < oldIndex) {
|
||||
newIndex = newIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
actions.move(oldIndex, newIndex);
|
||||
sorteableRowIds.splice(newIndex, 0, sorteableRowIds.splice(oldIndex, 1)[0]);
|
||||
|
||||
overId = row.id;
|
||||
});
|
||||
} else {
|
||||
const oldIndex = sorteableRowIds.indexOf(String(activeId));
|
||||
actions.move(oldIndex, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveId(null);
|
||||
}
|
||||
|
||||
function handleDragStart({ active }: DragStartEvent) {
|
||||
if (!table.getSelectedRowModel().rowsById[active.id]) {
|
||||
table.resetRowSelection();
|
||||
}
|
||||
|
||||
setActiveId(active.id);
|
||||
}
|
||||
|
||||
function handleDragCancel() {
|
||||
setActiveId(null);
|
||||
}
|
||||
|
||||
function filterItems(items: string[] | Row<unknown>[]) {
|
||||
if (!activeId) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter((idOrRow) => {
|
||||
const id = typeof idOrRow === "string" ? idOrRow : idOrRow.id;
|
||||
return id === activeId || !table.getSelectedRowModel().rowsById[id];
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
measuring={measuringConfig}
|
||||
sensors={sensors}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
onDragCancel={handleDragCancel}
|
||||
collisionDetection={closestCenter}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className='sticky z-10 top-16 bg-card/90'>
|
||||
<CardTitle>
|
||||
<CustomerInvoiceItemsSortableDataTableToolbar table={table} />
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table className='table-fixed'>
|
||||
<TableHeader className='sticky top-0 z-10 bg-background'>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className='hover:bg-transparent'>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className='px-2 py-1'
|
||||
style={{
|
||||
width:
|
||||
header.getSize() === Number.MAX_SAFE_INTEGER
|
||||
? "auto"
|
||||
: header.getSize(),
|
||||
}}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<DataTableColumnHeader table={table} header={header} />
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<SortableContext
|
||||
items={filterItems(sorteableRowIds)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{filterItems(table.getRowModel().rows).map((row) => (
|
||||
<CustomerInvoiceItemsSortableTableRow
|
||||
key={(row as Row<any>).id}
|
||||
id={(row as Row<any>).id}
|
||||
>
|
||||
{(row as Row<any>).getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className='px-2 py-2 align-top'
|
||||
style={{
|
||||
width:
|
||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
||||
? "auto"
|
||||
: cell.column.getSize(),
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</CustomerInvoiceItemsSortableTableRow>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{createPortal(
|
||||
<DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}>
|
||||
{activeId && (
|
||||
<div className='relative flex flex-wrap'>
|
||||
{table.getSelectedRowModel().rows.length ? (
|
||||
<Badge
|
||||
variant='destructive'
|
||||
className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2'
|
||||
>
|
||||
{table.getSelectedRowModel().rows.length}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{false &&
|
||||
createPortal(
|
||||
<DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}>
|
||||
{activeId && (
|
||||
<div className='relative flex flex-wrap'>
|
||||
{table.getSelectedRowModel().rows.length ? (
|
||||
<Badge
|
||||
variant='destructive'
|
||||
className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2'
|
||||
>
|
||||
{table.getSelectedRowModel().rows.length}
|
||||
</Badge>
|
||||
) : null}
|
||||
<div className='absolute z-40 bg-white border rounded shadow opacity-100 top left hover:bg-white border-muted-foreground/50'>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map(
|
||||
(row) =>
|
||||
row.id === activeId && (
|
||||
<TableRow key={row.id} id={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
className='p-1 align-top'
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{table.getSelectedRowModel().rows.length > 1 && (
|
||||
<div className='absolute z-30 transform -translate-x-1 translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-1'>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map(
|
||||
(row) =>
|
||||
row.id === activeId && (
|
||||
<TableRow key={row.id} id={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
className='p-1 align-top'
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{table.getSelectedRowModel().rows.length > 2 && (
|
||||
<div className='absolute z-20 transform translate-x-1 -translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left -rotate-1'>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map(
|
||||
(row) =>
|
||||
row.id === activeId && (
|
||||
<TableRow key={row.id} id={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
className='p-1 align-top'
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{table.getSelectedRowModel().rows.length > 3 && (
|
||||
<div className='absolute z-10 transform translate-x-2 -translate-y-2 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-2'>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map(
|
||||
(row) =>
|
||||
row.id === activeId && (
|
||||
<TableRow key={row.id} id={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
className='p-1 align-top'
|
||||
key={cell.id}
|
||||
style={{ width: cell.column.getSize() }}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<ButtonGroup>
|
||||
<AppendEmptyRowButton onClick={() => table.options.meta?.appendItem()} />
|
||||
<AppendCatalogArticleRowButton
|
||||
onClick={() => {
|
||||
if (table.options.meta && table.options.meta.pickCatalogArticle) {
|
||||
table.options.meta?.pickCatalogArticle();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<AppendBlockRowButton
|
||||
onClick={() => {
|
||||
if (table.options.meta && table.options.meta.pickBlock) {
|
||||
table.options.meta?.pickBlock();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import { DraggableSyntheticListeners } from "@dnd-kit/core";
|
||||
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { TableRow } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react";
|
||||
import { CustomerInvoiceItemsSortableProps } from "./customer-invoice-items-sortable-datatable";
|
||||
|
||||
interface Context {
|
||||
attributes: Record<string, any>;
|
||||
listeners: DraggableSyntheticListeners;
|
||||
ref(node: HTMLElement | null): void;
|
||||
}
|
||||
export const CustomerInvoiceItemsSortableTableRowContext = createContext<Context>({
|
||||
attributes: {},
|
||||
listeners: undefined,
|
||||
ref() {},
|
||||
});
|
||||
|
||||
function animateLayoutChanges(args: any) {
|
||||
if (args.isSorting || args.wasDragging) {
|
||||
return defaultAnimateLayoutChanges(args);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function CustomerInvoiceItemsSortableTableRow({
|
||||
id,
|
||||
children,
|
||||
}: PropsWithChildren<CustomerInvoiceItemsSortableProps>) {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
setActivatorNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({
|
||||
animateLayoutChanges,
|
||||
id,
|
||||
});
|
||||
|
||||
const style: CSSProperties = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
attributes,
|
||||
listeners,
|
||||
ref: setActivatorNodeRef,
|
||||
}),
|
||||
[attributes, listeners, setActivatorNodeRef]
|
||||
);
|
||||
|
||||
return (
|
||||
<CustomerInvoiceItemsSortableTableRowContext.Provider value={context}>
|
||||
<TableRow
|
||||
key={id}
|
||||
id={String(id)}
|
||||
className={cn(
|
||||
isDragging ? "opacity-60" : "opacity-100",
|
||||
"m-0 hover:bg-muted hover:focus-within:bg-accent focus-within:bg-accent"
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</TableRow>
|
||||
</CustomerInvoiceItemsSortableTableRowContext.Provider>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer-invoice-items-card-editor";
|
||||
export * from "./customer-invoice-items-sortable-table-row";
|
||||
4
modules/customer-invoices/src/web/globals.css
Normal file
4
modules/customer-invoices/src/web/globals.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@source "./components";
|
||||
@source "./pages";
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./use-create-customer-invoice-mutation";
|
||||
export * from "./use-customer-invoices-context";
|
||||
export * from "./use-customer-invoices-query";
|
||||
export * from "./use-detail-columns";
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
import {
|
||||
DataTablaRowActionFunction,
|
||||
DataTableRowActions,
|
||||
DataTableRowDragHandleCell,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { Checkbox } from "@repo/shadcn-ui/components";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { useId, useMemo } from "react";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function useDetailColumns<TData>(
|
||||
columns: ReadonlyArray<ColumnDef<TData>>,
|
||||
{
|
||||
enableDragHandleColumn = false,
|
||||
enableSelectionColumn = false,
|
||||
enableActionsColumn = false,
|
||||
rowActionFn,
|
||||
}: {
|
||||
enableDragHandleColumn?: boolean;
|
||||
enableSelectionColumn?: boolean;
|
||||
enableActionsColumn?: boolean;
|
||||
rowActionFn?: DataTablaRowActionFunction<TData>;
|
||||
} = {}
|
||||
): ColumnDef<TData>[] {
|
||||
const idPrefix = useId();
|
||||
|
||||
return useMemo(() => {
|
||||
const baseColumns: ColumnDef<TData>[] = [...columns];
|
||||
|
||||
if (enableDragHandleColumn) {
|
||||
baseColumns.unshift({
|
||||
id: "row_drag_handle",
|
||||
header: () => null,
|
||||
cell: (info) => <DataTableRowDragHandleCell rowId={info.row.id} />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 40,
|
||||
});
|
||||
}
|
||||
|
||||
if (enableSelectionColumn) {
|
||||
baseColumns.unshift({
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
id={`${idPrefix}-select-all`}
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label='Seleccionar todo'
|
||||
className='translate-y-[0px]'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
id={`${idPrefix}-select-row-${row.id}`}
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
onCheckedChange={row.getToggleSelectedHandler()}
|
||||
aria-label='Seleccionar fila'
|
||||
className='mt-2'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 40,
|
||||
});
|
||||
}
|
||||
|
||||
if (enableActionsColumn) {
|
||||
const RowActionsCell = (props: any) => (
|
||||
<DataTableRowActions rowContext={props} actions={rowActionFn} />
|
||||
);
|
||||
|
||||
baseColumns.push({
|
||||
id: "row_actions",
|
||||
header: () => null,
|
||||
cell: RowActionsCell,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 48,
|
||||
});
|
||||
}
|
||||
|
||||
return baseColumns;
|
||||
}, [
|
||||
columns,
|
||||
rowActionFn,
|
||||
idPrefix,
|
||||
enableDragHandleColumn,
|
||||
enableSelectionColumn,
|
||||
enableActionsColumn,
|
||||
]);
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCreateCustomerInvoiceMutation } from "../../hooks";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { InvoiceEditForm } from "./invoice-edit-form";
|
||||
import { CustomerInvoiceEditForm } from "./customer-invoice-edit-form";
|
||||
|
||||
export const CustomerInvoiceCreate = () => {
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
@ -61,13 +61,13 @@ export const CustomerInvoiceCreate = () => {
|
||||
<p className='text-muted-foreground'>{t("customerInvoices.create.description")}</p>
|
||||
</div>
|
||||
<div className='flex items-center justify-end mb-4'>
|
||||
<Button className='btn btn-primary' onClick={() => navigate("/customer-invoices/list")}>
|
||||
{t("customerInvoices.create.back_to_list")}
|
||||
<Button className='cursor-pointer' onClick={() => navigate("/customer-invoices/list")}>
|
||||
{t("pages.create.back_to_list")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full h-full py-4 @container'>
|
||||
<InvoiceEditForm onSubmit={handleSubmit} isPending={isPending} />
|
||||
<CustomerInvoiceEditForm onSubmit={handleSubmit} isPending={isPending} />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
|
||||
@ -2,6 +2,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { ClientSelector } from "@erp/customers/components";
|
||||
|
||||
import { formatDate } from "@erp/core/client";
|
||||
import {
|
||||
Button,
|
||||
Calendar,
|
||||
@ -32,7 +35,10 @@ import {
|
||||
import { format } from "date-fns";
|
||||
import { es } from "date-fns/locale";
|
||||
import { CalendarIcon, PlusIcon, Save, Trash2Icon, X } from "lucide-react";
|
||||
import { InvoiceData } from "./types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CustomerInvoiceItemsCardEditor } from "../../components/items";
|
||||
import { MODULE_NAME } from "../../manifest";
|
||||
import { CustomerInvoiceData } from "./customer-invoice.schema";
|
||||
import { formatCurrency } from "./utils";
|
||||
|
||||
const invoiceSchema = z.object({
|
||||
@ -119,8 +125,8 @@ const invoiceSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const defaultInvoiceData: InvoiceData = {
|
||||
id: "893b2c74-e80f-4015-b0ed-6111b9c36ad2",
|
||||
const defaultInvoiceData: CustomerInvoiceData = {
|
||||
id: "",
|
||||
invoice_status: "draft",
|
||||
invoice_number: "1",
|
||||
invoice_series: "A",
|
||||
@ -128,15 +134,15 @@ const defaultInvoiceData: InvoiceData = {
|
||||
operation_date: "2025-04-30T00:00:00.000Z",
|
||||
language_code: "ES",
|
||||
currency: "EUR",
|
||||
customer_id: "c1d2e3f4-5678-90ab-cdef-1234567890ab",
|
||||
customer_id: "",
|
||||
items: [
|
||||
{
|
||||
id_article: "",
|
||||
description: "Item 1",
|
||||
description: "",
|
||||
quantity: {
|
||||
amount: 100,
|
||||
scale: 2,
|
||||
},
|
||||
|
||||
unit_price: {
|
||||
amount: 100,
|
||||
scale: 2,
|
||||
@ -196,27 +202,26 @@ const defaultInvoiceData: InvoiceData = {
|
||||
scale: 2,
|
||||
currency_code: "EUR",
|
||||
},
|
||||
metadata: {
|
||||
entity: "customer-invoice",
|
||||
},
|
||||
};
|
||||
|
||||
interface InvoiceFormProps {
|
||||
initialData?: InvoiceData;
|
||||
initialData?: CustomerInvoiceData;
|
||||
isPending?: boolean;
|
||||
/**
|
||||
* Callback function to handle form submission.
|
||||
* @param data - The invoice data submitted by the form.
|
||||
*/
|
||||
onSubmit?: (data: InvoiceData) => void;
|
||||
onSubmit?: (data: CustomerInvoiceData) => void;
|
||||
}
|
||||
|
||||
export const InvoiceEditForm = ({
|
||||
export const CustomerInvoiceEditForm = ({
|
||||
initialData = defaultInvoiceData,
|
||||
onSubmit,
|
||||
isPending,
|
||||
}: InvoiceFormProps) => {
|
||||
const form = useForm<InvoiceData>({
|
||||
const { t } = useTranslation(MODULE_NAME);
|
||||
|
||||
const form = useForm<CustomerInvoiceData>({
|
||||
resolver: zodResolver(invoiceSchema),
|
||||
defaultValues: initialData,
|
||||
});
|
||||
@ -242,7 +247,7 @@ export const InvoiceEditForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (data: InvoiceData) => {
|
||||
const handleSubmit = (data: CustomerInvoiceData) => {
|
||||
console.log("Datos del formulario:", data);
|
||||
onSubmit?.(data);
|
||||
};
|
||||
@ -258,147 +263,212 @@ export const InvoiceEditForm = ({
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)} className='space-y-6'>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit, handleError)} className='grid gap-6'>
|
||||
{/* Información básica */}
|
||||
<Card className='@container/card'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Información Básica</CardTitle>
|
||||
<CardDescription>Detalles generales de la factura</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className=' gap-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='invoice_status'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Estado</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<CardContent className='grid grid-cols-1 gap-4 space-y-6'>
|
||||
<div className='grid grid-cols-1'>
|
||||
<ClientSelector />
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='customer_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>ID Cliente</FormLabel>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Seleccionar estado' />
|
||||
</SelectTrigger>
|
||||
<Input placeholder='ID del cliente' {...field} />
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value='draft'>Borrador</SelectItem>
|
||||
<SelectItem value='sent'>Enviada</SelectItem>
|
||||
<SelectItem value='paid'>Pagada</SelectItem>
|
||||
<SelectItem value='cancelled'>Cancelada</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='invoice_number'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("form_fields.invoice_number.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='1' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='invoice_series'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Serie</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='A' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='issue_date'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fecha de Emisión</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start text-left font-normal'
|
||||
>
|
||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='invoice_number'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Número</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='1' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='invoice_series'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Serie</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='A' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='issue_date'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fecha de Emisión</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='invoice_status'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Estado</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start text-left font-normal'
|
||||
>
|
||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||
{field.value
|
||||
? format(new Date(field.value), "dd/MM/yyyy")
|
||||
: "Seleccionar fecha"}
|
||||
</Button>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Seleccionar estado' />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SelectContent>
|
||||
<SelectItem value='draft'>Borrador</SelectItem>
|
||||
<SelectItem value='sent'>Enviada</SelectItem>
|
||||
<SelectItem value='paid'>Pagada</SelectItem>
|
||||
<SelectItem value='cancelled'>Cancelada</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='operation_date'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fecha de Operación</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start text-left font-normal'
|
||||
>
|
||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||
{field.value
|
||||
? format(new Date(field.value), "dd/MM/yyyy")
|
||||
: "Seleccionar fecha"}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='operation_date'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fecha de Operación</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start text-left font-normal'
|
||||
>
|
||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='customer_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>ID Cliente</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='ID del cliente' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='operation_date'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Inicio periodo de facturación</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start text-left font-normal'
|
||||
>
|
||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='operation_date'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Fin periodo de facturación</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start text-left font-normal'
|
||||
>
|
||||
<CalendarIcon className='mr-2 h-4 w-4' />
|
||||
{field.value ? formatDate(field.value) : "Seleccionar fecha"}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-0'>
|
||||
<Calendar
|
||||
mode='single'
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
onSelect={(date) => field.onChange(date?.toISOString())}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
@ -449,6 +519,9 @@ export const InvoiceEditForm = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/*Items */}
|
||||
<CustomerInvoiceItemsCardEditor defaultValues={defaultInvoiceData} />
|
||||
|
||||
{/* Items */}
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
@ -583,7 +656,6 @@ export const InvoiceEditForm = ({
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Configuración de Impuestos */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -653,7 +725,6 @@ export const InvoiceEditForm = ({
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className='flex justify-end space-x-4'>
|
||||
<Button type='button' variant='outline' disabled={isPending} onClick={handleCancel}>
|
||||
Cancelar
|
||||
@ -0,0 +1,63 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { AG_GRID_LOCALE_ES } from "@ag-grid-community/locale";
|
||||
// Grid
|
||||
import type { ColDef, GridOptions, ValueFormatterParams } from "ag-grid-community";
|
||||
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
import { MoneyDTO } from "@erp/core";
|
||||
import { formatDate, formatMoney } from "@erp/core/client";
|
||||
// Core CSS
|
||||
import { AgGridReact } from "ag-grid-react";
|
||||
|
||||
|
||||
|
||||
// Create new GridExample component
|
||||
export const CustomerInvoiceItemsEditorGrid = ({ items: any[] }) => {
|
||||
|
||||
// Column Definitions: Defines & controls grid columns.
|
||||
const [colDefs] = useState<ColDef[]>([
|
||||
|
||||
]);
|
||||
|
||||
const gridOptions: GridOptions = {
|
||||
columnDefs: colDefs,
|
||||
defaultColDef: {
|
||||
editable: true,
|
||||
flex: 1,
|
||||
minWidth: 100,
|
||||
filter: false,
|
||||
sortable: false,
|
||||
resizable: true,
|
||||
},
|
||||
sideBar: true,
|
||||
statusBar: {
|
||||
statusPanels: [
|
||||
{ statusPanel: "agTotalAndFilteredRowCountComponent", align: "left" },
|
||||
{ statusPanel: "agAggregationComponent" },
|
||||
],
|
||||
},
|
||||
rowGroupPanelShow: "always",
|
||||
pagination: true,
|
||||
paginationPageSize: 10,
|
||||
paginationPageSizeSelector: [10, 20, 30, 50],
|
||||
enableCharts: true,
|
||||
localeText: AG_GRID_LOCALE_ES,
|
||||
rowSelection: { mode: "multiRow" },
|
||||
};
|
||||
|
||||
// Container: Defines the grid's theme & dimensions.
|
||||
return (
|
||||
<div
|
||||
className='ag-theme-alpine'
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<AgGridReact rowData={data?.items ?? []} loading={isLoading || isPending} {...gridOptions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
import { CreateCustomerInvoiceCommandSchema } from "@erp/customer-invoices/common/dto";
|
||||
import * as z from "zod/v4";
|
||||
|
||||
export const CustomerInvoiceItemDataFormSchema = CreateCustomerInvoiceCommandSchema.extend({
|
||||
subtotal_price: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
currency_code: z.string(),
|
||||
}),
|
||||
discount: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
}),
|
||||
discount_price: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
currency_code: z.string(),
|
||||
}),
|
||||
before_tax_price: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
currency_code: z.string(),
|
||||
}),
|
||||
tax: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
}),
|
||||
tax_price: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
currency_code: z.string(),
|
||||
}),
|
||||
total_price: z.object({
|
||||
amount: z.number().nullable(),
|
||||
scale: z.number(),
|
||||
currency_code: z.string(),
|
||||
}),
|
||||
});
|
||||
@ -1,35 +0,0 @@
|
||||
import { IMoneyDTO, IPercentageDTO, IQuantityDTO } from "@erp/core";
|
||||
|
||||
export interface InvoiceItem {
|
||||
id_article: string;
|
||||
description: string;
|
||||
quantity: IQuantityDTO;
|
||||
unit_price: IMoneyDTO;
|
||||
subtotal_price: IMoneyDTO;
|
||||
discount: IPercentageDTO;
|
||||
discount_price: IMoneyDTO;
|
||||
total_price: IMoneyDTO;
|
||||
}
|
||||
|
||||
export interface InvoiceData {
|
||||
id: string;
|
||||
invoice_status: string;
|
||||
invoice_number: string;
|
||||
invoice_series: string;
|
||||
issue_date: string;
|
||||
operation_date: string;
|
||||
language_code: string;
|
||||
currency: string;
|
||||
customer_id: string;
|
||||
items: InvoiceItem[];
|
||||
subtotal_price: IMoneyDTO;
|
||||
discount: IPercentageDTO;
|
||||
discount_price: IMoneyDTO;
|
||||
before_tax_price: IMoneyDTO;
|
||||
tax: IPercentageDTO;
|
||||
tax_price: IMoneyDTO;
|
||||
total_price: IMoneyDTO;
|
||||
metadata: {
|
||||
entity: string;
|
||||
};
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { CustomerInvoicesGrid } from "../components";
|
||||
import { CustomerInvoicesListGrid } from "../components";
|
||||
import { MODULE_NAME } from "../manifest";
|
||||
|
||||
export const CustomerInvoicesList = () => {
|
||||
@ -28,20 +28,21 @@ export const CustomerInvoicesList = () => {
|
||||
<AppContent>
|
||||
<div className='flex items-center justify-between space-y-2'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>
|
||||
{t("customerInvoices.list.title")}
|
||||
</h2>
|
||||
<p className='text-muted-foreground'>{t("customerInvoices.list.description")}</p>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>{t("pages.list.title")}</h2>
|
||||
<p className='text-muted-foreground'>{t("pages.list.description")}</p>
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button onClick={() => navigate("/customer-invoices/create")}>
|
||||
<Button
|
||||
onClick={() => navigate("/customer-invoices/create")}
|
||||
className='cursor-pointer'
|
||||
>
|
||||
<PlusIcon className='w-4 h-4 mr-2' />
|
||||
{t("customerInvoices.create.title")}
|
||||
{t("pages.create.title")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full h-full py-4'>
|
||||
<CustomerInvoicesGrid />
|
||||
<CustomerInvoicesListGrid />
|
||||
</div>
|
||||
</AppContent>
|
||||
</>
|
||||
|
||||
48
modules/customers/package.json
Normal file
48
modules/customers/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@erp/customers",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/common/index.ts",
|
||||
"./api": "./src/api/index.ts",
|
||||
"./client": "./src/web/manifest.ts",
|
||||
"./globals.css": "./src/web/globals.css",
|
||||
"./components": "./src/web/components/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@types/react-i18next": "^8.1.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-grid-community/locale": "34.0.0",
|
||||
"@erp/core": "workspace:*",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@repo/rdx-criteria": "workspace:*",
|
||||
"@repo/rdx-ddd": "workspace:*",
|
||||
"@repo/rdx-ui": "workspace:*",
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.74.11",
|
||||
"ag-grid-community": "^33.3.0",
|
||||
"ag-grid-react": "^33.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"express": "^4.18.2",
|
||||
"i18next": "^25.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"sequelize": "^6.37.5",
|
||||
"slugify": "^1.6.6",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"zod": "^3.25.67"
|
||||
}
|
||||
}
|
||||
29
modules/customers/src/api/index.ts
Normal file
29
modules/customers/src/api/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||
//import { customerInvoicesRouter, models } from "./infrastructure";
|
||||
|
||||
export const customersAPIModule: IModuleServer = {
|
||||
name: "customers",
|
||||
version: "1.0.0",
|
||||
dependencies: [],
|
||||
|
||||
init(params: ModuleParams) {
|
||||
// const contacts = getService<ContactsService>("contacts");
|
||||
const { logger } = params;
|
||||
//customerInvoicesRouter(params);
|
||||
logger.info("🚀 Customers module initialized", { label: "customers" });
|
||||
},
|
||||
registerDependencies(params) {
|
||||
const { database, logger } = params;
|
||||
logger.info("🚀 Customers module dependencies registered", {
|
||||
label: "customers",
|
||||
});
|
||||
return {
|
||||
//models,
|
||||
services: {
|
||||
/*...*/
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default customersAPIModule;
|
||||
435
modules/customers/src/web/components/client-selector.tsx
Normal file
435
modules/customers/src/web/components/client-selector.tsx
Normal file
@ -0,0 +1,435 @@
|
||||
"use client";
|
||||
|
||||
import { generateUUIDv4 } from "@repo/rdx-utils";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Separator,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import {
|
||||
Building,
|
||||
Calendar,
|
||||
Edit,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const mockCustomers = [
|
||||
{
|
||||
id: "a1d2e3f4-5678-90ab-cdef-1234567890ab",
|
||||
name: "Juan Pérez",
|
||||
email: "juan@email.com",
|
||||
phone: "+34 600 123 456",
|
||||
company: "Tech Corp",
|
||||
address: "Calle Mayor 123, Madrid",
|
||||
createdAt: "2024-01-15",
|
||||
status: "Activo",
|
||||
},
|
||||
{
|
||||
id: "b1d2e3f4-5678-90ab-cdef-1234567890ab",
|
||||
name: "María García",
|
||||
email: "maria@email.com",
|
||||
phone: "+34 600 789 012",
|
||||
company: "Design Studio",
|
||||
address: "Av. Diagonal 456, Barcelona",
|
||||
createdAt: "2024-02-20",
|
||||
status: "Activo",
|
||||
},
|
||||
{
|
||||
id: "c1d2e3f4-5678-90ab-cdef-1234567890ab",
|
||||
name: "Carlos López",
|
||||
email: "carlos@email.com",
|
||||
phone: "+34 600 345 678",
|
||||
company: "Marketing Plus",
|
||||
address: "Gran Vía 789, Valencia",
|
||||
createdAt: "2024-01-30",
|
||||
status: "Inactivo",
|
||||
},
|
||||
{
|
||||
id: "d1d2e3f4-5678-90ab-cdef-1234567890ab",
|
||||
name: "Ana Martínez",
|
||||
email: "ana@email.com",
|
||||
phone: "+34 600 901 234",
|
||||
company: "Consulting Group",
|
||||
address: "Calle Sierpes 321, Sevilla",
|
||||
createdAt: "2024-03-10",
|
||||
status: "Activo",
|
||||
},
|
||||
];
|
||||
|
||||
export const ClientSelector = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [newCustomer, setNewCustomer] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
company: "",
|
||||
address: "",
|
||||
});
|
||||
|
||||
const handleCreateCustomer = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const createdCustomer = {
|
||||
id: generateUUIDv4(),
|
||||
...newCustomer,
|
||||
createdAt: new Date().toISOString().split("T")[0],
|
||||
status: "Activo",
|
||||
};
|
||||
|
||||
console.log("Cliente creado:", createdCustomer);
|
||||
setSelectedCustomer(createdCustomer);
|
||||
setIsCreateModalOpen(false);
|
||||
setNewCustomer({ name: "", email: "", phone: "", company: "", address: "" });
|
||||
};
|
||||
|
||||
const handleEditCustomer = () => {
|
||||
console.log("Editar cliente:", selectedCustomer);
|
||||
setIsDetailsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteCustomer = () => {
|
||||
console.log("Eliminar cliente:", selectedCustomer);
|
||||
setSelectedCustomer(null);
|
||||
setIsDetailsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleSelectCustomer = (customer) => {
|
||||
console.log("Seleccionar cliente:", customer);
|
||||
setSelectedCustomer(customer);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full max-w-md space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<Label>Cliente</Label>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full justify-start bg-transparent'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<Search className='mr-2 h-4 w-4' />
|
||||
{selectedCustomer ? selectedCustomer.name : "Buscar cliente..."}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput
|
||||
placeholder='Buscar cliente por nombre, email o empresa...'
|
||||
value={searchValue}
|
||||
onValueChange={setSearchValue}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<div className='p-6 text-center'>
|
||||
<User className='h-12 w-12 mx-auto text-muted-foreground mb-4' />
|
||||
<p className='text-sm text-muted-foreground mb-4'>
|
||||
No se encontró ningún cliente con "{searchValue}"
|
||||
</p>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setNewCustomer({ ...newCustomer, name: searchValue });
|
||||
setIsCreateModalOpen(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus className='h-4 w-4 mr-2' />
|
||||
Crear Cliente
|
||||
</Button>
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading='Clientes'>
|
||||
{mockCustomers.map((customer) => (
|
||||
<CommandItem
|
||||
key={customer.id}
|
||||
onSelect={() => handleSelectCustomer(customer)}
|
||||
className='flex items-center space-x-3 p-3'
|
||||
>
|
||||
<User className='h-4 w-4 flex-shrink-0' />
|
||||
<div className='flex-1 min-w-0'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<p className='font-medium truncate'>{customer.name}</p>
|
||||
<Badge
|
||||
variant={customer.status === "Activo" ? "default" : "secondary"}
|
||||
className='text-xs'
|
||||
>
|
||||
{customer.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='flex items-center space-x-4 text-xs text-muted-foreground'>
|
||||
<span className='flex items-center'>
|
||||
<Building className='h-3 w-3 mr-1' />
|
||||
{customer.company}
|
||||
</span>
|
||||
<span className='flex items-center'>
|
||||
<Mail className='h-3 w-3 mr-1' />
|
||||
{customer.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<Separator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={(e) => {
|
||||
setIsCreateModalOpen(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
className='flex items-center space-x-3 p-3 text-primary'
|
||||
>
|
||||
<Plus className='h-4 w-4' />
|
||||
<span>Crear nuevo cliente</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
|
||||
{selectedCustomer && (
|
||||
<Card className='border-primary'>
|
||||
<CardContent className='p-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<User className='h-8 w-8 text-primary' />
|
||||
<div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<h3 className='font-semibold'>{selectedCustomer.name}</h3>
|
||||
<Badge
|
||||
variant={selectedCustomer.status === "Activo" ? "default" : "secondary"}
|
||||
className='text-xs'
|
||||
>
|
||||
{selectedCustomer.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className='text-sm text-muted-foreground'>{selectedCustomer.company}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDetailsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Ver Detalles
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Cambiar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogContent className='max-w-md'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center space-x-2'>
|
||||
<Plus className='h-5 w-5' />
|
||||
<span>Crear Nuevo Cliente</span>
|
||||
</DialogTitle>
|
||||
<DialogDescription>Completa la información del nuevo cliente</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className='space-y-4'>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='name'>Nombre *</Label>
|
||||
<Input
|
||||
id='name'
|
||||
value={newCustomer.name}
|
||||
onChange={(e) => setNewCustomer({ ...newCustomer, name: e.target.value })}
|
||||
placeholder='Nombre completo'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='company'>Empresa</Label>
|
||||
<Input
|
||||
id='company'
|
||||
value={newCustomer.company}
|
||||
onChange={(e) => setNewCustomer({ ...newCustomer, company: e.target.value })}
|
||||
placeholder='Nombre de la empresa'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='email'>Email *</Label>
|
||||
<Input
|
||||
id='email'
|
||||
type='email'
|
||||
value={newCustomer.email}
|
||||
onChange={(e) => setNewCustomer({ ...newCustomer, email: e.target.value })}
|
||||
placeholder='correo@ejemplo.com'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='phone'>Teléfono</Label>
|
||||
<Input
|
||||
id='phone'
|
||||
value={newCustomer.phone}
|
||||
onChange={(e) => setNewCustomer({ ...newCustomer, phone: e.target.value })}
|
||||
placeholder='+34 600 000 000'
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='address'>Dirección</Label>
|
||||
<Input
|
||||
id='address'
|
||||
value={newCustomer.address}
|
||||
onChange={(e) => setNewCustomer({ ...newCustomer, address: e.target.value })}
|
||||
placeholder='Dirección completa'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsCreateModalOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateCustomer}
|
||||
disabled={!newCustomer.name || !newCustomer.email}
|
||||
>
|
||||
<Plus className='h-4 w-4 mr-2' />
|
||||
Crear Cliente
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isDetailsModalOpen} onOpenChange={setIsDetailsModalOpen}>
|
||||
<DialogContent className='max-w-md'>
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center space-x-2'>
|
||||
<User className='h-5 w-5' />
|
||||
<span>Detalles del Cliente</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedCustomer && (
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<h3 className='text-lg font-semibold'>{selectedCustomer.name}</h3>
|
||||
<p className='text-sm text-muted-foreground'>{selectedCustomer.company}</p>
|
||||
</div>
|
||||
<Badge variant={selectedCustomer.status === "Activo" ? "default" : "secondary"}>
|
||||
{selectedCustomer.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<Mail className='h-4 w-4 text-muted-foreground' />
|
||||
<div>
|
||||
<Label className='text-xs font-medium text-muted-foreground'>EMAIL</Label>
|
||||
<p className='font-medium'>{selectedCustomer.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-3'>
|
||||
<Phone className='h-4 w-4 text-muted-foreground' />
|
||||
<div>
|
||||
<Label className='text-xs font-medium text-muted-foreground'>TELÉFONO</Label>
|
||||
<p className='font-medium'>{selectedCustomer.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-start space-x-3'>
|
||||
<MapPin className='h-4 w-4 text-muted-foreground mt-1' />
|
||||
<div>
|
||||
<Label className='text-xs font-medium text-muted-foreground'>DIRECCIÓN</Label>
|
||||
<p className='font-medium'>{selectedCustomer.address}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-3'>
|
||||
<Calendar className='h-4 w-4 text-muted-foreground' />
|
||||
<div>
|
||||
<Label className='text-xs font-medium text-muted-foreground'>
|
||||
FECHA DE REGISTRO
|
||||
</Label>
|
||||
<p className='font-medium'>
|
||||
{new Date(selectedCustomer.createdAt).toLocaleDateString("es-ES")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className='flex space-x-2'>
|
||||
<Button className='flex-1'>
|
||||
<Mail className='h-4 w-4 mr-2' />
|
||||
Enviar Email
|
||||
</Button>
|
||||
<Button variant='outline' onClick={handleEditCustomer}>
|
||||
<Edit className='h-4 w-4 mr-2' />
|
||||
Editar
|
||||
</Button>
|
||||
<Button variant='outline' onClick={handleDeleteCustomer}>
|
||||
<Trash2 className='h-4 w-4 mr-2' />
|
||||
Eliminar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant='outline' onClick={() => setIsDetailsModalOpen(false)}>
|
||||
Cerrar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
modules/customers/src/web/components/index.ts
Normal file
1
modules/customers/src/web/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./client-selector";
|
||||
4
modules/customers/src/web/globals.css
Normal file
4
modules/customers/src/web/globals.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@source "./components";
|
||||
@source "./pages";
|
||||
23
modules/customers/src/web/manifest.ts
Normal file
23
modules/customers/src/web/manifest.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
||||
//import enResources from "../common/locales/en.json";
|
||||
//import esResources from "../common/locales/es.json";
|
||||
|
||||
export const MODULE_NAME = "Customers";
|
||||
const MODULE_VERSION = "1.0.0";
|
||||
|
||||
export const CustomersModuleManifiest: IModuleClient = {
|
||||
name: MODULE_NAME,
|
||||
version: MODULE_VERSION,
|
||||
dependencies: ["auth"],
|
||||
protected: true,
|
||||
layout: "app",
|
||||
|
||||
routes: (params: ModuleClientParams) => {
|
||||
//i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true);
|
||||
//i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true);
|
||||
//return CustomerInvoiceRoutes(params);
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export default CustomersModuleManifiest;
|
||||
33
modules/customers/tsconfig.json
Normal file
33
modules/customers/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@erp/customer-invoices/*": ["./src/*"]
|
||||
},
|
||||
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@ -20,7 +20,6 @@
|
||||
"dinero.js": "^1.9.1",
|
||||
"libphonenumber-js": "^1.11.20",
|
||||
"shallow-equal-object": "^1.1.1",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.24.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Result, generateUUIDv4 } from "@repo/rdx-utils";
|
||||
import * as z from "zod/v4";
|
||||
import { ValueObject } from "./value-object";
|
||||
|
||||
export class UniqueID extends ValueObject<string> {
|
||||
static validate(value: string) {
|
||||
const schema = z.uuid({ message: "Invalid UUID format" });
|
||||
return schema.safeParse(value);
|
||||
}
|
||||
|
||||
static create(id?: string, generateOnEmpty = false): Result<UniqueID, Error> {
|
||||
if (!id || id?.trim() === "") {
|
||||
if (!generateOnEmpty) {
|
||||
@ -17,20 +21,15 @@ export class UniqueID extends ValueObject<string> {
|
||||
|
||||
return result.success
|
||||
? Result.ok(new UniqueID(result.data))
|
||||
: Result.fail(new Error(result.error.errors[0].message));
|
||||
: Result.fail(new Error(result.error.message));
|
||||
}
|
||||
|
||||
static generate(): Result<UniqueID, never> {
|
||||
return Result.ok(new UniqueID(uuidv4()));
|
||||
}
|
||||
|
||||
static validate(id: string) {
|
||||
const schema = z.string().trim().uuid({ message: "Invalid UUID format" });
|
||||
return schema.safeParse(id);
|
||||
return UniqueID.generateNewID();
|
||||
}
|
||||
|
||||
static generateNewID(): Result<UniqueID, never> {
|
||||
return Result.ok(new UniqueID(uuidv4()));
|
||||
return Result.ok(new UniqueID(generateUUIDv4()));
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
|
||||
9
packages/rdx-ui/src/components/buttons/button-group.tsx
Normal file
9
packages/rdx-ui/src/components/buttons/button-group.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import React from "react";
|
||||
|
||||
export const ButtonGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center gap-2", className)} {...props} />
|
||||
)
|
||||
);
|
||||
ButtonGroup.displayName = "ButtonGroup";
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./back-history-button.tsx";
|
||||
export * from "./button-group.tsx";
|
||||
export * from "./help-button.tsx";
|
||||
|
||||
@ -0,0 +1,137 @@
|
||||
import { Header, Table, flexRender } from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Separator,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { t } from "i18next";
|
||||
import { ArrowDownIcon, ArrowDownUpIcon, ArrowUpIcon, EyeOffIcon } from "lucide-react";
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
table: Table<TData>;
|
||||
header: Header<TData, TValue>;
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
table,
|
||||
header,
|
||||
className,
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
if (!header.column.getCanSort()) {
|
||||
return (
|
||||
<>
|
||||
<div className={cn("data-[state=open]:bg-accent tracking-wide text-ellipsis", className)}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
|
||||
{false && header.column.getCanResize() && (
|
||||
<Separator
|
||||
orientation='vertical'
|
||||
className={cn(
|
||||
"absolute top-0 h-full w-[5px] bg-black/10 cursor-col-resize",
|
||||
table.options.columnResizeDirection,
|
||||
header.column.getIsResizing() ? "bg-primary opacity-100" : ""
|
||||
)}
|
||||
{...{
|
||||
onDoubleClick: () => header.column.resetSize(),
|
||||
onMouseDown: header.getResizeHandler(),
|
||||
onTouchStart: header.getResizeHandler(),
|
||||
style: {
|
||||
transform:
|
||||
table.options.columnResizeMode === "onEnd" && header.column.getIsResizing()
|
||||
? `translateX(${
|
||||
(table.options.columnResizeDirection === "rtl" ? -1 : 1) *
|
||||
(table.getState().columnSizingInfo.deltaOffset ?? 0)
|
||||
}px)`
|
||||
: "",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center space-x-2", className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label={
|
||||
header.column.getIsSorted() === "desc"
|
||||
? t("common.sort_desc_description")
|
||||
: header.column.getIsSorted() === "asc"
|
||||
? t("common.sort_asc_description")
|
||||
: t("sort_none_description")
|
||||
}
|
||||
size='sm'
|
||||
variant='ghost'
|
||||
className='-ml-3 h-8 data-[state=open]:bg-accent font-bold text-muted-foreground'
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
|
||||
{header.column.getIsSorted() === "desc" ? (
|
||||
<ArrowDownIcon className='w-4 h-4 ml-2' aria-hidden='true' />
|
||||
) : header.column.getIsSorted() === "asc" ? (
|
||||
<ArrowUpIcon className='w-4 h-4 ml-2' aria-hidden='true' />
|
||||
) : (
|
||||
<ArrowDownUpIcon
|
||||
className='w-4 h-4 ml-2 text-muted-foreground/30'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='start'>
|
||||
{header.column.getCanSort() && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => header.column.toggleSorting(false)}
|
||||
aria-label={t("common.sort_asc")}
|
||||
>
|
||||
<ArrowUpIcon
|
||||
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{t("common.sort_asc")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => header.column.toggleSorting(true)}
|
||||
aria-label={t("common.sort_desc")}
|
||||
>
|
||||
<ArrowDownIcon
|
||||
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{t("common.sort_desc")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{header.column.getCanSort() && header.column.getCanHide() && <DropdownMenuSeparator />}
|
||||
|
||||
{header.column.getCanHide() && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => header.column.toggleVisibility(false)}
|
||||
aria-label={t("Hide")}
|
||||
>
|
||||
<EyeOffIcon
|
||||
className='mr-2 h-3.5 w-3.5 text-muted-foreground/70'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
{t("Hide")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { CellContext } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { MoreVerticalIcon } from "lucide-react";
|
||||
import { ReactElement } from "react";
|
||||
|
||||
export type DataTablaRowActionFunction<TData> = (
|
||||
props: CellContext<TData, unknown>
|
||||
) => DataTableRowActionDefinition<TData>[];
|
||||
|
||||
export type DataTableRowActionDefinition<TData> = {
|
||||
label: string | "-";
|
||||
icon?: ReactElement<any, any>;
|
||||
shortcut?: string;
|
||||
onClick?: (props: CellContext<TData, unknown>, e: React.BaseSyntheticEvent) => void;
|
||||
};
|
||||
|
||||
export type DataTableRowActionsProps<TData, _TValue = unknown> = {
|
||||
className?: string;
|
||||
actions?: DataTablaRowActionFunction<TData>;
|
||||
rowContext: CellContext<TData, unknown>;
|
||||
};
|
||||
|
||||
export function DataTableRowActions<TData = any, TValue = unknown>({
|
||||
actions,
|
||||
rowContext,
|
||||
}: DataTableRowActionsProps<TData, TValue>) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size='icon' variant='outline' className='w-8 h-8'>
|
||||
<MoreVerticalIcon className='h-3.5 w-3.5' />
|
||||
<span className='sr-only'>{t("common.open_menu")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuLabel>{t("common.actions")} </DropdownMenuLabel>
|
||||
{actions &&
|
||||
actions(rowContext).map((action, index) => {
|
||||
// Use a more stable key: for separators, combine 'separator' and index; for items, use label and index
|
||||
if (action.label === "-") {
|
||||
// Use a more stable key by combining a static string and the previous/next action label if possible
|
||||
const prevLabel = actions(rowContext)[index - 1]?.label ?? "start";
|
||||
const nextLabel = actions(rowContext)[index + 1]?.label ?? "end";
|
||||
return <DropdownMenuSeparator key={`separator-${prevLabel}-${nextLabel}`} />;
|
||||
}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`action-${typeof action.label === "string" ? action.label : "item"}-${index}`}
|
||||
onClick={(event) => (action.onClick ? action.onClick(rowContext, event) : null)}
|
||||
>
|
||||
{action.icon && <>{action.icon}</>}
|
||||
{action.label}
|
||||
{action.shortcut && <DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import { t } from "i18next";
|
||||
import { GripVerticalIcon } from "lucide-react";
|
||||
|
||||
export interface DataTableRowDragHandleCellProps {
|
||||
rowId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DataTableRowDragHandleCell = ({
|
||||
rowId,
|
||||
className,
|
||||
}: DataTableRowDragHandleCellProps) => {
|
||||
const { attributes, listeners, isDragging } = useSortable({
|
||||
id: rowId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}}
|
||||
size='icon'
|
||||
variant='link'
|
||||
className={cn(
|
||||
isDragging ? "cursor-grabbing" : "cursor-grab",
|
||||
"w-4 h-4 mt-2 text-ring hover:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVerticalIcon className='w-4 h-4' />
|
||||
<span className='sr-only'>{t("common.move_row")}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
3
packages/rdx-ui/src/components/datatable/index.tsx
Normal file
3
packages/rdx-ui/src/components/datatable/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./datatable-column-header.tsx";
|
||||
export * from "./datatable-row-actions.tsx";
|
||||
export * from "./datatable-row-drag-handle-cell.tsx";
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./buttons/index.tsx";
|
||||
export * from "./custom-dialog.tsx";
|
||||
export * from "./datatable/index.tsx";
|
||||
export * from "./error-overlay.tsx";
|
||||
export * from "./layout/index.tsx";
|
||||
export * from "./loading-overlay/index.tsx";
|
||||
|
||||
@ -1 +1,3 @@
|
||||
@import '@repo/shadcn-ui/globals.css';
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@source "../components";
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"joi": "^17.13.3"
|
||||
"joi": "^17.13.3",
|
||||
"uuid": "^11.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/rdx-utils/src/helpers/id-utils.ts
Normal file
3
packages/rdx-utils/src/helpers/id-utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export const generateUUIDv4 = (): string => uuidv4();
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./collection";
|
||||
export * from "./id-utils";
|
||||
export * from "./maybe";
|
||||
export * from "./result";
|
||||
export * from "./result-collection";
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"css": "@repo/shadcn-ui/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
|
||||
@ -10,10 +10,7 @@
|
||||
"./components": "./src/components/index.tsx",
|
||||
"./components/*": "./src/components/*.tsx",
|
||||
"./lib/*": "./src/lib/*.ts",
|
||||
"./hooks/*": [
|
||||
"./src/hooks/*.ts",
|
||||
"./src/hooks/*/index.ts"
|
||||
]
|
||||
"./hooks/*": ["./src/hooks/*.ts", "./src/hooks/*/index.ts"]
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "biome lint --fix",
|
||||
@ -22,7 +19,8 @@
|
||||
"peerDependencies": {
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
"react-dom": "^18 || ^19",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
@ -32,8 +30,7 @@
|
||||
"@types/node": "^22.15.12",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"postcss": "^8.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
"postcss": "^8.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
|
||||
@ -1,48 +1,9 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@source "../components";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.3rem;
|
||||
--background: oklch(1 0 0);
|
||||
@ -112,6 +73,45 @@
|
||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@ -121,5 +121,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config = {
|
||||
// this file is located in packages/shadcn-ui, but it is being used by apps/web (and any future web apps).
|
||||
// hence, the following paths:
|
||||
content: ["app/**/*.{ts,tsx}", "../../packages/shadcn-ui/src/**/*.{ts,tsx}"],
|
||||
} satisfies Config;
|
||||
content: [
|
||||
"**/*.{ts,tsx}",
|
||||
"apps/**/*.{ts,tsx}",
|
||||
"../../packages/shadcn-ui/src/**/*.{ts,tsx}",
|
||||
"../../modules/**/*.{ts,tsx}",
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/react-library.json",
|
||||
"compilerOptions": {
|
||||
"module": "node16",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "node16",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@repo/shadcn-ui/*": ["./src/*"]
|
||||
|
||||
328
pnpm-lock.yaml
328
pnpm-lock.yaml
@ -173,7 +173,7 @@ importers:
|
||||
version: 29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3))
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
version: 29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@ -198,6 +198,9 @@ importers:
|
||||
'@erp/customer-invoices':
|
||||
specifier: workspace:*
|
||||
version: link:../../modules/customer-invoices
|
||||
'@erp/customers':
|
||||
specifier: workspace:*
|
||||
version: link:../../modules/customers
|
||||
'@repo/rdx-criteria':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-criteria
|
||||
@ -207,6 +210,9 @@ importers:
|
||||
'@repo/shadcn-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shadcn-ui
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(vite@6.3.5(@types/node@22.15.32)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4))
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.74.11
|
||||
version: 5.81.2(react@19.1.0)
|
||||
@ -250,8 +256,8 @@ importers:
|
||||
specifier: ^3.2.0
|
||||
version: 3.3.1
|
||||
tailwindcss:
|
||||
specifier: ^4.1.6
|
||||
version: 4.1.10
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.11
|
||||
tw-animate-css:
|
||||
specifier: ^1.2.9
|
||||
version: 1.3.4
|
||||
@ -271,9 +277,6 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.10
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.6
|
||||
version: 4.1.10(vite@6.3.5(@types/node@22.15.32)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4))
|
||||
'@tanstack/react-query-devtools':
|
||||
specifier: ^5.74.11
|
||||
version: 5.81.2(@tanstack/react-query@5.81.2(react@19.1.0))(react@19.1.0)
|
||||
@ -432,9 +435,21 @@ importers:
|
||||
'@ag-grid-community/locale':
|
||||
specifier: 34.0.0
|
||||
version: 34.0.0
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.1.0)
|
||||
'@erp/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@erp/customers':
|
||||
specifier: workspace:*
|
||||
version: link:../customers
|
||||
'@hookform/resolvers':
|
||||
specifier: ^5.0.1
|
||||
version: 5.1.1(react-hook-form@7.58.1(react@19.1.0))
|
||||
@ -456,6 +471,9 @@ importers:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.74.11
|
||||
version: 5.81.2(react@19.1.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
ag-grid-community:
|
||||
specifier: ^33.3.0
|
||||
version: 33.3.2
|
||||
@ -498,6 +516,15 @@ importers:
|
||||
slugify:
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
sonner:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
tailwindcss:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11
|
||||
tw-animate-css:
|
||||
specifier: ^1.3.4
|
||||
version: 1.3.4
|
||||
zod:
|
||||
specifier: ^3.25.67
|
||||
version: 3.25.67
|
||||
@ -524,6 +551,103 @@ importers:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
|
||||
modules/customers:
|
||||
dependencies:
|
||||
'@ag-grid-community/locale':
|
||||
specifier: 34.0.0
|
||||
version: 34.0.0
|
||||
'@erp/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@hookform/resolvers':
|
||||
specifier: ^5.0.1
|
||||
version: 5.1.1(react-hook-form@7.58.1(react@19.1.0))
|
||||
'@repo/rdx-criteria':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-criteria
|
||||
'@repo/rdx-ddd':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-ddd
|
||||
'@repo/rdx-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-ui
|
||||
'@repo/rdx-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/rdx-utils
|
||||
'@repo/shadcn-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shadcn-ui
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.74.11
|
||||
version: 5.81.2(react@19.1.0)
|
||||
ag-grid-community:
|
||||
specifier: ^33.3.0
|
||||
version: 33.3.2
|
||||
ag-grid-react:
|
||||
specifier: ^33.3.0
|
||||
version: 33.3.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.21.2
|
||||
i18next:
|
||||
specifier: ^25.1.1
|
||||
version: 25.2.1(typescript@5.8.3)
|
||||
lucide-react:
|
||||
specifier: ^0.503.0
|
||||
version: 0.503.0(react@19.1.0)
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.58.1
|
||||
version: 7.58.1(react@19.1.0)
|
||||
react-i18next:
|
||||
specifier: ^15.5.1
|
||||
version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
react-router-dom:
|
||||
specifier: ^6.26.0
|
||||
version: 6.30.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
sequelize:
|
||||
specifier: ^6.37.5
|
||||
version: 6.37.7(mysql2@3.14.1)
|
||||
slugify:
|
||||
specifier: ^1.6.6
|
||||
version: 1.6.6
|
||||
tailwindcss:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11
|
||||
tw-animate-css:
|
||||
specifier: ^1.3.5
|
||||
version: 1.3.5
|
||||
zod:
|
||||
specifier: ^3.25.67
|
||||
version: 3.25.67
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 1.9.4
|
||||
version: 1.9.4
|
||||
'@types/express':
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.23
|
||||
'@types/react':
|
||||
specifier: ^19.1.2
|
||||
version: 19.1.8
|
||||
'@types/react-dom':
|
||||
specifier: ^19.1.3
|
||||
version: 19.1.6(@types/react@19.1.8)
|
||||
'@types/react-i18next':
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
|
||||
packages/rdx-criteria:
|
||||
dependencies:
|
||||
'@codelytv/criteria':
|
||||
@ -551,9 +675,6 @@ importers:
|
||||
shallow-equal-object:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
uuid:
|
||||
specifier: ^11.0.5
|
||||
version: 11.1.0
|
||||
zod:
|
||||
specifier: ^3.24.4
|
||||
version: 3.25.67
|
||||
@ -657,7 +778,7 @@ importers:
|
||||
version: 0.0.4
|
||||
tailwindcss:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.10
|
||||
version: 4.1.11
|
||||
tsup:
|
||||
specifier: ^8.4.0
|
||||
version: 8.4.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3)
|
||||
@ -679,6 +800,9 @@ importers:
|
||||
joi:
|
||||
specifier: ^17.13.3
|
||||
version: 17.13.3
|
||||
uuid:
|
||||
specifier: ^11.0.5
|
||||
version: 11.1.0
|
||||
devDependencies:
|
||||
'@repo/typescript-config':
|
||||
specifier: workspace:*
|
||||
@ -835,10 +959,13 @@ importers:
|
||||
version: 3.3.1
|
||||
tailwindcss:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.10
|
||||
version: 4.1.11
|
||||
tw-animate-css:
|
||||
specifier: ^1.2.9
|
||||
version: 1.3.4
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
vaul:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@ -870,9 +997,6 @@ importers:
|
||||
postcss:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.6
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
|
||||
packages/typescript-config: {}
|
||||
|
||||
@ -2505,60 +2629,117 @@ packages:
|
||||
'@tailwindcss/node@4.1.10':
|
||||
resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==}
|
||||
|
||||
'@tailwindcss/node@4.1.11':
|
||||
resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.10':
|
||||
resolution: {integrity: sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.11':
|
||||
resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.10':
|
||||
resolution: {integrity: sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.11':
|
||||
resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.10':
|
||||
resolution: {integrity: sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.11':
|
||||
resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.10':
|
||||
resolution: {integrity: sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.11':
|
||||
resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10':
|
||||
resolution: {integrity: sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11':
|
||||
resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.10':
|
||||
resolution: {integrity: sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.11':
|
||||
resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.10':
|
||||
resolution: {integrity: sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
|
||||
resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.10':
|
||||
resolution: {integrity: sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
|
||||
resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.10':
|
||||
resolution: {integrity: sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
|
||||
resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.10':
|
||||
resolution: {integrity: sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -2571,29 +2752,57 @@ packages:
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
|
||||
resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@tybys/wasm-util'
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.10':
|
||||
resolution: {integrity: sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.11':
|
||||
resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.10':
|
||||
resolution: {integrity: sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.11':
|
||||
resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.1.10':
|
||||
resolution: {integrity: sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@tailwindcss/oxide@4.1.11':
|
||||
resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@tailwindcss/postcss@4.1.10':
|
||||
resolution: {integrity: sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==}
|
||||
|
||||
'@tailwindcss/vite@4.1.10':
|
||||
resolution: {integrity: sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==}
|
||||
'@tailwindcss/vite@4.1.11':
|
||||
resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==}
|
||||
peerDependencies:
|
||||
vite: ^5.2.0 || ^6
|
||||
vite: ^5.2.0 || ^6 || ^7
|
||||
|
||||
'@tanstack/query-core@5.81.2':
|
||||
resolution: {integrity: sha512-QLYkPdrudoMATDFa3MiLEwRhNnAlzHWDf0LKaXUqJd0/+QxN8uTPi7bahRlxoAyH0UbLMBdeDbYzWALj7THOtw==}
|
||||
@ -5725,6 +5934,9 @@ packages:
|
||||
tailwindcss@4.1.10:
|
||||
resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
|
||||
|
||||
tailwindcss@4.1.11:
|
||||
resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==}
|
||||
|
||||
tapable@2.2.2:
|
||||
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
||||
engines: {node: '>=6'}
|
||||
@ -5951,6 +6163,9 @@ packages:
|
||||
tw-animate-css@1.3.4:
|
||||
resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==}
|
||||
|
||||
tw-animate-css@1.3.5:
|
||||
resolution: {integrity: sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==}
|
||||
|
||||
type-detect@4.0.8:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
@ -7984,42 +8199,88 @@ snapshots:
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.1.10
|
||||
|
||||
'@tailwindcss/node@4.1.11':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
enhanced-resolve: 5.18.1
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.30.1
|
||||
magic-string: 0.30.17
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.1.11
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.10':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.11':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.1.10':
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
@ -8038,6 +8299,24 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.10
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.10
|
||||
|
||||
'@tailwindcss/oxide@4.1.11':
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
tar: 7.4.3
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.1.11
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.1.11
|
||||
'@tailwindcss/oxide-darwin-x64': 4.1.11
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.1.11
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.11
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.1.11
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.1.11
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.1.11
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.11
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.11
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.11
|
||||
|
||||
'@tailwindcss/postcss@4.1.10':
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
@ -8046,11 +8325,11 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
tailwindcss: 4.1.10
|
||||
|
||||
'@tailwindcss/vite@4.1.10(vite@6.3.5(@types/node@22.15.32)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4))':
|
||||
'@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.15.32)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.10
|
||||
'@tailwindcss/oxide': 4.1.10
|
||||
tailwindcss: 4.1.10
|
||||
'@tailwindcss/node': 4.1.11
|
||||
'@tailwindcss/oxide': 4.1.11
|
||||
tailwindcss: 4.1.11
|
||||
vite: 6.3.5(@types/node@22.15.32)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4)
|
||||
|
||||
'@tanstack/query-core@5.81.2': {}
|
||||
@ -11439,6 +11718,8 @@ snapshots:
|
||||
|
||||
tailwindcss@4.1.10: {}
|
||||
|
||||
tailwindcss@4.1.11: {}
|
||||
|
||||
tapable@2.2.2: {}
|
||||
|
||||
tar@6.2.1:
|
||||
@ -11547,7 +11828,7 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(esbuild@0.25.5)(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
ts-jest@29.4.0(@babel/core@7.27.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.27.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.15.32)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
ejs: 3.1.10
|
||||
@ -11565,7 +11846,6 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.27.4)
|
||||
esbuild: 0.25.5
|
||||
jest-util: 29.7.0
|
||||
|
||||
ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3):
|
||||
@ -11707,6 +11987,8 @@ snapshots:
|
||||
|
||||
tw-animate-css@1.3.4: {}
|
||||
|
||||
tw-animate-css@1.3.5: {}
|
||||
|
||||
type-detect@4.0.8: {}
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user