feat: implement proforma deletion and listing enhancements
- Added endpoint for retrieving deleted proformas with logical deletion. - Updated proforma listing to support active, archived, and deleted scopes. - Refactored repository and service layers to accommodate new proforma list scope. - Enhanced UI to display separate views for active, archived, and deleted proformas. - Updated localization files to reflect changes in proforma management. - Improved query handling to filter proformas based on their status and scope.
This commit is contained in:
parent
a264f43b95
commit
1bf85a05ff
@ -8,13 +8,20 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import type { AppSidebarNavSection, AppSidebarSectionAccent } from "./app-sidebar.config";
|
import type {
|
||||||
|
AppSidebarNavItem,
|
||||||
|
AppSidebarNavSection,
|
||||||
|
AppSidebarSectionAccent,
|
||||||
|
} from "./app-sidebar.config";
|
||||||
|
|
||||||
interface AppSidebarNavProps {
|
interface AppSidebarNavProps {
|
||||||
sections: AppSidebarNavSection[];
|
sections: AppSidebarNavSection[];
|
||||||
@ -29,9 +36,11 @@ interface SidebarSectionAccentStyles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AppSidebarNav = ({ sections }: AppSidebarNavProps) => {
|
export const AppSidebarNav = ({ sections }: AppSidebarNavProps) => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
const [openSections, setOpenSections] = React.useState<Record<string, boolean>>(() =>
|
const [openSections, setOpenSections] = React.useState<Record<string, boolean>>(() =>
|
||||||
Object.fromEntries(sections.map((s) => [s.title, true]))
|
Object.fromEntries(sections.map((s) => [s.title, true]))
|
||||||
);
|
);
|
||||||
|
const [openItems, setOpenItems] = React.useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const sectionAccentStyles: Record<AppSidebarSectionAccent, SidebarSectionAccentStyles> = {
|
const sectionAccentStyles: Record<AppSidebarSectionAccent, SidebarSectionAccentStyles> = {
|
||||||
blue: {
|
blue: {
|
||||||
@ -67,6 +76,25 @@ export const AppSidebarNav = ({ sections }: AppSidebarNavProps) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRouteActive = React.useCallback(
|
||||||
|
(item: AppSidebarNavItem): boolean => {
|
||||||
|
if (item.items?.length) {
|
||||||
|
return item.items.some((child) => isRouteActive(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.href) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.exact) {
|
||||||
|
return pathname === item.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||||
|
},
|
||||||
|
[pathname]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="space-y-2 pt-4">
|
<nav className="space-y-2 pt-4">
|
||||||
{sections.map((section) => {
|
{sections.map((section) => {
|
||||||
@ -121,32 +149,98 @@ export const AppSidebarNav = ({ sections }: AppSidebarNavProps) => {
|
|||||||
<SidebarMenu className="px-1">
|
<SidebarMenu className="px-1">
|
||||||
{section.items.map((item) => {
|
{section.items.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = false;
|
const isActive = isRouteActive(item);
|
||||||
|
const hasChildren = Boolean(item.items?.length);
|
||||||
|
const isItemOpen = openItems[item.title] ?? isActive;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={item.href}>
|
<SidebarMenuItem key={item.href}>
|
||||||
<SidebarMenuButton
|
{hasChildren ? (
|
||||||
className={cn(
|
<Collapsible
|
||||||
"h-9 rounded-md px-3 text-sm font-medium text-foreground/80",
|
onOpenChange={(open) =>
|
||||||
"hover:bg-muted hover:text-foreground",
|
setOpenItems((current) => ({
|
||||||
isActive && styles.activeItem
|
...current,
|
||||||
)}
|
[item.title]: open,
|
||||||
isActive={isActive}
|
}))
|
||||||
render={
|
}
|
||||||
<Link to={item.href}>
|
open={isItemOpen}
|
||||||
<Icon
|
>
|
||||||
aria-hidden="true"
|
<SidebarMenuButton
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-4 shrink-0",
|
"h-9 rounded-md px-3 text-sm font-medium text-foreground/80",
|
||||||
isActive ? styles.activeIcon : styles.inactiveIcon
|
"hover:bg-muted hover:text-foreground",
|
||||||
)}
|
isActive && styles.activeItem
|
||||||
/>
|
)}
|
||||||
<span className="truncate group-data-[collapsible=icon]:hidden">
|
isActive={isActive}
|
||||||
{item.title}
|
render={
|
||||||
</span>
|
<CollapsibleTrigger
|
||||||
</Link>
|
render={
|
||||||
}
|
<button className="flex w-full items-center gap-2" type="button">
|
||||||
/>
|
<Icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"size-4 shrink-0",
|
||||||
|
isActive ? styles.activeIcon : styles.inactiveIcon
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="truncate group-data-[collapsible=icon]:hidden">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto size-3.5 transition-transform duration-200 group-data-[collapsible=icon]:hidden",
|
||||||
|
isItemOpen && "rotate-180"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.items?.map((child) => {
|
||||||
|
const childIsActive = isRouteActive(child);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarMenuSubItem key={child.href}>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
isActive={childIsActive}
|
||||||
|
render={<Link to={child.href ?? "#"} />}
|
||||||
|
>
|
||||||
|
<span>{child.title}</span>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
) : (
|
||||||
|
<SidebarMenuButton
|
||||||
|
className={cn(
|
||||||
|
"h-9 rounded-md px-3 text-sm font-medium text-foreground/80",
|
||||||
|
"hover:bg-muted hover:text-foreground",
|
||||||
|
isActive && styles.activeItem
|
||||||
|
)}
|
||||||
|
isActive={isActive}
|
||||||
|
render={
|
||||||
|
<Link to={item.href ?? "#"}>
|
||||||
|
<Icon
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn(
|
||||||
|
"size-4 shrink-0",
|
||||||
|
isActive ? styles.activeIcon : styles.inactiveIcon
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="truncate group-data-[collapsible=icon]:hidden">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -23,8 +23,10 @@ import {
|
|||||||
|
|
||||||
export interface AppSidebarNavItem {
|
export interface AppSidebarNavItem {
|
||||||
title: string;
|
title: string;
|
||||||
href: string;
|
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
href?: string;
|
||||||
|
exact?: boolean;
|
||||||
|
items?: AppSidebarNavItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppSidebarSectionAccent = "blue" | "green" | "amber" | "violet";
|
export type AppSidebarSectionAccent = "blue" | "green" | "amber" | "violet";
|
||||||
@ -40,7 +42,32 @@ export const appSidebarNavSections: AppSidebarNavSection[] = [
|
|||||||
title: "Ventas",
|
title: "Ventas",
|
||||||
accent: "blue",
|
accent: "blue",
|
||||||
items: [
|
items: [
|
||||||
{ title: "Proformas", href: "/proformas", icon: FileTextIcon },
|
{
|
||||||
|
title: "Proformas",
|
||||||
|
href: "/proformas",
|
||||||
|
icon: FileTextIcon,
|
||||||
|
items: [
|
||||||
|
{ title: "Proformas", href: "/proformas", icon: FileTextIcon, exact: true },
|
||||||
|
{
|
||||||
|
title: "Archivadas",
|
||||||
|
href: "/proformas/archived",
|
||||||
|
icon: FileTextIcon,
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Eliminadas",
|
||||||
|
href: "/proformas/deleted",
|
||||||
|
icon: FileTextIcon,
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Nueva proforma",
|
||||||
|
href: "/proformas/create",
|
||||||
|
icon: FileTextIcon,
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{ title: "Pedidos de venta", href: "/sales-orders", icon: ShoppingCartIcon },
|
{ title: "Pedidos de venta", href: "/sales-orders", icon: ShoppingCartIcon },
|
||||||
{ title: "Clientes", href: "/customers", icon: UsersIcon },
|
{ title: "Clientes", href: "/customers", icon: UsersIcon },
|
||||||
{ title: "Facturación", href: "/customer-invoices", icon: ReceiptIcon },
|
{ title: "Facturación", href: "/customer-invoices", icon: ReceiptIcon },
|
||||||
|
|||||||
@ -1,39 +1,26 @@
|
|||||||
# Proforma archive contract
|
# Proformas archivadas
|
||||||
|
|
||||||
## Rules
|
## Endpoint
|
||||||
|
|
||||||
- Archive uses `archived_at` and does not change `status`.
|
- `GET /proformas/archived`
|
||||||
- Only `draft` and `rejected` proformas can be archived in V1.
|
|
||||||
- Only `draft` and `rejected` archived proformas can be unarchived in V1.
|
|
||||||
- Archived proformas remain available through `GET /proformas/:id`.
|
|
||||||
- Deleted proformas remain excluded from every flow.
|
|
||||||
- Numbering is never reused and `document_series.next_number` is untouched.
|
|
||||||
- Archived `draft` proformas can be deleted directly without unarchiving.
|
|
||||||
|
|
||||||
## Endpoints
|
## Semántica
|
||||||
|
|
||||||
- `PATCH /proformas/:proforma_id/archive`
|
- Devuelve solo proformas con `archived_at IS NOT NULL`.
|
||||||
- `PATCH /proformas/:proforma_id/unarchive`
|
- Excluye registros con `deleted_at IS NOT NULL`.
|
||||||
|
- La búsqueda `q` se ejecuta dentro del scope archivado.
|
||||||
|
|
||||||
Both endpoints:
|
## Query soportada
|
||||||
|
|
||||||
- return `200` with the updated proforma snapshot
|
- `q`
|
||||||
- return `404` when the proforma does not exist, belongs to another company, or is deleted
|
- `status`
|
||||||
- return `409` when the state is not allowed, when the proforma is already archived/non-archived, or when it is linked to an issued invoice
|
- `pageNumber`
|
||||||
|
- `pageSize`
|
||||||
|
- `orderBy`
|
||||||
|
- `order`
|
||||||
|
|
||||||
## Listing behavior
|
## UI
|
||||||
|
|
||||||
- `GET /proformas` excludes archived proformas by default through `filters[]=archived_at IS_NULL`
|
- Ruta frontend: `/proformas/archived`
|
||||||
- the backend does not expose `archive_view` as a query param
|
- Reutiliza el listado compartido con scope `archived`.
|
||||||
- the frontend keeps `archiveView=active|archived|all` only as UI state and translates it to `Criteria filters[]`
|
- Mantiene selector de estado, paginación y orden.
|
||||||
- active view sends `archived_at IS_NULL`
|
|
||||||
- archived view sends `archived_at IS_NOT_NULL`
|
|
||||||
- all view sends no `archived_at` filter
|
|
||||||
- `q` always applies inside the scope produced by `filters[]`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- `archived` is a visibility dimension, not a business status
|
|
||||||
- V1 does not support archiving `sent`, `approved`, or `issued`
|
|
||||||
- V1 does not add `archived_by` or `archive_reason`
|
|
||||||
- soft-deleted proformas stay excluded structurally by `deleted_at`
|
|
||||||
|
|||||||
@ -1,45 +1,26 @@
|
|||||||
# Proforma Delete Contract
|
# Proformas eliminadas
|
||||||
|
|
||||||
## Objetivo
|
## Endpoint
|
||||||
|
|
||||||
Definir el borrado funcional de proformas sin romper trazabilidad ni numeracion.
|
- `GET /proformas/deleted`
|
||||||
|
|
||||||
## Reglas funcionales
|
## Semántica
|
||||||
|
|
||||||
- solo se pueden borrar proformas en estado `draft`
|
- Devuelve solo proformas con `deleted_at IS NOT NULL`.
|
||||||
- `sent`, `approved`, `rejected` e `issued` devuelven conflicto
|
- Usa `paranoid: false` en persistencia para poder listarlas.
|
||||||
- `rejected` no se borra; queda reservado para una futura operacion de archivado
|
- La búsqueda `q` se ejecuta dentro del scope eliminado.
|
||||||
- el borrado es logico mediante `deleted_at`
|
|
||||||
- el borrado de `draft` no depende de `archived_at`
|
|
||||||
- una proforma `draft` archivada puede borrarse directamente
|
|
||||||
- una proforma `rejected` archivada no puede borrarse
|
|
||||||
- la numeracion no se reutiliza
|
|
||||||
- no se decrementa `document_series.next_number`
|
|
||||||
- no se borran fisicamente `proforma_items` ni `proforma_taxes`
|
|
||||||
|
|
||||||
## Validaciones defensivas
|
## Query soportada
|
||||||
|
|
||||||
- si `linked_invoice_id` tiene valor, el borrado se bloquea
|
- `q`
|
||||||
- si existe `issued_invoices.source_proforma_id = proforma.id`, el borrado se bloquea
|
- `status`
|
||||||
|
- `pageNumber`
|
||||||
## Contrato REST
|
- `pageSize`
|
||||||
|
- `orderBy`
|
||||||
- endpoint: `DELETE /proformas/:proforma_id`
|
- `order`
|
||||||
- respuesta satisfactoria: `204 No Content`
|
|
||||||
- `404 Not Found` si no existe, no pertenece a la empresa activa o ya esta borrada
|
|
||||||
- `409 Conflict` si existe pero no cumple las reglas de borrado
|
|
||||||
|
|
||||||
## Efecto operativo
|
|
||||||
|
|
||||||
- `GET /proformas/:id` debe tratar la proforma borrada como inexistente
|
|
||||||
- `GET /proformas` no debe incluir proformas borradas
|
|
||||||
- `GET /proformas` puede listar activas, archivadas o todas segun `Criteria filters[]`, pero nunca borradas
|
|
||||||
- `PUT /proformas/:id` no debe editar proformas borradas
|
|
||||||
- `POST /proformas/:id/issue` no debe emitir proformas borradas
|
|
||||||
|
|
||||||
## UI
|
## UI
|
||||||
|
|
||||||
- la accion `Eliminar` solo debe mostrarse para proformas `draft`
|
- Ruta frontend: `/proformas/deleted`
|
||||||
- en la vista de archivadas, `Eliminar` tambien debe mostrarse para `draft`
|
- Vista read-only V1.
|
||||||
- la confirmacion debe advertir que la referencia no se reutilizara
|
- No implementa restauración, purge ni force delete.
|
||||||
- en esta fase la accion esta disponible en el listado activo y en archivadas para `draft`
|
|
||||||
|
|||||||
@ -1,47 +1,26 @@
|
|||||||
# Proforma List Contract
|
# Proformas activas
|
||||||
|
|
||||||
## UI model
|
## Endpoint
|
||||||
|
|
||||||
- frontend keeps `archiveView=active|archived|all` as semantic UI state
|
- `GET /proformas`
|
||||||
- frontend keeps `status=all|draft|sent|approved|rejected|issued`
|
|
||||||
- `archiveView` is not a backend query param
|
|
||||||
|
|
||||||
## Backend contract
|
## Semántica
|
||||||
|
|
||||||
`GET /proformas` stays based on `Criteria`:
|
- Devuelve solo proformas con `archived_at IS NULL`.
|
||||||
|
- Devuelve solo proformas con `deleted_at IS NULL`.
|
||||||
|
- No admite filtros frontend sobre `archived_at` ni `deleted_at`.
|
||||||
|
|
||||||
|
## Query soportada
|
||||||
|
|
||||||
- `q`
|
- `q`
|
||||||
|
- `status`
|
||||||
- `pageNumber`
|
- `pageNumber`
|
||||||
- `pageSize`
|
- `pageSize`
|
||||||
- `orderBy`
|
- `orderBy`
|
||||||
- `order`
|
- `order`
|
||||||
- `filters[]`
|
|
||||||
|
|
||||||
## Allowed filters
|
## UI
|
||||||
|
|
||||||
- `archived_at IS_NULL`
|
- Ruta frontend: `/proformas`
|
||||||
- `archived_at IS_NOT_NULL`
|
- Mantiene búsqueda, paginación, orden y selector de estado.
|
||||||
- `status EQUALS draft|sent|approved|rejected|issued`
|
- No muestra selector visual de archivadas/todas.
|
||||||
|
|
||||||
## Scope rules
|
|
||||||
|
|
||||||
- active view -> `archived_at IS_NULL`
|
|
||||||
- archived view -> `archived_at IS_NOT_NULL`
|
|
||||||
- all view -> no `archived_at` filter
|
|
||||||
- status `all` -> no `status` filter
|
|
||||||
- any concrete status -> `status EQUALS <value>`
|
|
||||||
- `q` applies after the scope is defined by `filters[]`
|
|
||||||
|
|
||||||
## Resource visibility
|
|
||||||
|
|
||||||
- `GET /proformas` defaults to active scope
|
|
||||||
- `GET /proformas/:id` returns archived proformas too
|
|
||||||
- soft-deleted proformas never appear in list results
|
|
||||||
- soft-deleted proformas are treated as non-existent by `GET /proformas/:id`
|
|
||||||
|
|
||||||
## Frontend persistence
|
|
||||||
|
|
||||||
- URL stores `archiveView`
|
|
||||||
- URL stores `status`
|
|
||||||
- URL stores `q`
|
|
||||||
- `localStorage["proformas:list:grid"]` keeps only table preferences
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import type { InvoiceStatus, Proforma } from "../../../domain";
|
import type { InvoiceStatus, Proforma } from "../../../domain";
|
||||||
import type { ProformaSummary } from "../models";
|
import type { ProformaSummary } from "../models";
|
||||||
|
import type { ProformaListScope } from "../use-cases";
|
||||||
|
|
||||||
export interface IProformaRepository {
|
export interface IProformaRepository {
|
||||||
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
|
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
|
||||||
@ -30,6 +31,7 @@ export interface IProformaRepository {
|
|||||||
|
|
||||||
findByCriteriaInCompany(
|
findByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
|
scope: ProformaListScope,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: unknown
|
transaction: unknown
|
||||||
): Promise<Result<Collection<ProformaSummary>, Error>>;
|
): Promise<Result<Collection<ProformaSummary>, Error>>;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
|
|||||||
import type { Proforma } from "../../../domain";
|
import type { Proforma } from "../../../domain";
|
||||||
import type { ProformaSummary } from "../models";
|
import type { ProformaSummary } from "../models";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
import type { ProformaListScope } from "../use-cases";
|
||||||
|
|
||||||
export interface IProformaFinder {
|
export interface IProformaFinder {
|
||||||
findProformaById(
|
findProformaById(
|
||||||
@ -27,6 +28,7 @@ export interface IProformaFinder {
|
|||||||
|
|
||||||
findProformasByCriteria(
|
findProformasByCriteria(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
|
scope: ProformaListScope,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: unknown
|
transaction?: unknown
|
||||||
): Promise<Result<Collection<ProformaSummary>, Error>>;
|
): Promise<Result<Collection<ProformaSummary>, Error>>;
|
||||||
@ -61,9 +63,10 @@ export class ProformaFinder implements IProformaFinder {
|
|||||||
|
|
||||||
async findProformasByCriteria(
|
async findProformasByCriteria(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
|
scope: ProformaListScope,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction?: unknown
|
transaction?: unknown
|
||||||
): Promise<Result<Collection<ProformaSummary>, Error>> {
|
): Promise<Result<Collection<ProformaSummary>, Error>> {
|
||||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
return this.repository.findByCriteriaInCompany(companyId, scope, criteria, transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,12 @@ import { Result } from "@repo/rdx-utils";
|
|||||||
import type { IProformaFinder } from "../services";
|
import type { IProformaFinder } from "../services";
|
||||||
import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders";
|
import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
|
export type ProformaListScope = "normal" | "archived" | "deleted";
|
||||||
|
|
||||||
type ListProformasUseCaseInput = {
|
type ListProformasUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
criteria: Criteria;
|
criteria: Criteria;
|
||||||
|
scope: ProformaListScope;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ListProformasUseCase {
|
export class ListProformasUseCase {
|
||||||
@ -19,11 +22,16 @@ export class ListProformasUseCase {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute(params: ListProformasUseCaseInput) {
|
public execute(params: ListProformasUseCaseInput) {
|
||||||
const { criteria, companyId } = params;
|
const { criteria, companyId, scope } = params;
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.finder.findProformasByCriteria(companyId, criteria, transaction);
|
const result = await this.finder.findProformasByCriteria(
|
||||||
|
companyId,
|
||||||
|
scope,
|
||||||
|
criteria,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
if (result.isFailure) {
|
if (result.isFailure) {
|
||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
|
|||||||
@ -6,12 +6,11 @@ import {
|
|||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
import { Criteria } from "@repo/rdx-criteria/server";
|
import { Criteria } from "@repo/rdx-criteria/server";
|
||||||
|
|
||||||
import type { ListProformasUseCase } from "../../../../application/index.ts";
|
import type { ListProformasUseCase, ProformaListScope } from "../../../../application/index.ts";
|
||||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||||
|
|
||||||
export class ListProformasController extends ExpressController {
|
export class ListProformasController extends ExpressController {
|
||||||
private static readonly ALLOWED_FILTERS = {
|
private static readonly ALLOWED_FILTERS = {
|
||||||
archived_at: new Set(["IS_NULL", "IS_NOT_NULL"]),
|
|
||||||
status: new Set(["EQUALS"]),
|
status: new Set(["EQUALS"]),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -23,7 +22,10 @@ export class ListProformasController extends ExpressController {
|
|||||||
"issued",
|
"issued",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public constructor(private readonly useCase: ListProformasUseCase) {
|
public constructor(
|
||||||
|
private readonly useCase: ListProformasUseCase,
|
||||||
|
private readonly scope: ProformaListScope
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.errorMapper = proformasApiErrorMapper;
|
this.errorMapper = proformasApiErrorMapper;
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ export class ListProformasController extends ExpressController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const criteria = this.getCriteriaWithDefaultOrder();
|
const criteria = this.getCriteriaWithDefaultOrder();
|
||||||
const result = await this.useCase.execute({ criteria, companyId });
|
const result = await this.useCase.execute({ criteria, companyId, scope: this.scope });
|
||||||
|
|
||||||
return result.match(
|
return result.match(
|
||||||
(data) =>
|
(data) =>
|
||||||
|
|||||||
@ -62,7 +62,30 @@ export const proformasRouter = (params: StartParams) => {
|
|||||||
validateRequest(ListProformasRequestSchema, "query"),
|
validateRequest(ListProformasRequestSchema, "query"),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.listProformas();
|
const useCase = deps.useCases.listProformas();
|
||||||
const controller = new ListProformasController(useCase /*, deps.presenters.list */);
|
const controller = new ListProformasController(useCase, "normal" /*, deps.presenters.list */);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/archived",
|
||||||
|
validateRequest(ListProformasRequestSchema, "query"),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.listProformas();
|
||||||
|
const controller = new ListProformasController(
|
||||||
|
useCase,
|
||||||
|
"archived" /*, deps.presenters.list */
|
||||||
|
);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/deleted",
|
||||||
|
validateRequest(ListProformasRequestSchema, "query"),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.listProformas();
|
||||||
|
const controller = new ListProformasController(useCase, "deleted" /*, deps.presenters.list */);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
} from "sequelize";
|
} from "sequelize";
|
||||||
|
|
||||||
import type { IProformaRepository, ProformaSummary } from "../../../../../application";
|
import type { IProformaRepository, ProformaSummary } from "../../../../../application";
|
||||||
|
import type { ProformaListScope } from "../../../../../application";
|
||||||
import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../../../../../domain";
|
import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../../../../../domain";
|
||||||
import {
|
import {
|
||||||
IssuedInvoiceModel,
|
IssuedInvoiceModel,
|
||||||
@ -40,7 +41,6 @@ export class SequelizeProformaRepositoryV2
|
|||||||
implements IProformaRepository
|
implements IProformaRepository
|
||||||
{
|
{
|
||||||
private static readonly ALLOWED_FILTERS = {
|
private static readonly ALLOWED_FILTERS = {
|
||||||
archived_at: new Set(["IS_NULL", "IS_NOT_NULL"]),
|
|
||||||
status: new Set(["EQUALS"]),
|
status: new Set(["EQUALS"]),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -337,6 +337,7 @@ export class SequelizeProformaRepositoryV2
|
|||||||
|
|
||||||
public async findByCriteriaInCompany(
|
public async findByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
|
scope: ProformaListScope,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
options: FindOptions<InferAttributes<ProformaModel>> = {}
|
options: FindOptions<InferAttributes<ProformaModel>> = {}
|
||||||
@ -411,6 +412,7 @@ export class SequelizeProformaRepositoryV2
|
|||||||
const baseWhere: WhereOptions<InferAttributes<ProformaModel>> = {
|
const baseWhere: WhereOptions<InferAttributes<ProformaModel>> = {
|
||||||
company_id: companyId.toString(),
|
company_id: companyId.toString(),
|
||||||
};
|
};
|
||||||
|
const scopeWhere = this.buildScopeWhere(scope);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composición defensiva del WHERE.
|
* Composición defensiva del WHERE.
|
||||||
@ -421,6 +423,7 @@ export class SequelizeProformaRepositoryV2
|
|||||||
const where: WhereOptions<InferAttributes<ProformaModel>> = {
|
const where: WhereOptions<InferAttributes<ProformaModel>> = {
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
baseWhere,
|
baseWhere,
|
||||||
|
scopeWhere,
|
||||||
...(options.where ? [options.where] : []),
|
...(options.where ? [options.where] : []),
|
||||||
...(criteriaQuery.where ? [criteriaQuery.where] : []),
|
...(criteriaQuery.where ? [criteriaQuery.where] : []),
|
||||||
],
|
],
|
||||||
@ -486,6 +489,7 @@ export class SequelizeProformaRepositoryV2
|
|||||||
const findQuery: FindOptions<InferAttributes<ProformaModel>> = {
|
const findQuery: FindOptions<InferAttributes<ProformaModel>> = {
|
||||||
...options,
|
...options,
|
||||||
...criteriaQuery,
|
...criteriaQuery,
|
||||||
|
paranoid: scope !== "deleted",
|
||||||
where,
|
where,
|
||||||
include,
|
include,
|
||||||
order,
|
order,
|
||||||
@ -502,6 +506,7 @@ export class SequelizeProformaRepositoryV2
|
|||||||
* y `col` deben tiparse con `CountOptions`.
|
* y `col` deben tiparse con `CountOptions`.
|
||||||
*/
|
*/
|
||||||
const countQuery: CountOptions<InferAttributes<ProformaModel>> = {
|
const countQuery: CountOptions<InferAttributes<ProformaModel>> = {
|
||||||
|
paranoid: scope !== "deleted",
|
||||||
where,
|
where,
|
||||||
include: getCountIncludes(include),
|
include: getCountIncludes(include),
|
||||||
distinct: true,
|
distinct: true,
|
||||||
@ -539,4 +544,26 @@ export class SequelizeProformaRepositoryV2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildScopeWhere(scope: ProformaListScope): WhereOptions<InferAttributes<ProformaModel>> {
|
||||||
|
switch (scope) {
|
||||||
|
case "archived":
|
||||||
|
return {
|
||||||
|
archived_at: {
|
||||||
|
[Op.ne]: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case "deleted":
|
||||||
|
return {
|
||||||
|
deleted_at: {
|
||||||
|
[Op.ne]: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case "normal":
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
archived_at: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
IssuedInvoiceModel,
|
IssuedInvoiceModel,
|
||||||
} from "../../../../common";
|
} from "../../../../common";
|
||||||
import type { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../mappers";
|
import type { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../mappers";
|
||||||
|
import type { ProformaListScope } from "../../../../../application";
|
||||||
|
|
||||||
export class ProformaRepository
|
export class ProformaRepository
|
||||||
extends SequelizeRepository<Proforma>
|
extends SequelizeRepository<Proforma>
|
||||||
@ -428,6 +429,7 @@ export class ProformaRepository
|
|||||||
*/
|
*/
|
||||||
public async findByCriteriaInCompany(
|
public async findByCriteriaInCompany(
|
||||||
companyId: UniqueID,
|
companyId: UniqueID,
|
||||||
|
_scope: ProformaListScope,
|
||||||
criteria: Criteria,
|
criteria: Criteria,
|
||||||
transaction: Transaction,
|
transaction: Transaction,
|
||||||
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
||||||
|
|||||||
@ -183,15 +183,16 @@
|
|||||||
"description": "Manage your customer proformas",
|
"description": "Manage your customer proformas",
|
||||||
"list": {
|
"list": {
|
||||||
"title": "Customer proformas",
|
"title": "Customer proformas",
|
||||||
"description": "List all customer proformas",
|
"description": "List non-archived and non-deleted customer proformas",
|
||||||
|
"loadErrorTitle": "Unable to load the proformas list",
|
||||||
|
"empty": {
|
||||||
|
"normal": "There are no proformas.",
|
||||||
|
"archived": "There are no archived proformas.",
|
||||||
|
"deleted": "There are no deleted proformas."
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"status": {
|
"status": {
|
||||||
"label": "Status"
|
"label": "Status"
|
||||||
},
|
|
||||||
"archive_view": {
|
|
||||||
"active": "Active",
|
|
||||||
"archived": "Archived",
|
|
||||||
"all": "All"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"columns": {
|
"columns": {
|
||||||
@ -215,6 +216,14 @@
|
|||||||
"total_amount": "Total"
|
"total_amount": "Total"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"archived": {
|
||||||
|
"title": "Archived proformas",
|
||||||
|
"description": "Review archived proformas without mixing them into the operational list"
|
||||||
|
},
|
||||||
|
"deleted": {
|
||||||
|
"title": "Deleted proformas",
|
||||||
|
"description": "Review deleted proformas in read-only mode"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "New customer proforma",
|
"title": "New customer proforma",
|
||||||
"description": "Create a new customer proforma",
|
"description": "Create a new customer proforma",
|
||||||
|
|||||||
@ -184,15 +184,16 @@
|
|||||||
"description": "Gestiona tus proformas",
|
"description": "Gestiona tus proformas",
|
||||||
"list": {
|
"list": {
|
||||||
"title": "Proformas",
|
"title": "Proformas",
|
||||||
"description": "Lista todas las proformas",
|
"description": "Lista las proformas no archivadas ni eliminadas",
|
||||||
|
"loadErrorTitle": "No se pudo cargar el listado de proformas",
|
||||||
|
"empty": {
|
||||||
|
"normal": "No hay proformas.",
|
||||||
|
"archived": "No hay proformas archivadas.",
|
||||||
|
"deleted": "No hay proformas eliminadas."
|
||||||
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"status": {
|
"status": {
|
||||||
"label": "Estado"
|
"label": "Estado"
|
||||||
},
|
|
||||||
"archive_view": {
|
|
||||||
"active": "Activas",
|
|
||||||
"archived": "Archivadas",
|
|
||||||
"all": "Todas"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"columns": {
|
"columns": {
|
||||||
@ -216,6 +217,14 @@
|
|||||||
"total_amount": "Importe total"
|
"total_amount": "Importe total"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"archived": {
|
||||||
|
"title": "Proformas archivadas",
|
||||||
|
"description": "Consulta las proformas archivadas sin mezclar el listado operativo"
|
||||||
|
},
|
||||||
|
"deleted": {
|
||||||
|
"title": "Proformas eliminadas",
|
||||||
|
"description": "Consulta las proformas eliminadas en modo solo lectura"
|
||||||
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Nueva proforma",
|
"title": "Nueva proforma",
|
||||||
"description": "Crear una nueva proforma",
|
"description": "Crear una nueva proforma",
|
||||||
|
|||||||
@ -10,6 +10,14 @@ const ProformasListPage = lazy(() =>
|
|||||||
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ArchivedProformasListPage = lazy(() =>
|
||||||
|
import("./proformas/list").then((m) => ({ default: m.ListArchivedProformasPage }))
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeletedProformasListPage = lazy(() =>
|
||||||
|
import("./proformas/list").then((m) => ({ default: m.ListDeletedProformasPage }))
|
||||||
|
);
|
||||||
|
|
||||||
const ProformasCreatePage = lazy(() =>
|
const ProformasCreatePage = lazy(() =>
|
||||||
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
|
||||||
);
|
);
|
||||||
@ -48,6 +56,14 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
path: "list",
|
path: "list",
|
||||||
element: <ProformasListPage />,
|
element: <ProformasListPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "archived",
|
||||||
|
element: <ArchivedProformasListPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "deleted",
|
||||||
|
element: <DeletedProformasListPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,14 @@ import {
|
|||||||
useProformaArchiveMutation,
|
useProformaArchiveMutation,
|
||||||
useProformaUnarchiveMutation,
|
useProformaUnarchiveMutation,
|
||||||
} from "../../shared";
|
} from "../../shared";
|
||||||
|
import type { ProformaListScope } from "../types/proforma-list-filters";
|
||||||
|
|
||||||
import { useListProformasController } from "./use-list-proformas.controller";
|
import { useListProformasController } from "./use-list-proformas.controller";
|
||||||
import { useProformaSummaryPanelController } from "./use-proforma-summary-panel.controller";
|
import { useProformaSummaryPanelController } from "./use-proforma-summary-panel.controller";
|
||||||
|
|
||||||
export const useListProformasPageController = () => {
|
export const useListProformasPageController = (scope: ProformaListScope) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const listCtrl = useListProformasController();
|
const listCtrl = useListProformasController(scope);
|
||||||
const deleteDialogCtrl = useDeleteProformaDialogController();
|
const deleteDialogCtrl = useDeleteProformaDialogController();
|
||||||
const issueDialogCtrl = useIssueProformaDialogController();
|
const issueDialogCtrl = useIssueProformaDialogController();
|
||||||
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
|
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
|
||||||
|
|||||||
@ -15,10 +15,7 @@ import {
|
|||||||
type ProformaStatus,
|
type ProformaStatus,
|
||||||
useProformasListQuery,
|
useProformasListQuery,
|
||||||
} from "../../shared";
|
} from "../../shared";
|
||||||
import type {
|
import type { ProformaListScope, ProformaListStatusFilter } from "../types/proforma-list-filters";
|
||||||
ProformaArchiveView,
|
|
||||||
ProformaListStatusFilter,
|
|
||||||
} from "../types/proforma-list-filters";
|
|
||||||
import { buildListProformasCriteria } from "../utils/build-list-proformas-criteria";
|
import { buildListProformasCriteria } from "../utils/build-list-proformas-criteria";
|
||||||
|
|
||||||
// Datos por defecto mientras se carga la consulta o en caso de error.
|
// Datos por defecto mientras se carga la consulta o en caso de error.
|
||||||
@ -54,16 +51,13 @@ const isSortDirection = (value: string | null): value is DataTableSortDirection
|
|||||||
return value === "asc" || value === "desc";
|
return value === "asc" || value === "desc";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useListProformasController = () => {
|
export const useListProformasController = (scope: ProformaListScope) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [search, setSearch] = useState(searchParams.get("q") ?? "");
|
const [search, setSearch] = useState(searchParams.get("q") ?? "");
|
||||||
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>(
|
const [statusFilter, setStatusFilter] = useState<ProformaListStatusFilter>(
|
||||||
((searchParams.get("status") as ProformaStatus | "all" | null) ??
|
((searchParams.get("status") as ProformaStatus | "all" | null) ??
|
||||||
"all") as ProformaListStatusFilter
|
"all") as ProformaListStatusFilter
|
||||||
);
|
);
|
||||||
const [archiveView, setArchiveView] = useState<ProformaArchiveView>(
|
|
||||||
(searchParams.get("archiveView") as ProformaArchiveView | null) ?? "active"
|
|
||||||
);
|
|
||||||
|
|
||||||
const tablePreferences = useDataTablePreferences({
|
const tablePreferences = useDataTablePreferences({
|
||||||
storageKey: "proformas:list:grid",
|
storageKey: "proformas:list:grid",
|
||||||
@ -102,7 +96,6 @@ export const useListProformasController = () => {
|
|||||||
((searchParams.get("status") as ProformaStatus | "all" | null) ??
|
((searchParams.get("status") as ProformaStatus | "all" | null) ??
|
||||||
"all") as ProformaListStatusFilter
|
"all") as ProformaListStatusFilter
|
||||||
);
|
);
|
||||||
setArchiveView((searchParams.get("archiveView") as ProformaArchiveView | null) ?? "active");
|
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
// Criterios de ordenamiento
|
// Criterios de ordenamiento
|
||||||
@ -128,7 +121,6 @@ export const useListProformasController = () => {
|
|||||||
const criteria = useMemo<NonNullable<ListProformasByCriteriaParams["criteria"]>>(
|
const criteria = useMemo<NonNullable<ListProformasByCriteriaParams["criteria"]>>(
|
||||||
() =>
|
() =>
|
||||||
buildListProformasCriteria({
|
buildListProformasCriteria({
|
||||||
archiveView,
|
|
||||||
order,
|
order,
|
||||||
orderBy,
|
orderBy,
|
||||||
pageNumber: pageIndex,
|
pageNumber: pageIndex,
|
||||||
@ -136,10 +128,10 @@ export const useListProformasController = () => {
|
|||||||
q: debouncedSearch || "",
|
q: debouncedSearch || "",
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
}),
|
}),
|
||||||
[archiveView, debouncedSearch, pageIndex, pageSize, orderBy, order, statusFilter]
|
[debouncedSearch, pageIndex, pageSize, orderBy, order, statusFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = useProformasListQuery({ criteria });
|
const query = useProformasListQuery({ criteria, scope });
|
||||||
|
|
||||||
const setStatusFilterValue = useCallback(
|
const setStatusFilterValue = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
@ -161,22 +153,6 @@ export const useListProformasController = () => {
|
|||||||
[setSearchParams, statusFilter]
|
[setSearchParams, statusFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setArchiveViewValue = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const nextValue = value === "archived" || value === "all" ? value : "active";
|
|
||||||
if (archiveView === nextValue) return;
|
|
||||||
|
|
||||||
setArchiveView(nextValue);
|
|
||||||
setSearchParams((prevParams) => {
|
|
||||||
const params = new URLSearchParams(prevParams);
|
|
||||||
params.set("page", String(INITIAL_PAGE_INDEX + 1));
|
|
||||||
params.set("archiveView", nextValue);
|
|
||||||
return params;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[archiveView, setSearchParams]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setSearchValue = useCallback(
|
const setSearchValue = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
const nextValue = value.trim().replace(/\s+/g, " ");
|
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||||
@ -272,9 +248,6 @@ export const useListProformasController = () => {
|
|||||||
|
|
||||||
sort,
|
sort,
|
||||||
setSort: setSortValue,
|
setSort: setSortValue,
|
||||||
|
|
||||||
archiveView,
|
|
||||||
setArchiveView: setArchiveViewValue,
|
|
||||||
statusFilter,
|
statusFilter,
|
||||||
setStatusFilter: setStatusFilterValue,
|
setStatusFilter: setStatusFilterValue,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ProformaStatus } from "../../shared";
|
import type { ProformaStatus } from "../../shared";
|
||||||
|
|
||||||
export type ProformaArchiveView = "active" | "archived" | "all";
|
export type ProformaListScope = "normal" | "archived" | "deleted";
|
||||||
|
|
||||||
export type ProformaListStatusFilter = "all" | ProformaStatus;
|
export type ProformaListStatusFilter = "all" | ProformaStatus;
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
type ProformaListRow,
|
type ProformaListRow,
|
||||||
type ProformaStatus,
|
type ProformaStatus,
|
||||||
} from "../../../../shared";
|
} from "../../../../shared";
|
||||||
|
import type { ProformaListScope } from "../../../types/proforma-list-filters";
|
||||||
import { ProformaStatusBadge } from "../../components";
|
import { ProformaStatusBadge } from "../../components";
|
||||||
|
|
||||||
type GridActionHandlers = {
|
type GridActionHandlers = {
|
||||||
@ -53,6 +54,7 @@ type GridActionHandlers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useProformasGridColumns(
|
export function useProformasGridColumns(
|
||||||
|
scope: ProformaListScope,
|
||||||
actionHandlers: GridActionHandlers = {}
|
actionHandlers: GridActionHandlers = {}
|
||||||
): ColumnDef<ProformaListRow, unknown>[] {
|
): ColumnDef<ProformaListRow, unknown>[] {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -282,16 +284,27 @@ export function useProformasGridColumns(
|
|||||||
const isDraft = proforma.status === PROFORMA_STATUS.DRAFT;
|
const isDraft = proforma.status === PROFORMA_STATUS.DRAFT;
|
||||||
const isIssued = proforma.status === PROFORMA_STATUS.ISSUED;
|
const isIssued = proforma.status === PROFORMA_STATUS.ISSUED;
|
||||||
const isApproved = proforma.status === PROFORMA_STATUS.APPROVED;
|
const isApproved = proforma.status === PROFORMA_STATUS.APPROVED;
|
||||||
|
const isNormalScope = scope === "normal";
|
||||||
|
const isArchivedScope = scope === "archived";
|
||||||
|
const isDeletedScope = scope === "deleted";
|
||||||
const isArchivable =
|
const isArchivable =
|
||||||
|
isNormalScope &&
|
||||||
!proforma.isArchived &&
|
!proforma.isArchived &&
|
||||||
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
||||||
proforma.status === PROFORMA_STATUS.REJECTED);
|
proforma.status === PROFORMA_STATUS.REJECTED);
|
||||||
const isUnarchivable =
|
const isUnarchivable =
|
||||||
|
isArchivedScope &&
|
||||||
proforma.isArchived &&
|
proforma.isArchived &&
|
||||||
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
||||||
proforma.status === PROFORMA_STATUS.REJECTED);
|
proforma.status === PROFORMA_STATUS.REJECTED);
|
||||||
const availableTransitions =
|
const availableTransitions = isNormalScope
|
||||||
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
|
? PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? []
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const canEdit = isNormalScope && !isIssued && actionHandlers.onEditClick;
|
||||||
|
const canIssue = isNormalScope && !isIssued && isApproved && actionHandlers.onIssueClick;
|
||||||
|
const canDelete = !isDeletedScope && isDraft && actionHandlers.onDeleteClick;
|
||||||
|
const canDownloadPdf = !isDeletedScope && actionHandlers.onDownloadPdf;
|
||||||
|
|
||||||
const isPDFLoading =
|
const isPDFLoading =
|
||||||
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id;
|
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id;
|
||||||
@ -299,7 +312,7 @@ export function useProformasGridColumns(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{!isIssued && actionHandlers.onEditClick && (
|
{canEdit && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
@ -374,7 +387,10 @@ export function useProformasGridColumns(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cambiar estado */}
|
{/* Cambiar estado */}
|
||||||
{!isIssued && availableTransitions.length && actionHandlers.onChangeStatusClick && (
|
{!isDeletedScope &&
|
||||||
|
!isIssued &&
|
||||||
|
availableTransitions.length > 0 &&
|
||||||
|
actionHandlers.onChangeStatusClick && (
|
||||||
<TooltipProvider key={availableTransitions[0]}>
|
<TooltipProvider key={availableTransitions[0]}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
@ -403,7 +419,7 @@ export function useProformasGridColumns(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Emitir factura: solo si approved */}
|
{/* Emitir factura: solo si approved */}
|
||||||
{!isIssued && isApproved && actionHandlers.onIssueClick && (
|
{canIssue && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
@ -429,35 +445,37 @@ export function useProformasGridColumns(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Descargar en PDF */}
|
{/* Descargar en PDF */}
|
||||||
<TooltipProvider>
|
{canDownloadPdf && (
|
||||||
<Tooltip>
|
<TooltipProvider>
|
||||||
<TooltipTrigger
|
<Tooltip>
|
||||||
render={
|
<TooltipTrigger
|
||||||
<Button
|
render={
|
||||||
className={"size-8"}
|
<Button
|
||||||
disabled={isPDFLoading}
|
className={"size-8"}
|
||||||
onClick={(e) => {
|
disabled={isPDFLoading}
|
||||||
stop(e);
|
onClick={(e) => {
|
||||||
actionHandlers.onDownloadPdf?.(proforma);
|
stop(e);
|
||||||
}}
|
actionHandlers.onDownloadPdf?.(proforma);
|
||||||
size="icon"
|
}}
|
||||||
variant="ghost"
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
{isPDFLoading ? (
|
>
|
||||||
<Spinner className="size-4 cursor-progress" />
|
{isPDFLoading ? (
|
||||||
) : (
|
<Spinner className="size-4 cursor-progress" />
|
||||||
<FileDownIcon className="size-4 cursor-pointer" />
|
) : (
|
||||||
)}
|
<FileDownIcon className="size-4 cursor-pointer" />
|
||||||
<span className="sr-only">Descargar PDF</span>
|
)}
|
||||||
</Button>
|
<span className="sr-only">Descargar PDF</span>
|
||||||
}
|
</Button>
|
||||||
/>
|
}
|
||||||
<TooltipContent>Descargar PDF</TooltipContent>
|
/>
|
||||||
</Tooltip>
|
<TooltipContent>Descargar PDF</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Eliminar */}
|
{/* Eliminar */}
|
||||||
{isDraft && actionHandlers.onDeleteClick && (
|
{canDelete && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
@ -486,6 +504,6 @@ export function useProformasGridColumns(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t, actionHandlers]
|
[t, actionHandlers, scope]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,9 +13,6 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
Tabs,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import {
|
import {
|
||||||
CheckCircle2Icon,
|
CheckCircle2Icon,
|
||||||
@ -35,16 +32,59 @@ import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
|||||||
import { IssueProformaDialog } from "../../../issue-proforma";
|
import { IssueProformaDialog } from "../../../issue-proforma";
|
||||||
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
||||||
import type { ProformaListRow } from "../../../shared";
|
import type { ProformaListRow } from "../../../shared";
|
||||||
|
import type { ProformaListScope } from "../../types/proforma-list-filters";
|
||||||
import { useListProformasPageController } from "../../controllers";
|
import { useListProformasPageController } from "../../controllers";
|
||||||
import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks";
|
import { ProformaSummaryPanel, ProformasGrid, useProformasGridColumns } from "../blocks";
|
||||||
import { ArchiveProformaDialog, ProformaStatusBadge, UnarchiveProformaDialog } from "../components";
|
import { ArchiveProformaDialog, ProformaStatusBadge, UnarchiveProformaDialog } from "../components";
|
||||||
|
|
||||||
export const ListProformasPage = () => {
|
interface ScopedListProformasPageProps {
|
||||||
|
scope: ProformaListScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCOPE_CONFIG: Record<
|
||||||
|
ProformaListScope,
|
||||||
|
{
|
||||||
|
currentPath: string;
|
||||||
|
titleKey: string;
|
||||||
|
descriptionKey: string;
|
||||||
|
emptyStateKey: string;
|
||||||
|
showCreateAction: boolean;
|
||||||
|
enablePanel: boolean;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
normal: {
|
||||||
|
currentPath: "/proformas",
|
||||||
|
titleKey: "pages.proformas.list.title",
|
||||||
|
descriptionKey: "pages.proformas.list.description",
|
||||||
|
emptyStateKey: "pages.proformas.list.empty.normal",
|
||||||
|
showCreateAction: true,
|
||||||
|
enablePanel: true,
|
||||||
|
},
|
||||||
|
archived: {
|
||||||
|
currentPath: "/proformas/archived",
|
||||||
|
titleKey: "pages.proformas.archived.title",
|
||||||
|
descriptionKey: "pages.proformas.archived.description",
|
||||||
|
emptyStateKey: "pages.proformas.list.empty.archived",
|
||||||
|
showCreateAction: false,
|
||||||
|
enablePanel: true,
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
currentPath: "/proformas/deleted",
|
||||||
|
titleKey: "pages.proformas.deleted.title",
|
||||||
|
descriptionKey: "pages.proformas.deleted.description",
|
||||||
|
emptyStateKey: "pages.proformas.list.empty.deleted",
|
||||||
|
showCreateAction: false,
|
||||||
|
enablePanel: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const scopeConfig = SCOPE_CONFIG[scope];
|
||||||
|
|
||||||
const { currentReturnTo } = useReturnToNavigation({
|
const { currentReturnTo } = useReturnToNavigation({
|
||||||
fallbackPath: "/proformas",
|
fallbackPath: scopeConfig.currentPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -58,7 +98,7 @@ export const ListProformasPage = () => {
|
|||||||
unarchiveDialogCtrl,
|
unarchiveDialogCtrl,
|
||||||
isPDFDownloading: isPdfDownloading,
|
isPDFDownloading: isPdfDownloading,
|
||||||
pdfDownloadingId,
|
pdfDownloadingId,
|
||||||
} = useListProformasPageController();
|
} = useListProformasPageController(scope);
|
||||||
|
|
||||||
const handleEditClick = (proformaId: string) => {
|
const handleEditClick = (proformaId: string) => {
|
||||||
navigate({
|
navigate({
|
||||||
@ -69,7 +109,7 @@ export const ListProformasPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = useProformasGridColumns({
|
const columns = useProformasGridColumns(scope, {
|
||||||
onEditClick: (proforma) => handleEditClick(proforma.id),
|
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||||
onIssueClick: (proformaRow) =>
|
onIssueClick: (proformaRow) =>
|
||||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||||
@ -85,7 +125,7 @@ export const ListProformasPage = () => {
|
|||||||
isPdfDownloading,
|
isPdfDownloading,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPanelOpen = panelCtrl.panelState.isOpen;
|
const isPanelOpen = scopeConfig.enablePanel && panelCtrl.panelState.isOpen;
|
||||||
|
|
||||||
const listContent = (
|
const listContent = (
|
||||||
<div className="flex h-full min-w-0 flex-col gap-4 overflow-hidden">
|
<div className="flex h-full min-w-0 flex-col gap-4 overflow-hidden">
|
||||||
@ -96,24 +136,6 @@ export const ListProformasPage = () => {
|
|||||||
value={listCtrl.search}
|
value={listCtrl.search}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
onValueChange={listCtrl.setArchiveView}
|
|
||||||
value={listCtrl.archiveView}
|
|
||||||
>
|
|
||||||
<TabsList variant="line">
|
|
||||||
<TabsTrigger value="active">
|
|
||||||
{t("pages.proformas.list.filters.archive_view.active")}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="archived">
|
|
||||||
{t("pages.proformas.list.filters.archive_view.archived")}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="all">
|
|
||||||
{t("pages.proformas.list.filters.archive_view.all")}
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
|
onValueChange={(value) => listCtrl.setStatusFilter(value ?? "all")}
|
||||||
value={listCtrl.statusFilter}
|
value={listCtrl.statusFilter}
|
||||||
@ -142,12 +164,21 @@ export const ListProformasPage = () => {
|
|||||||
onColumnVisibilityChange={listCtrl.tablePreferences.setColumnVisibility}
|
onColumnVisibilityChange={listCtrl.tablePreferences.setColumnVisibility}
|
||||||
onPageChange={listCtrl.setPageIndex}
|
onPageChange={listCtrl.setPageIndex}
|
||||||
onPageSizeChange={listCtrl.setPageSize}
|
onPageSizeChange={listCtrl.setPageSize}
|
||||||
onRowClick={(proformaId) => panelCtrl.openProformaPanel(proformaId, "view")}
|
onRowClick={
|
||||||
|
scopeConfig.enablePanel
|
||||||
|
? (proformaId) => panelCtrl.openProformaPanel(proformaId, "view")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onSortChange={listCtrl.setSort}
|
onSortChange={listCtrl.setSort}
|
||||||
pageIndex={listCtrl.pageIndex}
|
pageIndex={listCtrl.pageIndex}
|
||||||
pageSize={listCtrl.pageSize}
|
pageSize={listCtrl.pageSize}
|
||||||
sort={listCtrl.sort}
|
sort={listCtrl.sort}
|
||||||
/>
|
/>
|
||||||
|
{listCtrl.data.totalItems === 0 ? (
|
||||||
|
<div className="rounded border border-dashed border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
||||||
|
{t(scopeConfig.emptyStateKey)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Explicación técnica */}
|
{/* Explicación técnica */}
|
||||||
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-4">
|
<div className="mt-8 rounded border border-border bg-card p-3 sm:p-4 text-xs sm:text-sm text-muted-foreground space-y-4">
|
||||||
@ -200,25 +231,27 @@ export const ListProformasPage = () => {
|
|||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
description={t("pages.proformas.list.description")}
|
description={t(scopeConfig.descriptionKey)}
|
||||||
rightSlot={
|
rightSlot={
|
||||||
<Button
|
scopeConfig.showCreateAction ? (
|
||||||
aria-label={t("pages.proformas.create.title")}
|
<Button
|
||||||
onClick={() =>
|
aria-label={t("pages.proformas.create.title")}
|
||||||
navigate({
|
onClick={() =>
|
||||||
pathname: "/proformas/create",
|
navigate({
|
||||||
search: createSearchParams({
|
pathname: "/proformas/create",
|
||||||
returnTo: currentReturnTo,
|
search: createSearchParams({
|
||||||
}).toString(),
|
returnTo: currentReturnTo,
|
||||||
})
|
}).toString(),
|
||||||
}
|
})
|
||||||
size={"default"}
|
}
|
||||||
>
|
size={"default"}
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
>
|
||||||
{t("pages.proformas.create.title")}
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
</Button>
|
{t("pages.proformas.create.title")}
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
title={t("pages.proformas.list.title")}
|
title={t(scopeConfig.titleKey)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
@ -293,7 +326,7 @@ export const ListProformasPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
{isPanelOpen ? (
|
{scopeConfig.enablePanel && isPanelOpen ? (
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
autoSave="list-proformas-page"
|
autoSave="list-proformas-page"
|
||||||
className="h-full mx-auto w-full space-y-4"
|
className="h-full mx-auto w-full space-y-4"
|
||||||
@ -395,3 +428,9 @@ export const ListProformasPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ListProformasPage = () => <ScopedListProformasPage scope="normal" />;
|
||||||
|
|
||||||
|
export const ListArchivedProformasPage = () => <ScopedListProformasPage scope="archived" />;
|
||||||
|
|
||||||
|
export const ListDeletedProformasPage = () => <ScopedListProformasPage scope="deleted" />;
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
|
|
||||||
import type { ProformaArchiveView, ProformaListStatusFilter } from "../types/proforma-list-filters";
|
import type { ProformaListStatusFilter } from "../types/proforma-list-filters";
|
||||||
|
|
||||||
type BuildListProformasCriteriaParams = {
|
type BuildListProformasCriteriaParams = {
|
||||||
archiveView: ProformaArchiveView;
|
|
||||||
status: ProformaListStatusFilter;
|
status: ProformaListStatusFilter;
|
||||||
q: string;
|
q: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
@ -12,18 +11,6 @@ type BuildListProformasCriteriaParams = {
|
|||||||
order: "asc" | "desc";
|
order: "asc" | "desc";
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildArchiveFilters(archiveView: ProformaArchiveView): NonNullable<CriteriaDTO["filters"]> {
|
|
||||||
if (archiveView === "active") {
|
|
||||||
return [{ field: "archived_at", operator: "IS_NULL" }];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (archiveView === "archived") {
|
|
||||||
return [{ field: "archived_at", operator: "IS_NOT_NULL" }];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildStatusFilters(status: ProformaListStatusFilter): NonNullable<CriteriaDTO["filters"]> {
|
function buildStatusFilters(status: ProformaListStatusFilter): NonNullable<CriteriaDTO["filters"]> {
|
||||||
if (status === "all") {
|
if (status === "all") {
|
||||||
return [];
|
return [];
|
||||||
@ -35,7 +22,6 @@ function buildStatusFilters(status: ProformaListStatusFilter): NonNullable<Crite
|
|||||||
export function buildListProformasCriteria(
|
export function buildListProformasCriteria(
|
||||||
params: BuildListProformasCriteriaParams
|
params: BuildListProformasCriteriaParams
|
||||||
): CriteriaDTO {
|
): CriteriaDTO {
|
||||||
const archiveFilters = buildArchiveFilters(params.archiveView);
|
|
||||||
const statusFilters = buildStatusFilters(params.status);
|
const statusFilters = buildStatusFilters(params.status);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -44,6 +30,6 @@ export function buildListProformasCriteria(
|
|||||||
pageSize: params.pageSize,
|
pageSize: params.pageSize,
|
||||||
orderBy: params.orderBy,
|
orderBy: params.orderBy,
|
||||||
order: params.order,
|
order: params.order,
|
||||||
filters: [...archiveFilters, ...statusFilters],
|
filters: statusFilters,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { CriteriaDTO } from "@erp/core";
|
|||||||
import type { IDataSource } from "@erp/core/client";
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
import type { ListProformasResponseDTO } from "../../../../common";
|
import type { ListProformasResponseDTO } from "../../../../common";
|
||||||
|
import type { ProformaListScope } from "../../list/types/proforma-list-filters";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recupera una lista de proformas del sistema utilizando la
|
* Recupera una lista de proformas del sistema utilizando la
|
||||||
@ -15,6 +16,7 @@ import type { ListProformasResponseDTO } from "../../../../common";
|
|||||||
|
|
||||||
export type ListProformasByCriteriaParams = {
|
export type ListProformasByCriteriaParams = {
|
||||||
criteria?: CriteriaDTO;
|
criteria?: CriteriaDTO;
|
||||||
|
scope: ProformaListScope;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,8 +26,12 @@ export function getListProformasByCriteria(
|
|||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
params: ListProformasByCriteriaParams
|
params: ListProformasByCriteriaParams
|
||||||
): Promise<ListProformasResult> {
|
): Promise<ListProformasResult> {
|
||||||
const { criteria, signal } = params || { criteria: undefined, signal: undefined };
|
const { criteria, scope, signal } = params;
|
||||||
return dataSource.getList<ListProformasResponseDTO>("proformas", {
|
|
||||||
|
const resource =
|
||||||
|
scope === "archived" ? "proformas/archived" : scope === "deleted" ? "proformas/deleted" : "proformas";
|
||||||
|
|
||||||
|
return dataSource.getList<ListProformasResponseDTO>(resource, {
|
||||||
signal,
|
signal,
|
||||||
...criteria,
|
...criteria,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { QueryKey } from "@tanstack/react-query";
|
import type { QueryKey } from "@tanstack/react-query";
|
||||||
|
|
||||||
import type { ProformasListRequestDTO } from "../../../../common";
|
import type { ProformasListRequestDTO } from "../../../../common";
|
||||||
|
import type { ProformaListScope } from "../../list/types/proforma-list-filters";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefijo base para listados
|
* Prefijo base para listados
|
||||||
@ -11,9 +12,13 @@ export const DOCUMENT_SERIES_QUERY_KEY = ["customer-invoices", "document-series"
|
|||||||
/**
|
/**
|
||||||
* Query key para listado de proformas
|
* Query key para listado de proformas
|
||||||
*/
|
*/
|
||||||
export const LIST_PROFORMAS_QUERY_KEY = (criteria?: ProformasListRequestDTO): QueryKey =>
|
export const LIST_PROFORMAS_QUERY_KEY = (
|
||||||
|
scope: ProformaListScope,
|
||||||
|
criteria?: ProformasListRequestDTO
|
||||||
|
): QueryKey =>
|
||||||
[
|
[
|
||||||
...LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
...LIST_PROFORMAS_QUERY_KEY_PREFIX,
|
||||||
|
scope,
|
||||||
{
|
{
|
||||||
pageNumber: criteria?.pageNumber ?? 1,
|
pageNumber: criteria?.pageNumber ?? 1,
|
||||||
pageSize: criteria?.pageSize ?? 5,
|
pageSize: criteria?.pageSize ?? 5,
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import { getListProformasByCriteria } from "../api";
|
|||||||
import type { ProformaList } from "../entities";
|
import type { ProformaList } from "../entities";
|
||||||
|
|
||||||
import { LIST_PROFORMAS_QUERY_KEY } from "./keys";
|
import { LIST_PROFORMAS_QUERY_KEY } from "./keys";
|
||||||
|
import type { ProformaListScope } from "../../list/types/proforma-list-filters";
|
||||||
|
|
||||||
export interface ProformasListQueryOptions {
|
export interface ProformasListQueryOptions {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
criteria?: Partial<CriteriaDTO>;
|
criteria?: Partial<CriteriaDTO>;
|
||||||
|
scope: ProformaListScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProformasListQuery = (
|
export const useProformasListQuery = (
|
||||||
@ -19,11 +21,12 @@ export const useProformasListQuery = (
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = options?.enabled ?? true;
|
const enabled = options?.enabled ?? true;
|
||||||
const criteria = options?.criteria ?? {};
|
const criteria = options?.criteria ?? {};
|
||||||
|
const scope = options?.scope ?? "normal";
|
||||||
|
|
||||||
return useQuery<ProformaList, DefaultError>({
|
return useQuery<ProformaList, DefaultError>({
|
||||||
queryKey: LIST_PROFORMAS_QUERY_KEY(criteria),
|
queryKey: LIST_PROFORMAS_QUERY_KEY(scope, criteria),
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const dto = await getListProformasByCriteria(dataSource, { signal, criteria });
|
const dto = await getListProformasByCriteria(dataSource, { signal, criteria, scope });
|
||||||
return ListProformasAdapter.fromDto(dto);
|
return ListProformasAdapter.fromDto(dto);
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user