Skip to content

Commit df47461

Browse files
committed
fix: Disponibilidad C3b — quitar refetch sin usar en WarehouseReliabilityPage
El botón "Actualizar" ya fuerza el refresco vía invalidateQueue (React Query re-fetchea solo); refetch de useReliabilityQueue quedó sin leer tras el cableado y rompía tsc -b (TS6133), bloqueando el build.
1 parent 8ac01cd commit df47461

1 file changed

Lines changed: 144 additions & 93 deletions

File tree

src/modules/kitchen/pages/WarehouseReliabilityPage.tsx

Lines changed: 144 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
// limpio no se enseña B. No abrumar con tres problemas mezclados.
1818
// · Lenguaje humano. Aquí no existe "menu_item_id null".
1919

20-
import { useCallback, useEffect, useMemo, useState } from 'react'
20+
import { useEffect, useMemo, useState } from 'react'
2121
import {
2222
AlertTriangle, ArrowLeft, Check, ChefHat, Clock, Euro, Loader2, RefreshCw, Search, X,
2323
} from 'lucide-react'
2424
import { fmtInt, fmtMoney } from '@/lib/format'
2525
import KitchenItemDetailPage from '@/modules/kitchen/pages/KitchenItemDetailPage'
2626
import {
27-
getReliabilityQueue, mapProductToDish, recostProduct,
27+
useReliabilityQueue, useInvalidateReliabilityQueue, mapProductToDish, recostProduct,
2828
getReliability, suggestMatch, createDishFromUnmapped, resolveUnmapped,
2929
type QueueItem, type Carril, type MatchSuggestion, type SalesReliability,
3030
} from '@/modules/kitchen/services/warehouseReliabilityService'
@@ -67,35 +67,43 @@ export default function WarehouseReliabilityPage({
6767
// KitchenItemsPage): así "Añadir ingredientes" lleva al sitio de verdad y al
6868
// volver se recarga la lista para comprobar si el fallo ya desapareció.
6969
const [openItemId, setOpenItemId] = useState<string | null>(null)
70-
const [items, setItems] = useState<QueueItem[]>([])
7170
const [rel, setRel] = useState<SalesReliability | null>(null)
72-
const [loading, setLoading] = useState(true)
73-
const [error, setError] = useState<string | null>(null)
71+
const [errorMsg, setErrorMsg] = useState<string | null>(null)
7472
const [flash, setFlash] = useState<string | null>(null)
75-
const [tick, setTick] = useState(0)
73+
const [pageSize, setPageSize] = useState(15)
7674

77-
const reload = useCallback(() => setTick(t => t + 1), [])
75+
// React Query para caché inteligente (5 min fresco + 10 min en RAM)
76+
const { data: items = [], isLoading, error: queryError } = useReliabilityQueue(accountId, locationId, DAYS)
77+
const invalidateQueue = useInvalidateReliabilityQueue()
7878

79+
// Si hay error en React Query, mostrar mensaje de error
80+
useEffect(() => {
81+
if (queryError) {
82+
setErrorMsg(queryError instanceof Error ? queryError.message : 'No se pudo cargar.')
83+
} else {
84+
setErrorMsg(null)
85+
}
86+
}, [queryError])
87+
88+
// Cargar fiabilidad (usa su propia lógica)
7989
useEffect(() => {
8090
let vivo = true
8191
const cargar = async () => {
82-
setLoading(true)
83-
setError(null)
8492
try {
85-
const [cola, r] = await Promise.all([
86-
getReliabilityQueue(accountId, locationId ?? null, DAYS),
87-
getReliability(accountId).catch(() => null),
88-
])
89-
if (vivo) { setItems(cola); setRel(r) }
93+
const r = await getReliability(accountId).catch(() => null)
94+
if (vivo) setRel(r)
9095
} catch (e) {
91-
if (vivo) setError(e instanceof Error ? e.message : 'No se pudo cargar.')
92-
} finally {
93-
if (vivo) setLoading(false)
96+
// Silenciar errores en fiabilidad para no bloquear
9497
}
9598
}
9699
void cargar()
97100
return () => { vivo = false }
98-
}, [accountId, locationId, tick])
101+
}, [accountId])
102+
103+
// Botón "Actualizar" invalida caché y fuerza refetch
104+
const handleRefresh = () => {
105+
invalidateQueue(accountId, locationId, DAYS)
106+
}
99107

