Punto 7: gerencia puede corregir tickets sin cancelarlos (lineas, precios, descuento y notas) con ajuste automatico de monedero e inventario

This commit is contained in:
2026-09-24 17:53:22 +00:00
parent dcf2c367f1
commit e89bcdca3a
7 changed files with 881 additions and 4 deletions

View File

@@ -1,11 +1,13 @@
import type { FC } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { Search, Ticket, XCircle, RefreshCw, AlertTriangle, Printer, Receipt, User } from 'lucide-react';
import { Search, Ticket, XCircle, RefreshCw, AlertTriangle, Printer, Receipt, User, Pencil } from 'lucide-react';
import Layout from '../components/Layout';
import { Card, Button, Input, Select, Modal, TextArea, Badge, EmptyState, Skeleton, PageHeader, toast } from '../components/ui';
import TicketPrint from '../components/TicketPrint';
import TicketEditor from '../components/TicketEditor';
import { odooApi, type Sale } from '../services/odoo';
import { useAuth } from '../lib/auth';
import { readDateFilterPrefs, writeDateFilterPrefs, DATE_PRESETS, rangeForPreset, type DatePreset } from '../lib/dateRange';
const PREF_KEY = 'skeen_tickets_filtros';
@@ -45,6 +47,8 @@ const PAGE_SIZE = 50;
* Ventas. Permiso de cancelación: admin y gerencia (el API lo valida).
*/
const Tickets: FC = () => {
const { user } = useAuth();
const puedeCorregir = user?.role === 'admin' || user?.role === 'gerencia';
const prefs = readDateFilterPrefs(PREF_KEY);
const [preset, setPreset] = useState<DatePreset>(prefs?.preset || 'hoy');
const [desde, setDesde] = useState(prefs?.start || rangeForPreset('hoy').start);
@@ -64,6 +68,32 @@ const Tickets: FC = () => {
const [motivo, setMotivo] = useState('');
const [enviando, setEnviando] = useState(false);
const [reimprimir, setReimprimir] = useState<Sale | null>(null);
const [editando, setEditando] = useState<Sale | null>(null);
const [catalogo, setCatalogo] = useState<{
servicios: { id: number; name: string; price: number }[];
items: { id: number; name: string; price: number; qty: number }[];
}>({ servicios: [], items: [] });
const [doctores, setDoctores] = useState<{ id: number; name: string }[]>([]);
// El catálogo y los doctores solo hacen falta para corregir un ticket: se
// cargan perezosamente la primera vez que se abre el editor.
const cargarCatalogos = useCallback(async () => {
if (catalogo.servicios.length) return;
try {
const [sv, iv, dc] = await Promise.all([
odooApi.getServices({ page_size: 200 }),
odooApi.getInventory(),
odooApi.getDoctors(),
]);
setCatalogo({
servicios: (sv.services || []).map((x) => ({ id: x.id, name: x.name, price: x.price })),
items: (iv.items || []).map((x) => ({ id: x.id, name: x.name, price: x.price ?? 0, qty: x.qty })),
});
setDoctores((dc.doctors || []).map((x) => ({ id: x.id, name: x.name })));
} catch {
toast.error('No se pudo cargar el catálogo para corregir el ticket');
}
}, [catalogo.servicios.length]);
const aplicarPreset = (p: DatePreset) => {
setPreset(p);
@@ -264,6 +294,16 @@ const Tickets: FC = () => {
<Button size="sm" variant="ghost" onClick={() => setDetalle(s)} title="Ver detalle del ticket">
<Receipt size={14} />
</Button>
{s.state !== 'cancelled' && puedeCorregir && (
<Button
size="sm"
variant="ghost"
title="Corregir ticket"
onClick={async () => { await cargarCatalogos(); setEditando(s); }}
>
<Pencil size={14} />
</Button>
)}
{s.state !== 'cancelled' && (
<Button size="sm" variant="ghost" onClick={() => abrirCancelar(s)} title="Cancelar ticket (nota de crédito)">
<XCircle size={14} className="text-rose-600" />
@@ -305,6 +345,11 @@ const Tickets: FC = () => {
</Button>
)}
<Button variant="outline" onClick={() => setDetalle(null)}>Cerrar</Button>
{detalle && detalle.state !== 'cancelled' && puedeCorregir && (
<Button variant="secondary" onClick={async () => { const d = detalle; setDetalle(null); await cargarCatalogos(); setEditando(d); }}>
<Pencil size={14} className="mr-1.5" /> Corregir
</Button>
)}
{detalle && detalle.state !== 'cancelled' && (
<Button variant="danger" onClick={() => { const d = detalle; setDetalle(null); abrirCancelar(d); }}>
<XCircle size={14} className="mr-1.5" /> Cancelar ticket
@@ -440,6 +485,16 @@ const Tickets: FC = () => {
)}
</Modal>
{editando && (
<TicketEditor
ticket={editando}
catalogo={{ servicios: catalogo.servicios, items: catalogo.items }}
doctores={doctores}
onClose={() => setEditando(null)}
onGuardado={(sale) => { setDetalle(sale); load(); }}
/>
)}
<TicketPrint
isOpen={!!reimprimir}
onClose={() => setReimprimir(null)}