ADM-008: Puntos de Venta (CRUD fundacional) #19

Merged
dmolinari merged 18 commits from feature/ADM-008 into main 2026-04-17 17:31:21 +00:00
5 changed files with 3 additions and 174 deletions
Showing only changes of commit 6be637b4cf - Show all commits

View File

@@ -1,22 +0,0 @@
import { axiosClient } from '@/api/axiosClient'
import type { TipoComprobante, ReservarNumeroResponse, ProximoNumeroResponse } from '../types'
export async function reservarNumero(
puntoDeVentaId: number,
tipoComprobante: TipoComprobante,
): Promise<ReservarNumeroResponse> {
const response = await axiosClient.post<ReservarNumeroResponse>(
`/api/v1/admin/puntos-de-venta/${puntoDeVentaId}/secuencias/${tipoComprobante}/reservar`,
)
return response.data
}
export async function getProximoNumero(
puntoDeVentaId: number,
tipoComprobante: TipoComprobante,
): Promise<ProximoNumeroResponse> {
const response = await axiosClient.get<ProximoNumeroResponse>(
`/api/v1/admin/puntos-de-venta/${puntoDeVentaId}/secuencias/${tipoComprobante}/proximo`,
)
return response.data
}

View File

@@ -1,96 +0,0 @@
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

@@ -1,30 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { reservarNumero, getProximoNumero } from '../api/secuencias.api'
import type { TipoComprobante } from '../types'
// ─── Reservar ────────────────────────────────────────────────────────────────
export function useReservarNumero(puntoDeVentaId: number) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (tipoComprobante: TipoComprobante) =>
reservarNumero(puntoDeVentaId, tipoComprobante),
onSuccess: (_data, tipoComprobante) => {
// Invalidate the proximo query for this pdv+tipo so it refetches
queryClient.invalidateQueries({
queryKey: ['puntos-de-venta', 'proximo', puntoDeVentaId, tipoComprobante],
})
},
})
}
// ─── Próximo número (read-only) ──────────────────────────────────────────────
export function useProximoNumero(puntoDeVentaId: number, tipoComprobante: TipoComprobante) {
return useQuery({
queryKey: ['puntos-de-venta', 'proximo', puntoDeVentaId, tipoComprobante],
queryFn: () => getProximoNumero(puntoDeVentaId, tipoComprobante),
enabled: !!puntoDeVentaId,
staleTime: 5_000,
})
}

View File

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

View File

@@ -1,13 +1,7 @@
// ADM-008 — shared types for puntos-de-venta feature // ADM-008 — shared types for puntos-de-venta feature
// NOTE: numeración AFIP (NumeroFactura, CAI) es asignada externamente por IMAC/Infogestión.
export enum TipoComprobante { // Un worker futuro (INT-001) polleará la vista de Infogestión para asociar
FacturaA = 1, // NumeroOrdenInterno ↔ NumeroFacturaAFIP + CAI. No se generan números aquí.
FacturaB = 2,
FacturaC = 3,
NotaCreditoA = 4,
NotaCreditoB = 5,
NotaCreditoC = 6,
}
export interface PuntoDeVentaListItem { export interface PuntoDeVentaListItem {
id: number id: number
@@ -53,16 +47,6 @@ export interface PuntosDeVentaQuery {
activo?: boolean activo?: boolean
} }
export interface ReservarNumeroResponse {
tipoComprobante: TipoComprobante
numeroReservado: number
}
export interface ProximoNumeroResponse {
tipoComprobante: TipoComprobante
proximoNumero: number
}
export interface PagedResult<T> { export interface PagedResult<T> {
items: T[] items: T[]
page: number page: number