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:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user