feat(web): panel de reserva de numeros en PdV detail (ADM-008)

Gap detectado durante smoke: la DetailPage tenia los hooks
useReservarNumero/useProximoNumero creados en Batch 6 pero faltaba
el componente que los consume.

SecuenciasPanel.tsx: tabla con los 6 tipos AFIP (FacturaA/B/C, NC A/B/C),
proximo numero por tipo, boton Reservar. Toast con el numero reservado.
Deshabilitado si PdV o Medio padre estan inactivos.

Integrado en PuntoDeVentaDetailPage bajo guard de permiso.
This commit is contained in:
2026-04-17 13:38:21 -03:00
parent 4368c42599
commit 9263d9a178
2 changed files with 104 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { TipoComprobante } from '../types'
import { useReservarNumero, useProximoNumero } from '../hooks/useReservarNumero'
const TIPOS: Array<{ value: TipoComprobante; label: string }> = [
{ value: TipoComprobante.FacturaA, label: 'Factura A' },
{ value: TipoComprobante.FacturaB, label: 'Factura B' },
{ value: TipoComprobante.FacturaC, label: 'Factura C' },
{ value: TipoComprobante.NotaCreditoA, label: 'Nota Crédito A' },
{ value: TipoComprobante.NotaCreditoB, label: 'Nota Crédito B' },
{ value: TipoComprobante.NotaCreditoC, label: 'Nota Crédito C' },
]
interface Props {
puntoDeVentaId: number
disabled: boolean
}
export function SecuenciasPanel({ puntoDeVentaId, disabled }: Props) {
return (
<div className="rounded-md border border-border">
<div className="border-b border-border px-4 py-3">
<h2 className="text-sm font-semibold">Reserva de números de comprobante</h2>
<p className="text-xs text-muted-foreground">
Cada reserva incrementa el correlativo y devuelve el número asignado.
</p>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Tipo</TableHead>
<TableHead className="text-right">Próximo número</TableHead>
<TableHead className="text-right">Acción</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{TIPOS.map((tipo) => (
<SecuenciaRow
key={tipo.value}
puntoDeVentaId={puntoDeVentaId}
tipoValue={tipo.value}
tipoLabel={tipo.label}
disabled={disabled}
/>
))}
</TableBody>
</Table>
</div>
)
}
interface RowProps {
puntoDeVentaId: number
tipoValue: TipoComprobante
tipoLabel: string
disabled: boolean
}
function SecuenciaRow({ puntoDeVentaId, tipoValue, tipoLabel, disabled }: RowProps) {
const proximo = useProximoNumero(puntoDeVentaId, tipoValue)
const reservar = useReservarNumero(puntoDeVentaId)
const handleReservar = () => {
reservar.mutate(tipoValue, {
onSuccess: (data) => {
toast.success(`${tipoLabel}: número ${data.numeroReservado} reservado`)
},
onError: (err: unknown) => {
const apiError = err as { response?: { data?: { error?: string } } }
const code = apiError.response?.data?.error ?? 'error'
toast.error(`No se pudo reservar: ${code}`)
},
})
}
return (
<TableRow>
<TableCell>{tipoLabel}</TableCell>
<TableCell className="text-right font-mono">
{proximo.isLoading ? '…' : proximo.data?.proximoNumero ?? '—'}
</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="outline"
disabled={disabled || reservar.isPending}
onClick={handleReservar}
>
{reservar.isPending ? 'Reservando…' : 'Reservar'}
</Button>
</TableCell>
</TableRow>
)
}

View File

@@ -7,6 +7,7 @@ import { useMedio } from '../../medios/hooks/useMedio'
import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal'
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
import { PdvInactivoBanner } from '../components/PdvInactivoBanner'
import { SecuenciasPanel } from '../components/SecuenciasPanel'
function formatDate(iso: string | null): string {
if (!iso) return '—'
@@ -103,6 +104,13 @@ export function PuntoDeVentaDetailPage() {
/>
</div>
</CanPerform>
<CanPerform permission="administracion:puntos_de_venta:gestionar">
<SecuenciasPanel
puntoDeVentaId={puntoDeVentaId}
disabled={pdvInactivo || medioInactivo}
/>
</CanPerform>
</div>
)
}