En una cotización, poder mover las líneas de detalle

This commit is contained in:
David Arranz 2025-02-18 12:27:08 +01:00
parent d432fb90d2
commit 1396c70d1f
15 changed files with 266 additions and 241 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@uecko-presupuestador/client", "name": "@uecko-presupuestador/client",
"private": true, "private": true,
"version": "1.1.1", "version": "1.1.2",
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -362,108 +362,127 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
{table.getSelectedRowModel().rows.length} {table.getSelectedRowModel().rows.length}
</Badge> </Badge>
) : null} ) : 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> </div>
)} )}
</DragOverlay>, </DragOverlay>,
document.body 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> </CardContent>
<CardFooter> <CardFooter>
<ButtonGroup> <ButtonGroup>

View File

@ -62,7 +62,7 @@ export function QuoteItemsSortableTableRow({
key={id} key={id}
id={String(id)} id={String(id)}
className={cn( className={cn(
isDragging ? "opacity-40" : "opacity-100", isDragging ? "opacity-60" : "opacity-100",
"m-0 hover:bg-muted hover:focus-within:bg-accent focus-within:bg-accent" "m-0 hover:bg-muted hover:focus-within:bg-accent focus-within:bg-accent"
)} )}
ref={setNodeRef} ref={setNodeRef}

View File

@ -177,7 +177,7 @@ export const QuoteDetailsCardEditor = ({
}, },
], ],
{ {
enableDragHandleColumn: false, // <--- Desactivado temporalmente enableDragHandleColumn: true,
enableSelectionColumn: true, enableSelectionColumn: true,
enableActionsColumn: true, enableActionsColumn: true,
rowActionFn: (props) => { rowActionFn: (props) => {

View File

@ -245,7 +245,7 @@ export const QuoteEdit = () => {
}; };
if (isSubmitting || isPending) { if (isSubmitting || isPending) {
return <LoadingOverlay title='Guardando cotización' />; //return <LoadingOverlay title='Guardando cotización' />;
} }
if (status === "error") { if (status === "error") {
@ -257,72 +257,79 @@ export const QuoteEdit = () => {
} }
return ( return (
<Form {...form}> <>
<form onSubmit={handleSubmit((data) => onSubmit(data, false))}> {(isSubmitting || isPending) && <LoadingOverlay title='Guardando cotización' />}
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'> <Form {...form}>
<div className='flex items-center gap-4'> <form onSubmit={handleSubmit((data) => onSubmit(data, false))}>
<BackHistoryButton /> <div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'> <div className='flex items-center gap-4'>
{t("quotes.edit.title")} {data.reference} <BackHistoryButton />
</h1> <h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
<ColorBadge label={t(`quotes.status.${data.status}`)} className='ml-auto sm:ml-0' /> {t("quotes.edit.title")} {data.reference}
</h1>
<ColorBadge label={t(`quotes.status.${data.status}`)} className='ml-auto sm:ml-0' />
<div className='items-center hidden gap-2 md:ml-auto md:flex'> <div className='items-center hidden gap-2 md:ml-auto md:flex'>
<CancelButton <CancelButton
label={t("common.close")} label={t("common.close")}
variant='secondary' variant='secondary'
size='sm' size='sm'
onClick={handleClose} onClick={handleClose}
/> />
<SubmitButton <SubmitButton
label={t("common.save")} label={t("common.save")}
size='sm' size='sm'
disabled={formState.isSubmitting || formState.isLoading || formState.isValidating} disabled={formState.isSubmitting || formState.isLoading || formState.isValidating}
/> />
<Button <Button
size='sm' size='sm'
disabled={formState.isSubmitting || formState.isLoading || formState.isValidating} disabled={formState.isSubmitting || formState.isLoading || formState.isValidating}
onClick={handleSubmit((data) => onSubmit(data, true))} onClick={handleSubmit((data) => onSubmit(data, true))}
> >
{t("common.save_close")} {t("common.save_close")}
</Button>
</div>
</div>
<QuoteGeneralCardEditor />
<QuotePricesResume />
<QuoteDetailsCardEditor
currency={quoteCurrency}
language={quoteLanguage}
defaultValues={defaultValues}
/>
<Tabs
defaultValue='items'
className='hidden space-y-4 '
value={activeTab}
onValueChange={setActiveTab}
>
<TabsList>
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
{/* <TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>*/}
</TabsList>
<TabsContent
value='general'
forceMount
hidden={"general" !== activeTab}
></TabsContent>
<TabsContent value='items' forceMount hidden={"items" !== activeTab}></TabsContent>
</Tabs>
<div className='flex items-center justify-center gap-2 md:hidden'>
<Button variant='outline' size='sm'>
{t("common.discard")}
</Button> </Button>
<Button size='sm'>{t("quotes.edit.buttons.save_quote")}</Button>
</div> </div>
</div> </div>
</form>
<QuoteGeneralCardEditor /> </Form>
<QuotePricesResume /> </>
<QuoteDetailsCardEditor
currency={quoteCurrency}
language={quoteLanguage}
defaultValues={defaultValues}
/>
<Tabs
defaultValue='items'
className='hidden space-y-4 '
value={activeTab}
onValueChange={setActiveTab}
>
<TabsList>
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
{/* <TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>*/}
</TabsList>
<TabsContent value='general' forceMount hidden={"general" !== activeTab}></TabsContent>
<TabsContent value='items' forceMount hidden={"items" !== activeTab}></TabsContent>
</Tabs>
<div className='flex items-center justify-center gap-2 md:hidden'>
<Button variant='outline' size='sm'>
{t("common.discard")}
</Button>
<Button size='sm'>{t("quotes.edit.buttons.save_quote")}</Button>
</div>
</div>
</form>
</Form>
); );
}; };

View File

@ -1,6 +1,7 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/ui"; import { Button } from "@/ui";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
import { t } from "i18next";
import { GripVerticalIcon } from "lucide-react"; import { GripVerticalIcon } from "lucide-react";
export interface DataTableRowDragHandleCellProps { export interface DataTableRowDragHandleCellProps {
@ -33,7 +34,7 @@ export const DataTableRowDragHandleCell = ({
{...listeners} {...listeners}
> >
<GripVerticalIcon className='w-4 h-4' /> <GripVerticalIcon className='w-4 h-4' />
<span className='sr-only'>Mover fila</span> <span className='sr-only'>{t("common.move_row")}</span>
</Button> </Button>
); );
}; };

View File

@ -14,7 +14,7 @@ export const LoadingOverlay = ({
return ( return (
<div <div
className={ className={
"fixed top-0 bottom-0 left-0 right-0 z-50 w-full h-screen overflow-hidden flex justify-center" "fixed top-0 bottom-0 left-0 right-0 z-50 w-full h-screen overflow-hidden flex justify-center bg-background/85"
} }
{...props} {...props}
> >

View File

@ -41,6 +41,7 @@
"append_article_tooltip": "Select and add an item from the catalog", "append_article_tooltip": "Select and add an item from the catalog",
"append_block": "Append text block", "append_block": "Append text block",
"append_block_tooltip": "Select and add a text block", "append_block_tooltip": "Select and add a text block",
"move_row": "Move row",
"remove_row": "Remove", "remove_row": "Remove",
"remove_selected_rows": "Remove", "remove_selected_rows": "Remove",
"remove_selected_rows_tooltip": "Remove selected row(s)", "remove_selected_rows_tooltip": "Remove selected row(s)",

View File

@ -41,6 +41,7 @@
"append_article_tooltip": "Elegir un artículo del catálogo y añadirlo", "append_article_tooltip": "Elegir un artículo del catálogo y añadirlo",
"append_block": "Añadir bloque de texto", "append_block": "Añadir bloque de texto",
"append_block_tooltip": "Elegir un bloque de texto y añadirlo", "append_block_tooltip": "Elegir un bloque de texto y añadirlo",
"move_row": "Mover fila",
"remove_row": "Eliminar", "remove_row": "Eliminar",
"remove_selected_rows": "Eliminar", "remove_selected_rows": "Eliminar",
"remove_selected_rows_tooltip": "Elimina las fila(s) seleccionadas(s)", "remove_selected_rows_tooltip": "Elimina las fila(s) seleccionadas(s)",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -8,8 +8,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" /> <link href="https://fonts.upset.dev/css2?family=Poppins&display=swap" rel="stylesheet" />
<title>Uecko</title> <title>Uecko</title>
<script type="module" crossorigin src="/assets/index-he4FVkJC.js"></script> <script type="module" crossorigin src="/assets/index-C4XjvTbB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B5XW7DrB.css"> <link rel="stylesheet" crossorigin href="/assets/index-ChUo3IMj.css">
</head> </head>
<body> <body>

View File

@ -1,6 +1,6 @@
{ {
"name": "uecko-presupuestador", "name": "uecko-presupuestador",
"version": "1.1.1", "version": "1.1.2",
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"license": "ISC", "license": "ISC",
"private": true, "private": true,

View File

@ -16,7 +16,7 @@ import { Router } from "express";
export const quoteRouter = (appRouter: Router): void => { export const quoteRouter = (appRouter: Router): void => {
const quoteRoutes: Router = Router({ mergeParams: true }); const quoteRoutes: Router = Router({ mergeParams: true });
// Users CRUD // Quotes CRUD
quoteRoutes.get("/", checkUser, getDealerMiddleware, handleRequest(listQuotesController)); quoteRoutes.get("/", checkUser, getDealerMiddleware, handleRequest(listQuotesController));
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, handleRequest(getQuoteController)); quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, handleRequest(getQuoteController));
quoteRoutes.post("/", checkUser, getDealerMiddleware, handleRequest(createQuoteController)); quoteRoutes.post("/", checkUser, getDealerMiddleware, handleRequest(createQuoteController));

View File

@ -8,14 +8,10 @@ export class Result<T, E extends Error = Error> {
protected constructor(props: { isSuccess: boolean; error?: E; object?: T }) { protected constructor(props: { isSuccess: boolean; error?: E; object?: T }) {
const { isSuccess, error, object } = props; const { isSuccess, error, object } = props;
if (isSuccess && error) { if (isSuccess && error) {
throw new Error( throw new Error(`InvalidOperation: A result cannot be successful and contain an error`);
`InvalidOperation: A result cannot be successful and contain an error`,
);
} }
if (!isSuccess && !error) { if (!isSuccess && !error) {
throw new Error( throw new Error(`InvalidOperation: A failing result needs to contain an error message`);
`InvalidOperation: A failing result needs to contain an error message`,
);
} }
this.isSuccess = isSuccess; this.isSuccess = isSuccess;