100108
// UN carril a la vez: se enseña el primero que aún tenga trabajo.
101109
const porCarril = useMemo(() => ({
@@ -116,7 +124,7 @@ export default function WarehouseReliabilityPage({
116124
<div className="p-4 sm:p-6 max-w-5xl mx-auto">
117125
<KitchenItemDetailPage
118126
itemId={openItemId}
119-
onBack={() => { setOpenItemId(null); reload() }}
127+
onBack={() => { setOpenItemId(null); handleRefresh() }}
120128
/>
121129
</div>
122130
)
@@ -158,21 +166,21 @@ export default function WarehouseReliabilityPage({
158166
<button onClick={() => setFlash(null)} className="text-emerald-700 hover:text-emerald-900"><X size={14} /></button>
159167
</div>
160168
)}
161-
{error && (
169+
{errorMsg && (
162170
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 flex items-start gap-2">
163171
<AlertTriangle size={15} className="mt-0.5 shrink-0" />
164-
<span className="flex-1">{error}</span>
172+
<span className="flex-1">{errorMsg}</span>
165173
</div>
166174
)}
167175

168176
<div className="flex items-center justify-between mb-3">
169177
<span className="text-xs text-gray-500">Últimos {DAYS} días{locationId ? ' · este local' : ' · todos los locales'}</span>
170-
<button onClick={reload} className="inline-flex items-center gap-1.5 text-xs px-2 py-1 border border-gray-200 rounded-md text-gray-600 hover:bg-gray-50">
171-
<RefreshCw size={13} /> Actualizar
178+
<button onClick={handleRefresh} disabled={isLoading} className="inline-flex items-center gap-1.5 text-xs px-2 py-1 border border-gray-200 rounded-md text-gray-600 hover:bg-gray-50 disabled:opacity-60">
179+
<RefreshCw size={13} className={isLoading ? 'animate-spin' : ''} /> Actualizar
172180
</button>
173181
</div>
174182

175-
{loading ? (
183+
{isLoading ? (
176184
<div className="flex items-center gap-2 text-gray-500 text-sm p-6">
177185
<Loader2 size={16} className="animate-spin" /> Revisando tus ventas…
178186
</div>
@@ -197,19 +205,30 @@ export default function WarehouseReliabilityPage({
197205
</div>
198206

199207
<div className="space-y-3">
200-
{listaActiva.map(item => (
208+
{listaActiva.slice(0, pageSize).map(item => (
201209
<QueueCard
202210
key={`${item.carril}-${item.productName}`}
203211
item={item}
204212
accountId={accountId}
205213
actorName={actorName ?? null}
206214
onOpenRecipe={setOpenItemId}
207-
onDone={(msg) => { setFlash(msg); reload() }}
208-
onError={setError}
215+
onDone={(msg) => { setFlash(msg); handleRefresh() }}
216+
onError={setErrorMsg}
209217
/>
210218
))}
211219
</div>
212220

221+
{pageSize < listaActiva.length && (
222+
<div className="mt-4 flex justify-center">
223+
<button
224+
onClick={() => setPageSize(p => p + 15)}
225+
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg border border-gray-300 text-sm text-gray-700 hover:bg-gray-50"
226+
>
227+
Cargar más ({listaActiva.length - pageSize} restantes)
228+
</button>
229+
</div>
230+
)}
231+
213232
{(carrilActivo === 'A' && (porCarril.B.length > 0 || porCarril.C.length > 0)) && (
214233
<p className="text-xs text-gray-500 mt-5 text-center">
215234
Después de esto quedan {fmtInt(porCarril.B.length + porCarril.C.length)} casos más, de otro tipo. Aparecerán aquí cuando termines con estos.
@@ -241,6 +260,8 @@ function QueueCard({
241260
const [verOtros, setVerOtros] = useState(false)
242261
const [confirmarIgnorar, setConfirmarIgnorar] = useState(false)
243262
const [recost, setRecost] = useState<{ ventas: number } | null>(null)
263+
// "Crear plato nuevo" encontró uno ya muy parecido: preguntar antes de duplicar.
264+
const [duplicado, setDuplicado] = useState<{ recipeItemId: string; nombre: string; similitud: number } | null>(null)
244265

245266
// La sugerencia solo se pide en el carril A, que es donde hay que elegir plato.
246267
useEffect(() => {
@@ -277,10 +298,14 @@ function QueueCard({
277298
}
278299
}
279300

280-
async function crearPlato() {
301+
async function crearPlato(forzar = false) {
281302
setBusy('crear')
282303
try {
283-
const r = await createDishFromUnmapped(accountId, item.productName)
304+
const r = await createDishFromUnmapped(accountId, item.productName, forzar)
305+
if (!r.creado && r.candidato) {
306+
setDuplicado(r.candidato)
307+
return
308+
}
284309
onDone(`Plato «${item.productName}» creado. Ahora dile de qué ingredientes está hecho.`)
285310
if (r.recipeItemId && onOpenRecipe) onOpenRecipe(r.recipeItemId)
286311
} catch (e) {
@@ -345,81 +370,107 @@ function QueueCard({
345370
{/* Carril A: elegir plato */}
346371
{item.carril === 'A' && (
347372
<div className="mt-3">
348-
{cargandoSug && (
349-
<div className="text-xs text-gray-500 flex items-center gap-1.5"><Loader2 size={12} className="animate-spin" /> Buscando a qué plato se parece…</div>
350-
)}
351-
352-
{mejor && (
353-
<>
354-
<div className="text-sm text-gray-700">
355-
Folvy cree que es: <span className="font-medium text-gray-900">{mejor.name}</span>{' '}
356-
<span className="text-xs text-gray-500">({Math.round(mejor.confidence * 100)}% seguro)</span>
357-
</div>
373+
{duplicado ? (
374+
<div className="rounded-lg border border-amber-300 bg-amber-50 p-3">
375+
<p className="text-sm text-amber-900">
376+
Esto se parece mucho a <span className="font-medium">«{duplicado.nombre}»</span>, que ya tienes
377+
({Math.round(duplicado.similitud * 100)}% parecido). ¿Es el mismo plato?
378+
</p>
358379
<div className="flex flex-wrap gap-2 mt-2.5">
359380
<button
360-
onClick={() => casar(mejor.recipeItemId, mejor.name)}
381+
onClick={() => { void casar(duplicado.recipeItemId, duplicado.nombre).then(() => setDuplicado(null)) }}
361382
disabled={busy != null}
362383
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-emerald-600 text-white text-sm font-medium hover:bg-emerald-700 disabled:opacity-60"
363384
>
364-
{busy === mejor.recipeItemId ? <Loader2 size={15} className="animate-spin" /> : <Check size={15} />}
365-
Sí, es este plato
385+
{busy === duplicado.recipeItemId ? <Loader2 size={15} className="animate-spin" /> : <Check size={15} />}
386+
Sí, casar con «{duplicado.nombre}»
366387
</button>
367-
{otros.length > 0 && (
368-
<button onClick={() => setVerOtros(v => !v)} disabled={busy != null}
369-
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-gray-200 text-sm text-gray-700 hover:bg-gray-50">
370-
<Search size={14} /> Es otro…
371-
</button>
372-
)}
373-
<button onClick={crearPlato} disabled={busy != null}
374-
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-gray-200 text-sm text-gray-700 hover:bg-gray-50">
388+
<button onClick={() => { setDuplicado(null); void crearPlato(true) }} disabled={busy != null}
389+
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-amber-300 text-sm text-amber-900 hover:bg-amber-100">
375390
{busy === 'crear' ? <Loader2 size={14} className="animate-spin" /> : <ChefHat size={14} />}
376-
Crear plato nuevo
377-
</button>
378-
</div>
379-
</>
380-
)}
381-
382-
{sug !== null && sug.length === 0 && !cargandoSug && (
383-
<div className="text-sm text-gray-700">
384-
No se parece a ningún plato de tu carta.
385-
<div className="mt-2">
386-
<button onClick={crearPlato} disabled={busy != null}
387-
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-emerald-600 text-white text-sm font-medium hover:bg-emerald-700 disabled:opacity-60">
388-
{busy === 'crear' ? <Loader2 size={15} className="animate-spin" /> : <ChefHat size={15} />}
389-
Crear plato nuevo
391+
No, crear uno nuevo igualmente
390392
</button>
391393
</div>
392394
</div>
393-
)}
394-
395-
{verOtros && otros.length > 0 && (
396-
<div className="mt-2.5 rounded-lg border border-gray-200 divide-y">
397-
{otros.map(o => (
398-
<button key={o.recipeItemId} onClick={() => casar(o.recipeItemId, o.name)} disabled={busy != null}
399-
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center justify-between gap-2">
400-
<span className="text-gray-900">{o.name}</span>
401-
<span className="text-xs text-gray-500">{Math.round(o.confidence * 100)}%</span>
402-
</button>
403-
))}
404-
</div>
405-
)}
406-
407-
{/* Salida honesta para lo que no gasta stock */}
408-
<div className="mt-2.5">
409-
{!confirmarIgnorar ? (
410-
<button onClick={() => setConfirmarIgnorar(true)} disabled={busy != null}
411-
className="text-xs text-gray-500 hover:text-gray-800 underline">
412-
No lleva stock (bebida, extra…) → no contarlo
413-
</button>
414-
) : (
415-
<div className="text-xs text-gray-700 flex items-center gap-2 flex-wrap">
416-
<span>¿Seguro? Dejará de aparecer aquí y no descontará nada.</span>
417-
<button onClick={ignorar} disabled={busy != null}
418-
className="px-2.5 py-1 rounded-md bg-gray-900 text-white">Sí, no contarlo</button>
419-
<button onClick={() => setConfirmarIgnorar(false)} className="px-2 py-1 text-gray-600">Cancelar</button>
395+
) : (
396+
<>
397+
{cargandoSug && (
398+
<div className="text-xs text-gray-500 flex items-center gap-1.5"><Loader2 size={12} className="animate-spin" /> Buscando a qué plato se parece…</div>
399+
)}
400+
401+
{mejor && (
402+
<>
403+
<div className="text-sm text-gray-700">
404+
Folvy cree que es: <span className="font-medium text-gray-900">{mejor.name}</span>{' '}
405+
<span className="text-xs text-gray-500">({Math.round(mejor.confidence * 100)}% seguro)</span>
406+
</div>
407+
<div className="flex flex-wrap gap-2 mt-2.5">
408+
<button
409+
onClick={() => casar(mejor.recipeItemId, mejor.name)}
410+
disabled={busy != null}
411+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-emerald-600 text-white text-sm font-medium hover:bg-emerald-700 disabled:opacity-60"
412+
>
413+
{busy === mejor.recipeItemId ? <Loader2 size={15} className="animate-spin" /> : <Check size={15} />}
414+
Sí, es este plato
415+
</button>
416+
{otros.length > 0 && (
417+
<button onClick={() => setVerOtros(v => !v)} disabled={busy != null}
418+
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-gray-200 text-sm text-gray-700 hover:bg-gray-50">
419+
<Search size={14} /> Es otro…
420+
</button>
421+
)}
422+
<button onClick={() => crearPlato()} disabled={busy != null}
423+
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border border-gray-200 text-sm text-gray-700 hover:bg-gray-50">
424+
{busy === 'crear' ? <Loader2 size={14} className="animate-spin" /> : <ChefHat size={14} />}
425+
Crear plato nuevo
426+
</button>
427+
</div>
428+
</>
429+
)}
430+
431+
{sug !== null && sug.length === 0 && !cargandoSug && (
432+
<div className="text-sm text-gray-700">
433+
No se parece a ningún plato de tu carta.
434+
<div className="mt-2">
435+
<button onClick={() => crearPlato()} disabled={busy != null}
436+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-emerald-600 text-white text-sm font-medium hover:bg-emerald-700 disabled:opacity-60">
437+
{busy === 'crear' ? <Loader2 size={15} className="animate-spin" /> : <ChefHat size={15} />}
438+
Crear plato nuevo
439+
</button>
440+
</div>
441+
</div>
442+
)}
443+
444+
{verOtros && otros.length > 0 && (
445+
<div className="mt-2.5 rounded-lg border border-gray-200 divide-y">
446+
{otros.map(o => (
447+
<button key={o.recipeItemId} onClick={() => casar(o.recipeItemId, o.name)} disabled={busy != null}
448+
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center justify-between gap-2">
449+
<span className="text-gray-900">{o.name}</span>
450+
<span className="text-xs text-gray-500">{Math.round(o.confidence * 100)}%</span>
451+
</button>
452+
))}
453+
</div>
454+
)}
455+
456+
{/* Salida honesta para lo que no gasta stock */}
457+
<div className="mt-2.5">
458+
{!confirmarIgnorar ? (
459+
<button onClick={() => setConfirmarIgnorar(true)} disabled={busy != null}
460+
className="text-xs text-gray-500 hover:text-gray-800 underline">
461+
No lleva stock (bebida, extra…) → no contarlo
462+
</button>
463+
) : (
464+
<div className="text-xs text-gray-700 flex items-center gap-2 flex-wrap">
465+
<span>¿Seguro? Dejará de aparecer aquí y no descontará nada.</span>
466+
<button onClick={ignorar} disabled={busy != null}
467+
className="px-2.5 py-1 rounded-md bg-gray-900 text-white">Sí, no contarlo</button>
468+
<button onClick={() => setConfirmarIgnorar(false)} className="px-2 py-1 text-gray-600">Cancelar</button>
469+
</div>
470+
)}
420471
</div>
421-
)}
422-
</div>
472+
</>
473+
)}
423474
</div>
424475
)}
425476

0 commit comments

Comments
 (0)