Facturas de cliente
This commit is contained in:
parent
4e757c86d0
commit
b4fb4902dc
@ -0,0 +1,230 @@
|
||||
import { Button, Checkbox, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useCalcInvoiceItemTotals } from '../../../hooks';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { CustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||
import { AmountDTOInputField } from './amount-dto-input-field';
|
||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||
import { PercentageDTOInputField } from './percentage-dto-input-field';
|
||||
import { QuantityDTOInputField } from './quantity-dto-input-field';
|
||||
|
||||
export type ItemRowProps = {
|
||||
|
||||
item: CustomerInvoiceItemFormData;
|
||||
rowIndex: number;
|
||||
isSelected: boolean;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
readOnly: boolean;
|
||||
onToggleSelect: () => void;
|
||||
onDuplicate: () => void;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
|
||||
export const ItemRow = ({ item,
|
||||
rowIndex,
|
||||
isSelected,
|
||||
isFirst,
|
||||
isLast,
|
||||
readOnly,
|
||||
onToggleSelect,
|
||||
onDuplicate,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove, }: ItemRowProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { control, setValue } = useFormContext();
|
||||
const totals = useCalcInvoiceItemTotals(item);
|
||||
|
||||
console.log(totals);
|
||||
|
||||
// sincroniza el total con el form
|
||||
useEffect(() => {
|
||||
if (totals?.totalDTO) {
|
||||
setValue?.(`items.${rowIndex}.total_amount`, totals.totalDTO);
|
||||
}
|
||||
}, [totals.totalDTO, control, rowIndex]);
|
||||
|
||||
|
||||
return (
|
||||
<TableRow data-row-index={rowIndex}>
|
||||
{/* selección */}
|
||||
<TableCell className='align-top'>
|
||||
<div className='h-5'>
|
||||
<Checkbox
|
||||
aria-label={`Seleccionar fila ${rowIndex + 1}`}
|
||||
className="block h-5 w-5 leading-none align-middle"
|
||||
checked={isSelected}
|
||||
onCheckedChange={onToggleSelect}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* # */}
|
||||
<TableCell className='text-left pt-[6px]'>
|
||||
<span className='block translate-y-[-1px] text-muted-foreground tabular-nums text-xs'>
|
||||
{rowIndex + 1}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* description */}
|
||||
<TableCell>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${rowIndex}.description`}
|
||||
render={({ field }) => (
|
||||
<textarea
|
||||
{...field}
|
||||
aria-label={t("form_fields.item.description.label")}
|
||||
className='w-full resize-none bg-transparent p-0 pt-1.5 leading-5 min-h-8 focus:outline-none focus:bg-background'
|
||||
rows={1}
|
||||
spellCheck
|
||||
readOnly={readOnly}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* qty */}
|
||||
<TableCell className='text-right'>
|
||||
<QuantityDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.quantity`}
|
||||
readOnly={readOnly}
|
||||
inputId={`quantity-${rowIndex}`}
|
||||
emptyMode="blank"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* unit */}
|
||||
<TableCell className='text-right'>
|
||||
<AmountDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.unit_amount`}
|
||||
readOnly={readOnly}
|
||||
inputId={`unit-amount-${rowIndex}`}
|
||||
scale={4}
|
||||
locale={"es"}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* discount */}
|
||||
<TableCell className='text-right'>
|
||||
<PercentageDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.discount_percentage`}
|
||||
readOnly={readOnly}
|
||||
inputId={`discount-percentage-${rowIndex}`}
|
||||
showSuffix
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* taxes */}
|
||||
<TableCell>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${rowIndex}.tax_codes`}
|
||||
render={({ field }) => (
|
||||
<CustomerInvoiceTaxesMultiSelect
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
|
||||
{/* total (solo lectura) */}
|
||||
<TableCell className='text-right tabular-nums pt-[6px] leading-5'>
|
||||
<HoverCardTotalsSummary totals={totals}>
|
||||
<AmountDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.total_amount`}
|
||||
readOnly
|
||||
inputId={`total-amount-${rowIndex}`}
|
||||
locale="es"
|
||||
/>
|
||||
</HoverCardTotalsSummary>
|
||||
</TableCell>
|
||||
|
||||
{/* actions */}
|
||||
<TableCell className='pt-[4px]'>
|
||||
<div className='flex justify-end gap-0'>
|
||||
{onDuplicate && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={onDuplicate}
|
||||
disabled={readOnly}
|
||||
aria-label='Duplicar fila'
|
||||
className='h-8 w-8 self-start -translate-y-[1px]'
|
||||
>
|
||||
<CopyIcon className='size-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Duplicar</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{onMoveUp && (
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={onMoveUp}
|
||||
disabled={readOnly || isFirst}
|
||||
aria-label='Mover arriba'
|
||||
className='h-8 w-8 self-start -translate-y-[1px]'
|
||||
>
|
||||
<ArrowUpIcon className='size-4' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onMoveDown && (
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={onMoveDown}
|
||||
disabled={readOnly || isLast}
|
||||
aria-label='Mover abajo'
|
||||
className='h-8 w-8 self-start -translate-y-[1px]'
|
||||
>
|
||||
<ArrowDownIcon className='size-4' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onRemove && (
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={onRemove}
|
||||
disabled={readOnly}
|
||||
aria-label='Eliminar fila'
|
||||
className='h-8 w-8 self-start -translate-y-[1px]'
|
||||
>
|
||||
<Trash2Icon className='size-4' />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@ -1,18 +1,13 @@
|
||||
import { useRowSelection } from '@repo/rdx-ui/hooks';
|
||||
import { Button, Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||
import { ArrowDown, ArrowUp, CopyIcon, Trash2 } from "lucide-react";
|
||||
import { Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@repo/shadcn-ui/components";
|
||||
import * as React from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useCalcInvoiceItemTotals, useItemsTableNavigation } from '../../../hooks';
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useItemsTableNavigation } from '../../../hooks';
|
||||
import { useTranslation } from '../../../i18n';
|
||||
import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||
import { AmountDTOInputField } from './amount-dto-input-field';
|
||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||
import { ItemRow } from './item-row';
|
||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||
import { LastCellTabHook } from './last-cell-tab-hook';
|
||||
import { PercentageDTOInputField } from './percentage-dto-input-field';
|
||||
import { QuantityDTOInputField } from './quantity-dto-input-field';
|
||||
|
||||
interface ItemsEditorProps {
|
||||
value?: CustomerInvoiceItemFormData[];
|
||||
@ -105,183 +100,24 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className='text-sm'>
|
||||
{tableNav.fa.fields.map((f, rowIndex) => {
|
||||
|
||||
const isFirst = rowIndex === 0;
|
||||
const isLast = rowIndex === tableNav.fa.fields.length - 1;
|
||||
{tableNav.fa.fields.map((f, rowIndex) => (
|
||||
<ItemRow
|
||||
key={f.id}
|
||||
item={form.watch(`items.${rowIndex}`)}
|
||||
rowIndex={rowIndex}
|
||||
isSelected={selectedRows.has(rowIndex)}
|
||||
isFirst={rowIndex === 0}
|
||||
isLast={rowIndex === tableNav.fa.fields.length - 1}
|
||||
readOnly={readOnly}
|
||||
onToggleSelect={() => toggleRow(rowIndex)}
|
||||
onDuplicate={() => tableNav.duplicate(rowIndex)}
|
||||
onMoveUp={() => tableNav.moveUp(rowIndex)}
|
||||
onMoveDown={() => tableNav.moveDown(rowIndex)}
|
||||
onRemove={() => tableNav.remove(rowIndex)}
|
||||
/>
|
||||
))}
|
||||
|
||||
const item = form.watch(`items.${rowIndex}`);
|
||||
const totals = useCalcInvoiceItemTotals(item);
|
||||
|
||||
// sincronizar con react-hook-form
|
||||
React.useEffect(() => {
|
||||
form.setValue(`items.${rowIndex}.total_amount`, totals.totalDTO, { shouldDirty: true });
|
||||
}, [totals.totalDTO, form, rowIndex]);
|
||||
|
||||
|
||||
return (
|
||||
<TableRow key={`row-${f.id}`} data-row-index={rowIndex}>
|
||||
{/* selección */}
|
||||
<TableCell className='align-top'>
|
||||
<div className='h-5'>
|
||||
<Checkbox
|
||||
aria-label={t("common.select_row", { n: rowIndex + 1 })}
|
||||
checked={selectedRows.has(rowIndex)}
|
||||
disabled={readOnly}
|
||||
onCheckedChange={() => toggleRow(rowIndex)}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{/* # */}
|
||||
<TableCell className='text-left pt-[6px]'>
|
||||
<span className='block translate-y-[-1px] text-muted-foreground tabular-nums text-xs'>
|
||||
{rowIndex + 1}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
{/* description */}
|
||||
<TableCell>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${rowIndex}.description`}
|
||||
render={({ field }) => (
|
||||
<textarea
|
||||
{...field}
|
||||
aria-label={t("form_fields.item.description.label")}
|
||||
className='w-full resize-none bg-transparent p-0 pt-1.5 leading-5 min-h-8 focus:outline-none focus:bg-background'
|
||||
rows={1}
|
||||
spellCheck
|
||||
readOnly={readOnly}
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* qty */}
|
||||
<TableCell className='text-right'>
|
||||
<QuantityDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.quantity`}
|
||||
readOnly={readOnly}
|
||||
inputId={`quantity-${rowIndex}`}
|
||||
emptyMode='blank'
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* unit */}
|
||||
<TableCell className='text-right'>
|
||||
<AmountDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.unit_amount`}
|
||||
readOnly={readOnly}
|
||||
inputId={`unit-amount-${rowIndex}`}
|
||||
scale={4}
|
||||
locale={"es"}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* discount */}
|
||||
<TableCell className='text-right'>
|
||||
<PercentageDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.discount_percentage`}
|
||||
readOnly={readOnly}
|
||||
inputId={`discount-percentage-${rowIndex}`}
|
||||
showSuffix
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* taxes */}
|
||||
<TableCell>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${rowIndex}.tax_codes`}
|
||||
render={({ field }) => (
|
||||
<CustomerInvoiceTaxesMultiSelect
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
|
||||
{/* total (solo lectura) */}
|
||||
<TableCell className='text-right tabular-nums pt-[6px] leading-5'>
|
||||
<HoverCardTotalsSummary totals={totals}>
|
||||
<AmountDTOInputField
|
||||
control={control}
|
||||
name={`items.${rowIndex}.total_amount`}
|
||||
readOnly
|
||||
inputId={`total-amount-${rowIndex}`}
|
||||
locale="es"
|
||||
/>
|
||||
</HoverCardTotalsSummary>
|
||||
</TableCell>
|
||||
|
||||
{/* actions */}
|
||||
<TableCell className='pt-[4px]'>
|
||||
<div className='flex justify-end gap-0'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => tableNav.duplicate(rowIndex)}
|
||||
disabled={readOnly}
|
||||
aria-label='Duplicar fila'
|
||||
className='h-8 w-8 self-start translate-y-[-1px]'
|
||||
>
|
||||
<CopyIcon className='size-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Duplicar</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => tableNav.moveUp(rowIndex)}
|
||||
disabled={readOnly || isFirst}
|
||||
aria-label='Mover arriba'
|
||||
className='h-8 w-8 self-start translate-y-[-1px]'
|
||||
>
|
||||
<ArrowUp className='size-4' />
|
||||
</Button>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => tableNav.moveDown(rowIndex)}
|
||||
disabled={readOnly || isLast}
|
||||
aria-label='Mover abajo'
|
||||
className='h-8 w-8 self-start translate-y-[-1px]'
|
||||
>
|
||||
<ArrowDown className='size-4' />
|
||||
</Button>
|
||||
<Button
|
||||
size='icon'
|
||||
variant='ghost'
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => tableNav.remove(rowIndex)}
|
||||
disabled={readOnly}
|
||||
aria-label='Eliminar fila'
|
||||
className='h-8 w-8 self-start translate-y-[-1px]'
|
||||
>
|
||||
<Trash2 className='size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user