diff --git a/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json b/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json index 09b1f40b..d4dcc72e 100644 --- a/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json +++ b/modules/core/src/common/catalogs/taxes/spain-tax-catalog.json @@ -136,53 +136,53 @@ }, { - "name": "Retención 35%", + "name": "Retenc. 35%", "code": "retencion_35", "value": "3500", "scale": "2", "group": "Retención", - "description": "Retención profesional o fiscal tipo máximo.", + "description": "Retenc. profesional o fiscal tipo máximo.", "aeat_code": null }, { - "name": "Retención 19%", + "name": "Retenc. 19%", "code": "retencion_19", "value": "1900", "scale": "2", "group": "Retención", - "description": "Retención IRPF general.", + "description": "Retenc. IRPF general.", "aeat_code": "R1" }, { - "name": "Retención 15%", + "name": "Retenc. 15%", "code": "retencion_15", "value": "1500", "scale": "2", "group": "Retención", - "description": "Retención para autónomos y profesionales.", + "description": "Retenc. para autónomos y profesionales.", "aeat_code": "R2" }, { - "name": "Retención 7%", + "name": "Retenc. 7%", "code": "retencion_7", "value": "700", "scale": "2", "group": "Retención", - "description": "Retención para nuevos autónomos.", + "description": "Retenc. para nuevos autónomos.", "aeat_code": null }, { - "name": "Retención 2%", + "name": "Retenc. 2%", "code": "retencion_2", "value": "200", "scale": "2", "group": "Retención", - "description": "Retención sobre arrendamientos de inmuebles urbanos.", + "description": "Retenc. sobre arrendamientos de inmuebles urbanos.", "aeat_code": "R3" }, { - "name": "Rec. de equivalencia 5,2%", + "name": "Rec. 5,2%", "code": "rec_5_2", "value": "520", "scale": "2", @@ -191,7 +191,7 @@ "aeat_code": "51" }, { - "name": "Rec. de equivalencia 1,75%", + "name": "Rec. 1,75%", "code": "rec_1_75", "value": "175", "scale": "2", @@ -200,7 +200,7 @@ "aeat_code": "52" }, { - "name": "Rec. de equivalencia 1,4%", + "name": "Rec. 1,4%", "code": "rec_1_4", "value": "140", "scale": "2", @@ -209,7 +209,7 @@ "aeat_code": null }, { - "name": "Rec. de equivalencia 1%", + "name": "Rec. 1%", "code": "rec_1", "value": "100", "scale": "2", @@ -218,7 +218,7 @@ "aeat_code": null }, { - "name": "Rec. de equivalencia 0,62%", + "name": "Rec. 0,62%", "code": "rec_0_62", "value": "62", "scale": "2", @@ -227,7 +227,7 @@ "aeat_code": null }, { - "name": "Rec. de equivalencia 0,5%", + "name": "Rec. 0,5%", "code": "rec_0_5", "value": "50", "scale": "2", @@ -236,7 +236,7 @@ "aeat_code": null }, { - "name": "Rec. de equivalencia 0,26%", + "name": "Rec. 0,26%", "code": "rec_0_26", "value": "26", "scale": "2", @@ -245,7 +245,7 @@ "aeat_code": null }, { - "name": "Rec. de equivalencia 0%", + "name": "Rec. 0%", "code": "rec_0", "value": "0", "scale": "2", diff --git a/modules/customer-invoices/src/common/locales/en.json b/modules/customer-invoices/src/common/locales/en.json index 9442ac88..30fa4517 100644 --- a/modules/customer-invoices/src/common/locales/en.json +++ b/modules/customer-invoices/src/common/locales/en.json @@ -16,6 +16,7 @@ "rows_selected": "{{count}} fila(s) seleccionadas.", "rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas." }, + "catalog": { "status": { "draft": "Draft", @@ -198,6 +199,9 @@ } }, "components": { + "datatable": { + "actions": "Actions" + }, "customer_invoice_taxes_multi_select": { "label": "Taxes", "placeholder": "Select taxes", diff --git a/modules/customer-invoices/src/common/locales/es.json b/modules/customer-invoices/src/common/locales/es.json index dbbfef18..17dd7299 100644 --- a/modules/customer-invoices/src/common/locales/es.json +++ b/modules/customer-invoices/src/common/locales/es.json @@ -191,6 +191,9 @@ } }, "components": { + "datatable": { + "actions": "Acciones" + }, "customer_invoice_taxes_multi_select": { "label": "Impuestos", "placeholder": "Selecciona impuestos", diff --git a/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx b/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx index 992183c9..ce16555e 100644 --- a/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx @@ -4,41 +4,9 @@ import { cn } from "@repo/shadcn-ui/lib/utils"; import { useCallback, useMemo } from 'react'; import { useTranslation } from "../i18n"; -const taxesList = [ - { label: "IVA 21%", value: "iva_21", group: "IVA" }, - { label: "IVA 10%", value: "iva_10", group: "IVA" }, - { label: "IVA 7,5%", value: "iva_7_5", group: "IVA" }, - { label: "IVA 5%", value: "iva_5", group: "IVA" }, - { label: "IVA 4%", value: "iva_4", group: "IVA" }, - { label: "IVA 2%", value: "iva_2", group: "IVA" }, - { label: "IVA 0%", value: "iva_0", group: "IVA" }, - { label: "Exenta", value: "iva_exenta", group: "IVA" }, - { label: "No sujeto", value: "iva_no_sujeto", group: "IVA" }, - { label: "Iva Intracomunitario Bienes", value: "iva_intracomunitario_bienes", group: "IVA" }, - { label: "Iva Intracomunitario Servicio", value: "iva_intracomunitario_servicio", group: "IVA" }, - { label: "Exportación", value: "iva_exportacion", group: "IVA" }, - { label: "Inv. Suj. Pasivo", value: "iva_inversion_sujeto_pasivo", group: "IVA" }, - - { label: "Retención 35%", value: "retencion_35", group: "Retención" }, - { label: "Retención 19%", value: "retencion_19", group: "Retención" }, - { label: "Retención 15%", value: "retencion_15", group: "Retención" }, - { label: "Retención 7%", value: "retencion_7", group: "Retención" }, - { label: "Retención 2%", value: "retencion_2", group: "Retención" }, - - { label: "REC 5,2%", value: "rec_5_2", group: "Recargo de equivalencia" }, - { label: "REC 1,75%", value: "rec_1_75", group: "Recargo de equivalencia" }, - { label: "REC 1,4%", value: "rec_1_4", group: "Recargo de equivalencia" }, - { label: "REC 1%", value: "rec_1", group: "Recargo de equivalencia" }, - { label: "REC 0,62%", value: "rec_0_62", group: "Recargo de equivalencia" }, - { label: "REC 0,5%", value: "rec_0_5", group: "Recargo de equivalencia" }, - { label: "REC 0,26%", value: "rec_0_26", group: "Recargo de equivalencia" }, - { label: "REC 0%", value: "rec_0", group: "Recargo de equivalencia" }, -]; - - interface CustomerInvoiceTaxesMultiSelect { - value: string[]; + value?: string[]; onChange: (selectedValues: string[]) => void; className?: string; [key: string]: any; // Allow other props to be passed diff --git a/modules/customer-invoices/src/web/components/editor/items/amount-input.tsx b/modules/customer-invoices/src/web/components/editor/items/amount-input.tsx index 47249aec..a933e4ba 100644 --- a/modules/customer-invoices/src/web/components/editor/items/amount-input.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/amount-input.tsx @@ -1,5 +1,6 @@ import { formatCurrency } from '@erp/core'; import { useMoney } from '@erp/core/hooks'; +import { Input } from '@repo/shadcn-ui/components'; import { cn } from '@repo/shadcn-ui/lib/utils'; import * as React from "react"; import { findFocusableInCell, focusAndSelect } from './input-utils'; @@ -166,7 +167,7 @@ export function AmountInput({ if (readOnly && readOnlyMode === "textlike-input") { return ( - void }) { + // Editor simple reutilizando el mismo RHF + const { register } = useFormContext(); + return ( + + Edit line #{index + 1} + + Description + + + + + Qty + + + + Unit + + + + Discount % + + + + + Close + + + ); +} + diff --git a/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx b/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx new file mode 100644 index 00000000..66b82a67 --- /dev/null +++ b/modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx @@ -0,0 +1,85 @@ +"use client" + +import { Row, Table } from "@tanstack/react-table"; + + +import { + Button, + Tooltip, + TooltipContent, + TooltipTrigger +} from '@repo/shadcn-ui/components'; +import { ArrowDownIcon, ArrowUpIcon, CopyIcon, PencilIcon, Trash2Icon } from 'lucide-react'; + +interface DataTableRowActionsProps { + row: Row, + table: Table +} + +export function ItemDataTableRowActions({ + row, table +}: DataTableRowActionsProps) { + const ops = (table.options.meta as any)?.rowOps as { + duplicate?: (i: number) => void; + remove?: (i: number) => void; + move?: (f: number, t: number) => void; + canMoveUp?: (i: number) => boolean; + canMoveDown?: (i: number, last: number) => boolean; + }; + const openEditor = (table.options.meta as any)?.openEditor as (i: number) => void; + const lastRow = table.getRowModel().rows.length - 1; + const rowIndex = row.index; + + return ( + + + + openEditor?.(rowIndex)}> + + + + Edit + + + + ops?.duplicate?.(rowIndex)}> + + + + Copy + + + + ops?.move?.(rowIndex, rowIndex - 1)} + > + + + + Up + + + + ops?.move?.(rowIndex, rowIndex + 1)} + > + + + + Down + + + + ops?.remove?.(rowIndex)}> + + + + Delete + + + ); +} diff --git a/modules/customer-invoices/src/web/components/editor/items/items-editor.bak b/modules/customer-invoices/src/web/components/editor/items/items-editor copy.tsx similarity index 50% rename from modules/customer-invoices/src/web/components/editor/items/items-editor.bak rename to modules/customer-invoices/src/web/components/editor/items/items-editor copy.tsx index de78486d..7b67b5a8 100644 --- a/modules/customer-invoices/src/web/components/editor/items/items-editor.bak +++ b/modules/customer-invoices/src/web/components/editor/items/items-editor copy.tsx @@ -1,13 +1,13 @@ -import { useRowSelection } from '@repo/rdx-ui/hooks'; +import { CheckedState, useRowSelection } from '@repo/rdx-ui/hooks'; import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components"; import { useCallback } from 'react'; -import { useFormContext, useWatch } from "react-hook-form"; -import { useItemsTableNavigation } from '../../../hooks'; +import { useFormContext } from "react-hook-form"; +import { useInvoiceContext } from '../../../context'; +import { useInvoiceAutoRecalc, useItemsTableNavigation } from '../../../hooks'; import { useTranslation } from '../../../i18n'; -import { InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas'; +import { InvoiceFormData, InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas'; import { ItemRow } from './item-row'; import { ItemsEditorToolbar } from './items-editor-toolbar'; -import { LastCellTabHook } from './last-cell-tab-hook'; interface ItemsEditorProps { onChange?: (items: InvoiceItemFormData[]) => void; @@ -16,9 +16,11 @@ interface ItemsEditorProps { const createEmptyItem = () => defaultCustomerInvoiceItemFormData; -export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) => { +export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => { const { t } = useTranslation(); - const form = useFormContext(); + const context = useInvoiceContext(); + const form = useFormContext(); + const { control } = form; // Navegación y operaciones sobre las filas const tableNav = useItemsTableNavigation(form, { @@ -27,6 +29,8 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) => firstEditableField: "description", }); + const { fieldArray: { fields } } = tableNav; + const { selectedRows, selectedIndexes, @@ -34,111 +38,83 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) => toggleRow, setSelectAll, clearSelection, - } = useRowSelection(tableNav.fa.fields.length); + } = useRowSelection(fields.length); - const { control } = form; - const items = useWatch({ control: control, name: "items" }); + useInvoiceAutoRecalc(form, context); - // propagar cambios a componente padre - /*useEffect(() => { - onChange?.(items ?? []); - }, [items, onChange]);*/ - - const handleAdd = useCallback(() => { + const handleAddSelection = useCallback(() => { if (readOnly) return; tableNav.addEmpty(true); }, [readOnly, tableNav]); - const handleDuplicate = useCallback(() => { + const handleDuplicateSelection = useCallback(() => { if (readOnly || selectedIndexes.length === 0) return; // duplicar en orden ascendente no rompe índices selectedIndexes.forEach((i) => tableNav.duplicate(i)); }, [readOnly, selectedIndexes, tableNav]); - const handleMoveUp = useCallback(() => { + const handleMoveUpSelection = useCallback(() => { if (readOnly || selectedIndexes.length === 0) return; // mover de menor a mayor para mantener índices válidos selectedIndexes.forEach((i) => tableNav.moveUp(i)); }, [readOnly, selectedIndexes, tableNav]); - const handleMoveDown = useCallback(() => { + const handleMoveDownSelection = useCallback(() => { if (readOnly || selectedIndexes.length === 0) return; // mover de mayor a menor evita desplazar objetivos [...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i)); }, [readOnly, selectedIndexes, tableNav]); - const handleRemove = useCallback(() => { + const handleRemoveSelection = useCallback(() => { if (readOnly || selectedIndexes.length === 0) return; // borrar de mayor a menor para no invalidar índices siguientes [...selectedIndexes].reverse().forEach((i) => tableNav.remove(i)); clearSelection(); }, [readOnly, selectedIndexes, tableNav, clearSelection]); - const hasSelection = selectedIndexes.length > 0; - - return ( {/* Toolbar selección múltiple */} tableNav.addEmpty(true)} - onDuplicate={() => selectedIndexes.forEach((i) => tableNav.duplicate(i))} - onMoveUp={() => selectedIndexes.forEach((i) => tableNav.moveUp(i))} - onMoveDown={() => [...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i))} - onRemove={() => { - [...selectedIndexes].reverse().forEach((i) => tableNav.remove(i)); - clearSelection(); - }} /> - + onAdd={handleAddSelection} + onDuplicate={handleDuplicateSelection} + onMoveUp={handleMoveUpSelection} + onMoveDown={handleMoveDownSelection} + onRemove={handleRemoveSelection} /> - - {/* sel */} - {/* # */} - {/* description */} - {/* qty */} - {/* unit */} - {/* discount */} - {/* taxes */} - {/* taxes2 */} - {/* total */} - {/* actions */} - - - - setSelectAll(checked)} - /> - + + setSelectAll(checked)} + /> - # - {t("form_fields.item.description.label")} - {t("form_fields.item.quantity.label")} - {t("form_fields.item.unit_amount.label")} - {t("form_fields.item.discount_percentage.label")} - {t("form_fields.item.tax_codes.label")} - {t("form_fields.item.total_amount.label")} - + # + {t("form_fields.item.description.label")} + {t("form_fields.item.quantity.label")} + {t("form_fields.item.unit_amount.label")} + {t("form_fields.item.discount_percentage.label")} + {t("form_fields.item.tax_codes.label")} + {t("form_fields.item.total_amount.label")} + - {tableNav.fa.fields.map((f, rowIndex) => ( + {fields.map((f, rowIndex: number) => ( toggleRow(rowIndex)} onDuplicate={() => tableNav.duplicate(rowIndex)} @@ -151,22 +127,18 @@ export const ItemsEditor = ({ onChange, readOnly = false }: ItemsEditorProps) => - + tableNav.addEmpty(true)} /> - - - {/* Navegación por TAB: último campo de la fila */} - ); } diff --git a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx index 5fb83e56..66c0b53a 100644 --- a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx @@ -1,157 +1,48 @@ -import { CheckedState, useRowSelection } from '@repo/rdx-ui/hooks'; -import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components"; -import { useCallback } from 'react'; -import { useFormContext } from "react-hook-form"; +import { DataTable } from '@repo/rdx-ui/components'; +import { useFieldArray, useFormContext } from "react-hook-form"; import { useInvoiceContext } from '../../../context'; -import { useInvoiceAutoRecalc, useItemsTableNavigation } from '../../../hooks'; +import { useInvoiceAutoRecalc } from '../../../hooks'; import { useTranslation } from '../../../i18n'; -import { InvoiceFormData, InvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas'; -import { ItemRow } from './item-row'; -import { ItemsEditorToolbar } from './items-editor-toolbar'; +import { InvoiceFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas'; +import { ItemRowEditor } from './item-row-editor'; +import { useItemsColumns } from './use-items-columns'; -interface ItemsEditorProps { - onChange?: (items: InvoiceItemFormData[]) => void; - readOnly?: boolean; -} const createEmptyItem = () => defaultCustomerInvoiceItemFormData; -export const ItemsEditor = ({ readOnly = false }: ItemsEditorProps) => { +export const ItemsEditor = () => { const { t } = useTranslation(); const context = useInvoiceContext(); const form = useFormContext(); - const { control } = form; - - // Navegación y operaciones sobre las filas - const tableNav = useItemsTableNavigation(form, { - name: "items", - createEmpty: createEmptyItem, - firstEditableField: "description", - }); - - const { fieldArray: { fields } } = tableNav; - - const { - selectedRows, - selectedIndexes, - selectAllState, - toggleRow, - setSelectAll, - clearSelection, - } = useRowSelection(fields.length); + const { control, getValues } = form; useInvoiceAutoRecalc(form, context); - const handleAddSelection = useCallback(() => { - if (readOnly) return; - tableNav.addEmpty(true); - }, [readOnly, tableNav]); + const { fields, append, remove, move, insert } = useFieldArray({ + control, + name: "items", + }); - const handleDuplicateSelection = useCallback(() => { - if (readOnly || selectedIndexes.length === 0) return; - // duplicar en orden ascendente no rompe índices - selectedIndexes.forEach((i) => tableNav.duplicate(i)); - }, [readOnly, selectedIndexes, tableNav]); - - const handleMoveUpSelection = useCallback(() => { - if (readOnly || selectedIndexes.length === 0) return; - // mover de menor a mayor para mantener índices válidos - selectedIndexes.forEach((i) => tableNav.moveUp(i)); - }, [readOnly, selectedIndexes, tableNav]); - - const handleMoveDownSelection = useCallback(() => { - if (readOnly || selectedIndexes.length === 0) return; - // mover de mayor a menor evita desplazar objetivos - [...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i)); - }, [readOnly, selectedIndexes, tableNav]); - - const handleRemoveSelection = useCallback(() => { - if (readOnly || selectedIndexes.length === 0) return; - // borrar de mayor a menor para no invalidar índices siguientes - [...selectedIndexes].reverse().forEach((i) => tableNav.remove(i)); - clearSelection(); - }, [readOnly, selectedIndexes, tableNav, clearSelection]); + const columns = useItemsColumns(); return ( - {/* Toolbar selección múltiple */} - - - - - - - - - - - - - - - - - - - setSelectAll(checked)} - /> - - - # - {t("form_fields.item.description.label")} - {t("form_fields.item.quantity.label")} - {t("form_fields.item.unit_amount.label")} - {t("form_fields.item.discount_percentage.label")} - {t("form_fields.item.tax_codes.label")} - {t("form_fields.item.total_amount.label")} - - - - - - {fields.map((f, rowIndex: number) => ( - toggleRow(rowIndex)} - onDuplicate={() => tableNav.duplicate(rowIndex)} - onMoveUp={() => tableNav.moveUp(rowIndex)} - onMoveDown={() => tableNav.moveDown(rowIndex)} - onRemove={() => tableNav.remove(rowIndex)} - /> - ))} - - - - - - tableNav.addEmpty(true)} - /> - - - - - - + (r as any).id} + meta={{ + rowOps: { + //duplicate: (indexRow: number) => insert(indexRow + 1, { ...getValues(`items.${indexRow}`) /*, id: crypto.randomUUID()*/ }), + remove: (indexRow: number) => remove(indexRow), + move: (fromIndex: number, toIndex: number) => { + if (toIndex < 0 || toIndex >= fields.length) return; + move(fromIndex, toIndex); + }, + canMoveUp: (indexRow: number) => indexRow > 0, + canMoveDown: (indexRow: number, lastIndexRow: number) => indexRow < lastIndexRow, + }, + }} + renderRowEditor={(index, close) => } /> ); } diff --git a/modules/customer-invoices/src/web/components/editor/items/percentage-input.tsx b/modules/customer-invoices/src/web/components/editor/items/percentage-input.tsx index fdc7cd6e..db95e896 100644 --- a/modules/customer-invoices/src/web/components/editor/items/percentage-input.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/percentage-input.tsx @@ -1,3 +1,4 @@ +import { Input } from '@repo/shadcn-ui/components'; import { cn } from '@repo/shadcn-ui/lib/utils'; import * as React from "react"; import { findFocusableInCell, focusAndSelect } from './input-utils'; @@ -198,7 +199,7 @@ export function PercentageInput({ if (readOnly && readOnlyMode === "textlike-input") { return ( - [] { + const { t, readOnly, currency_code, language_code } = useInvoiceContext(); + const { control } = useFormContext(); + + // Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla + return React.useMemo[]>(() => [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!v)} + aria-label="Select all" + className="translate-y-[2px]" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!v)} + aria-label="Select row" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false, + size: 48, minSize: 40, maxSize: 64, + meta: { className: "w-[4ch]" }, // ancho aprox. por dígitos + }, + { + id: 'position', + header: ({ column }) => ( + + ), + cell: ({ row }) => row.index + 1, + enableSorting: false, + size: 32, + }, + { + accessorKey: "description", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + ( + { + const el = e.currentTarget; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }} + className={cn( + "min-w-[12rem] max-w-[46rem] w-full resize-none bg-transparent border-dashed transition", + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid", + "focus:resize-y" + )} + data-cell-focus + /> + )} + /> + ), + enableSorting: false, + size: 480, minSize: 240, maxSize: 768, + }, + { + accessorKey: "quantity", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + size: 52, minSize: 48, maxSize: 64, + }, + { + accessorKey: "unit_amount", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + size: 120, minSize: 100, maxSize: 160, + }, + { + accessorKey: "discount_percentage", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + size: 40, minSize: 40 + }, + { + accessorKey: "tax_codes", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + ( + + )} + /> + ), + enableSorting: false, + size: 240, minSize: 232, maxSize: 320, + }, + { + accessorKey: "total_amount", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + + + ), + enableSorting: false, + size: 120, minSize: 100, maxSize: 160, + }, + { + id: "actions", + header: ({ column }) => ( + + ), + cell: ({ row, table }) => , + }, + ], [t, readOnly, control, currency_code, language_code,]); +} diff --git a/modules/customer-invoices/src/web/components/items/customer-invoice-items-card-editor.tsx b/modules/customer-invoices/src/web/components/items/customer-invoice-items-card-editor.tsx index e36e0062..14074a31 100644 --- a/modules/customer-invoices/src/web/components/items/customer-invoice-items-card-editor.tsx +++ b/modules/customer-invoices/src/web/components/items/customer-invoice-items-card-editor.tsx @@ -19,7 +19,6 @@ 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 { useDetailColumns } from "../../hooks"; import { useTranslation } from "../../i18n"; import { formatCurrency } from "../../pages/create/utils"; import { CustomerInvoiceTaxesMultiSelect } from "../customer-invoice-taxes-multi-select"; @@ -215,8 +214,8 @@ export const CustomerInvoiceItemsCardEditor = ({ field.onChange(Number(e.target.value) * 100)} - //value={field.value / 100} + //onChange={(e) => field.onChange(Number(e.target.value) * 100)} + //value={field.value / 100} /> diff --git a/modules/customer-invoices/src/web/context/invoice-context.tsx b/modules/customer-invoices/src/web/context/invoice-context.tsx index fd0874f7..fa47356f 100644 --- a/modules/customer-invoices/src/web/context/invoice-context.tsx +++ b/modules/customer-invoices/src/web/context/invoice-context.tsx @@ -1,5 +1,8 @@ import { TaxCatalogProvider } from '@erp/core'; +import { TFunction } from 'i18next'; import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react"; +import { useTranslation } from "../i18n"; +import { MODULE_NAME } from '../manifest'; export type InvoiceContextValue = { company_id: string; @@ -12,6 +15,8 @@ export type InvoiceContextValue = { readOnly: boolean; taxCatalog: TaxCatalogProvider; + t: TFunction; + changeLanguage: (lang: string) => void; changeCurrency: (currency: string) => void; changeIsProforma: (value: boolean) => void; @@ -36,6 +41,8 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com currency_code: initialCurrency = "EUR", readOnly: initialReadOnly = false, is_proforma: initialProforma = true, children }: PropsWithChildren) => { + const { t } = useTranslation(); + // Estado interno local para campos dinámicos const [language_code, setLanguage] = useState(initialLang); const [currency_code, setCurrency] = useState(initialCurrency); @@ -53,6 +60,8 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com const value = useMemo(() => { return { + t, + invoice_id, company_id, status, @@ -68,7 +77,7 @@ export const InvoiceProvider = ({ taxCatalog: initialTaxCatalog, invoice_id, com changeIsProforma: setIsProformaMemo, setReadOnly: setReadOnlyMemo, } - }, [readOnly, company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo, setReadOnlyMemo]); + }, [t, readOnly, company_id, invoice_id, status, language_code, currency_code, is_proforma, taxCatalog, setLanguageMemo, setCurrencyMemo, setIsProformaMemo, setReadOnlyMemo]); return {children}; }; diff --git a/modules/customer-invoices/src/web/hooks/index.ts b/modules/customer-invoices/src/web/hooks/index.ts index ab262e09..e5084314 100644 --- a/modules/customer-invoices/src/web/hooks/index.ts +++ b/modules/customer-invoices/src/web/hooks/index.ts @@ -1,7 +1,6 @@ export * from "./calcs"; export * from "./use-create-customer-invoice-mutation"; export * from "./use-customer-invoices-query"; -export * from "./use-detail-columns"; export * from "./use-invoice-query"; export * from "./use-items-table-navigation"; export * from "./use-update-customer-invoice-mutation"; diff --git a/modules/customer-invoices/src/web/hooks/use-detail-columns.tsx b/modules/customer-invoices/src/web/hooks/use-detail-columns.tsx index c45f85b5..ad763e65 100644 --- a/modules/customer-invoices/src/web/hooks/use-detail-columns.tsx +++ b/modules/customer-invoices/src/web/hooks/use-detail-columns.tsx @@ -1,8 +1,3 @@ -import { - DataTablaRowActionFunction, - DataTableRowActions, - DataTableRowDragHandleCell, -} from "@repo/rdx-ui/components"; import { Checkbox } from "@repo/shadcn-ui/components"; import { ColumnDef } from "@tanstack/react-table"; diff --git a/modules/customer-invoices/src/web/i18n.ts b/modules/customer-invoices/src/web/i18n.ts index 109e9359..106cc77c 100644 --- a/modules/customer-invoices/src/web/i18n.ts +++ b/modules/customer-invoices/src/web/i18n.ts @@ -1,5 +1,5 @@ -import { i18n } from "i18next"; -import { useTranslation as useI18NextTranslation } from "react-i18next"; +import { KeyPrefix, Namespace, i18n } from "i18next"; +import { UseTranslationResponse, useTranslation as useI18NextTranslation } from "react-i18next"; import enResources from "../common/locales/en.json"; import esResources from "../common/locales/es.json"; import { MODULE_NAME } from "./manifest"; @@ -17,9 +17,13 @@ const addMissingBundles = (i18n: i18n) => { } }; -export const useTranslation = () => { +export const useTranslation = < + Ns extends Namespace = typeof MODULE_NAME, + K extends KeyPrefix = undefined, +>( + keyPrefix?: K +): UseTranslationResponse => { const { i18n } = useI18NextTranslation(); addMissingBundles(i18n); - - return useI18NextTranslation(MODULE_NAME); + return useI18NextTranslation(MODULE_NAME, { keyPrefix }); }; diff --git a/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx b/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx new file mode 100644 index 00000000..dfc73eb5 --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/data-table-column-header.tsx @@ -0,0 +1,69 @@ +import { Column } from "@tanstack/react-table"; +import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react"; + +import { useTranslation } from "../../locales/i18n.ts"; + +import { + Button, DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@repo/shadcn-ui/components'; +import { cn } from '@repo/shadcn-ui/lib/utils'; + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column + title: string +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + const { t } = useTranslation(); + + if (!column.getCanSort()) { + return {title} + } + + return ( + + + + + {title} + {column.getIsSorted() === "desc" ? ( + + ) : column.getIsSorted() === "asc" ? ( + + ) : ( + + )} + + + + column.toggleSorting(false)}> + + {t("components.datatabla.asc")} + + column.toggleSorting(true)}> + + {t("components.datatabla.desc")} + + + column.toggleVisibility(false)}> + + {t("components.datatabla.hide")} + + + + + ) +} diff --git a/packages/rdx-ui/src/components/datatable/data-table-faceted-filter.tsx b/packages/rdx-ui/src/components/datatable/data-table-faceted-filter.tsx new file mode 100644 index 00000000..701c7f6d --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/data-table-faceted-filter.tsx @@ -0,0 +1,141 @@ +import { Column } from "@tanstack/react-table" +import { Check, PlusCircle } from "lucide-react" +import * as React from "react" + +import { + Badge, Button, Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, Popover, + PopoverContent, + PopoverTrigger, Separator +} from '@repo/shadcn-ui/components' +import { cn } from '@repo/shadcn-ui/lib/utils' + +interface DataTableFacetedFilterProps { + column?: Column + title?: string + options: { + label: string + value: string + icon?: React.ComponentType<{ className?: string }> + }[] +} + +export function DataTableFacetedFilter({ + column, + title, + options, +}: DataTableFacetedFilterProps) { + const facets = column?.getFacetedUniqueValues() + const selectedValues = new Set(column?.getFilterValue() as string[]) + + return ( + + + + + {title} + {selectedValues?.size > 0 && ( + <> + + + {selectedValues.size} + + + {selectedValues.size > 2 ? ( + + {selectedValues.size} selected + + ) : ( + options + .filter((option) => selectedValues.has(option.value)) + .map((option) => ( + + {option.label} + + )) + )} + + > + )} + + + + + + + No results found. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value) + return ( + { + if (isSelected) { + selectedValues.delete(option.value) + } else { + selectedValues.add(option.value) + } + const filterValues = Array.from(selectedValues) + column?.setFilterValue( + filterValues.length ? filterValues : undefined + ) + }} + > + + + + {option.icon && ( + + )} + {option.label} + {facets?.get(option.value) && ( + + {facets.get(option.value)} + + )} + + ) + })} + + {selectedValues.size > 0 && ( + <> + + + column?.setFilterValue(undefined)} + className="justify-center text-center" + > + Clear filters + + + > + )} + + + + + ) +} diff --git a/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx b/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx new file mode 100644 index 00000000..eac5fe6c --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/data-table-pagination.tsx @@ -0,0 +1,100 @@ +import { Table } from "@tanstack/react-table" +import { + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, +} from "lucide-react" + +import { + Button, Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@repo/shadcn-ui/components' + +interface DataTablePaginationProps { + table: Table +} + +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + return ( + + + {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. + + + + Rows per page + { + table.setPageSize(Number(value)) + }} + > + + + + + {[10, 20, 25, 30, 40, 50].map((pageSize) => ( + + {pageSize} + + ))} + + + + + Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} + + + table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + Go to first page + + + table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + Go to previous page + + + table.nextPage()} + disabled={!table.getCanNextPage()} + > + Go to next page + + + table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + Go to last page + + + + + + ) +} diff --git a/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx b/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx new file mode 100644 index 00000000..40bf01da --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/data-table-toolbar.tsx @@ -0,0 +1,28 @@ +"use client" + +import { Button } from '@repo/shadcn-ui/components' +import { Table } from "@tanstack/react-table" +import { DataTableViewOptions } from './data-table-view-options.tsx' + + +interface DataTableToolbarProps { + table: Table +} + +export function DataTableToolbar({ + table, +}: DataTableToolbarProps) { + const isFiltered = table.getState().columnFilters.length > 0 + + return ( + + + + + + + Add Task + + + ) +} diff --git a/packages/rdx-ui/src/components/datatable/data-table-view-options.tsx b/packages/rdx-ui/src/components/datatable/data-table-view-options.tsx new file mode 100644 index 00000000..ad032f79 --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/data-table-view-options.tsx @@ -0,0 +1,56 @@ +"use client" + +import { Table } from "@tanstack/react-table" +import { Settings2 } from "lucide-react" + +import { + Button, DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@repo/shadcn-ui/components' + +export function DataTableViewOptions({ + table, +}: { + table: Table +}) { + return ( + + + + + View + + + + Toggle columns + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ) + })} + + + ) +} diff --git a/packages/rdx-ui/src/components/datatable/data-table.tsx b/packages/rdx-ui/src/components/datatable/data-table.tsx new file mode 100644 index 00000000..2c8c3b2a --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/data-table.tsx @@ -0,0 +1,183 @@ +"use client" + +import { + ColumnDef, + ColumnFiltersState, + ColumnSizingState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table" +import * as React from "react" + +import { + Dialog, + DialogContent, + Table, TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@repo/shadcn-ui/components' +import { DataTablePagination } from './data-table-pagination.tsx' +import { DataTableToolbar } from "./data-table-toolbar.tsx" + +import { useTranslation } from "../../locales/i18n.ts" + + +type DataTableRowOps = { + duplicate?(index: number): void; + remove?(index: number): void; + move?(from: number, to: number): void; + canMoveUp?(index: number): boolean; + canMoveDown?(index: number, lastIndex: number): boolean; +}; + +interface DataTableProps { + columns: ColumnDef[] + data: TData[], + meta?: Record, + + getRowId?: (row: TData, index: number) => string; + pageSize?: number; + enableRowSelection?: boolean; + + renderRowEditor?: (index: number, close: () => void) => React.ReactNode; // editor modal opcional. Se muestra dentro de un Dialog. +} + +export function DataTable({ + columns, + data, + meta, + getRowId, + pageSize = 25, + enableRowSelection = true, + renderRowEditor, +}: DataTableProps) { + const { t } = useTranslation(); + + const [rowSelection, setRowSelection] = React.useState({}); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [columnFilters, setColumnFilters] = React.useState([]); + const [sorting, setSorting] = React.useState([]); + const [colSizes, setColSizes] = React.useState({}); + const [editIndex, setEditIndex] = React.useState(null); + + const openEditor = React.useCallback((i: number) => setEditIndex(i), []); + const closeEditor = React.useCallback(() => setEditIndex(null), []); + + + const table = useReactTable({ + data, + columns, + columnResizeMode: "onChange", + onColumnSizingChange: setColSizes, + getRowId: getRowId ?? ((row: any, idx) => (row?.id ? String(row.id) : String(idx))), + state: { + columnSizing: colSizes, + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + initialState: { pagination: { pageSize } }, + meta: { ...meta, openEditor }, + + enableRowSelection, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + return ( + + + + + + + {table.getHeaderGroups().map((hg) => ( + + {hg.headers.map((h) => { + const w = h.getSize(); // px + const minW = h.column.columnDef.minSize; + const maxW = h.column.columnDef.maxSize; + return ( + + {h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())} + + ); + })} + + ))} + + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const w = cell.column.getSize(); + const minW = cell.column.columnDef.minSize; + const maxW = cell.column.columnDef.maxSize; + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + )) + ) : ( + + + {t("components.datatabla.empty")} + + + )} + + + + + + + {!!renderRowEditor && editIndex !== null && ( + (!open ? closeEditor() : null)}> + + {renderRowEditor(editIndex, closeEditor)} + + + )} + + ); +} \ No newline at end of file diff --git a/packages/rdx-ui/src/components/datatable/index.tsx b/packages/rdx-ui/src/components/datatable/index.tsx index ff08138e..fbc9d09c 100644 --- a/packages/rdx-ui/src/components/datatable/index.tsx +++ b/packages/rdx-ui/src/components/datatable/index.tsx @@ -1,3 +1,3 @@ -export * from "./datatable-column-header.tsx"; -export * from "./datatable-row-actions.tsx"; -export * from "./datatable-row-drag-handle-cell.tsx"; +export * from "./data-table-column-header.tsx"; +export * from "./data-table.tsx"; + diff --git a/packages/rdx-ui/src/components/datatable/user-nav.tsx b/packages/rdx-ui/src/components/datatable/user-nav.tsx new file mode 100644 index 00000000..8b54a943 --- /dev/null +++ b/packages/rdx-ui/src/components/datatable/user-nav.tsx @@ -0,0 +1,62 @@ +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/registry/new-york-v4/ui/avatar" +import { Button } from "@/registry/new-york-v4/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/registry/new-york-v4/ui/dropdown-menu" + +export function UserNav() { + return ( + + + + + + SC + + + + + + + shadcn + + m@example.com + + + + + + + Profile + ⇧⌘P + + + Billing + ⌘B + + + Settings + ⌘S + + New Team + + + + Log out + ⇧⌘Q + + + + ) +} diff --git a/packages/rdx-ui/src/components/datatable/datatable-column-header.tsx b/packages/rdx-ui/src/components/datatable2/datatable-column-header.tsx similarity index 100% rename from packages/rdx-ui/src/components/datatable/datatable-column-header.tsx rename to packages/rdx-ui/src/components/datatable2/datatable-column-header.tsx diff --git a/packages/rdx-ui/src/components/datatable/datatable-row-actions.tsx b/packages/rdx-ui/src/components/datatable2/datatable-row-actions.tsx similarity index 100% rename from packages/rdx-ui/src/components/datatable/datatable-row-actions.tsx rename to packages/rdx-ui/src/components/datatable2/datatable-row-actions.tsx diff --git a/packages/rdx-ui/src/components/datatable/datatable-row-drag-handle-cell.tsx b/packages/rdx-ui/src/components/datatable2/datatable-row-drag-handle-cell.tsx similarity index 100% rename from packages/rdx-ui/src/components/datatable/datatable-row-drag-handle-cell.tsx rename to packages/rdx-ui/src/components/datatable2/datatable-row-drag-handle-cell.tsx diff --git a/packages/rdx-ui/src/components/datatable2/index.tsx b/packages/rdx-ui/src/components/datatable2/index.tsx new file mode 100644 index 00000000..ff08138e --- /dev/null +++ b/packages/rdx-ui/src/components/datatable2/index.tsx @@ -0,0 +1,3 @@ +export * from "./datatable-column-header.tsx"; +export * from "./datatable-row-actions.tsx"; +export * from "./datatable-row-drag-handle-cell.tsx"; diff --git a/packages/rdx-ui/src/components/index.tsx b/packages/rdx-ui/src/components/index.tsx index 2cd514b7..29f922fb 100644 --- a/packages/rdx-ui/src/components/index.tsx +++ b/packages/rdx-ui/src/components/index.tsx @@ -13,3 +13,4 @@ export * from "./multi-select.tsx"; export * from "./multiple-selector.tsx"; export * from "./scroll-to-top.tsx"; export * from "./tailwind-indicator.tsx"; + diff --git a/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx b/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx index 1cf83cec..dd057c39 100644 --- a/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx +++ b/packages/rdx-ui/src/components/lookup-dialog/lookup-dialog.tsx @@ -1,4 +1,5 @@ -import { useTranslation } from "@repo/rdx-ui/locales/i18n.ts"; +import { useTranslation } from "../../locales/i18n.ts"; + import { Button, Dialog, diff --git a/packages/rdx-ui/src/locales/en.json b/packages/rdx-ui/src/locales/en.json index 2d41b43a..f02f2067 100644 --- a/packages/rdx-ui/src/locales/en.json +++ b/packages/rdx-ui/src/locales/en.json @@ -7,6 +7,13 @@ "read_only": "Read only" }, "components": { + "datatable": { + "asc": "Asc", + "desc": "Desc", + "hide": "Hide", + "empty": "No results found", + "actions": "Actions" + }, "loading_indicator": { "title": "Loading...", "subtitle": "This may take a few seconds. Please do not close this page." diff --git a/packages/rdx-ui/src/locales/es.json b/packages/rdx-ui/src/locales/es.json index 71448651..70ce65d4 100644 --- a/packages/rdx-ui/src/locales/es.json +++ b/packages/rdx-ui/src/locales/es.json @@ -7,6 +7,13 @@ "search": "Buscar" }, "components": { + "datatable": { + "asc": "Asc", + "desc": "Desc", + "hide": "Ocultar", + "empty": "No hay resultados", + "actions": "Acciones" + }, "loading_indicator": { "title": "Cargando...", "subtitle": "Esto puede tardar unos segundos. Por favor, no cierre esta página." diff --git a/packages/rdx-ui/tsconfig.json b/packages/rdx-ui/tsconfig.json index bd86d0df..488afc03 100644 --- a/packages/rdx-ui/tsconfig.json +++ b/packages/rdx-ui/tsconfig.json @@ -8,6 +8,9 @@ "plugins": [{ "name": "typescript-plugin-css-modules" }] }, - "include": ["src"], + "include": [ + "src", + "../../modules/customer-invoices/src/web/components/editor/items/items-data-table-row-actions.tsx" + ], "exclude": ["node_modules", "dist"] }
Rows per page
shadcn
+ m@example.com +