Skip to content

Commit 2fc6b1b

Browse files
authored
Merge pull request #9 from Llorente29/hubrise-fase2-connect-button
HubRise Fase 2: cablear boton "Conectar a delivery" en la carta
2 parents 4f0f41c + ed29092 commit 2fc6b1b

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

src/modules/kitchen/pages/KitchenMenuPage.tsx

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// Patrón: useApp() + useActiveAccount() + useIsMobile(), igual que KitchenItemsPage.
1515

1616
import { useEffect, useMemo, useState } from 'react'
17-
import { Search, ChevronDown, ChevronRight, CircleDashed, CheckCircle2, AlertTriangle, UtensilsCrossed, Package, Link2Off, Plus, FolderPlus, ArrowRightLeft, X, Undo2, Info, ArrowUp, ArrowDown, Trash2, UploadCloud, Loader2, Sparkles, PackagePlus } from 'lucide-react'
17+
import { Search, ChevronDown, ChevronRight, CircleDashed, CheckCircle2, AlertTriangle, UtensilsCrossed, Package, Link2Off, Link2, Plus, FolderPlus, ArrowRightLeft, X, Undo2, Info, ArrowUp, ArrowDown, Trash2, UploadCloud, Loader2, Sparkles, PackagePlus } from 'lucide-react'
1818
import { useActiveAccount } from '@/modules/multitenancy/hooks/useActiveAccount'
1919
import {
2020
listBrandsWithCatalog,
@@ -34,6 +34,7 @@ import NewCategoryModal from '@/modules/kitchen/components/NewCategoryModal'
3434
import type { MenuItemEconomics } from '@/types/kitchen'
3535
import { publishBrandCatalog, type PublishResult } from '@/modules/kitchen/services/catalogPublishService'
3636
import PublishStatusChip from '@/modules/kitchen/components/PublishStatusChip'
37+
import { connectBrandToDelivery, type ConnectResult } from '@/modules/kitchen/services/hubriseBrandConnectService'
3738

3839
function formatEur(value: number | null): string {
3940
if (value === null || value === undefined) return '—'
@@ -80,6 +81,8 @@ export default function KitchenMenuPage() {
8081
const [publishing, setPublishing] = useState(false)
8182
const [publishResult, setPublishResult] = useState<PublishResult | null>(null)
8283
const [publishStatusKey, setPublishStatusKey] = useState(0)
84+
const [connecting, setConnecting] = useState(false)
85+
const [connectResult, setConnectResult] = useState<ConnectResult | null>(null)
8386

8487
// Cargar marcas con catálogo
8588
useEffect(() => {
@@ -242,6 +245,24 @@ export default function KitchenMenuPage() {
242245
}
243246
}
244247

248+
// ── Conectar la marca a delivery (Fase 2, self-service) ───────────────────
249+
// Crea/reusa el catálogo HubRise por local (sin bridge) y publica la carta.
250+
async function handleConnect() {
251+
if (!selectedBrand || connecting) return
252+
setConnecting(true)
253+
setConnectResult(null)
254+
setError(null)
255+
try {
256+
const res = await connectBrandToDelivery(selectedBrand.id)
257+
setConnectResult(res)
258+
setPublishStatusKey((k) => k + 1)
259+
} catch (e: unknown) {
260+
setError(e instanceof Error ? e.message : String(e))
261+
} finally {
262+
setConnecting(false)
263+
}
264+
}
265+
245266
// ── Capa 1: organizar la carta (mover/recategorizar) ──────────────────────
246267

247268
// Categoría actual de cada producto (para poder deshacer un movimiento).
@@ -508,6 +529,17 @@ export default function KitchenMenuPage() {
508529
{selectedBrand.catalogSource === 'folvy' && activeAccountId && (
509530
<PublishStatusChip accountId={activeAccountId} brandId={selectedBrand.id} refreshKey={publishStatusKey} />
510531
)}
532+
{selectedBrand.catalogSource === 'folvy' && (
533+
<button
534+
onClick={handleConnect}
535+
disabled={connecting}
536+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg font-medium border border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:opacity-50"
537+
title="Crear/reusar el catálogo de esta marca en HubRise y publicar la carta (sin bridge)"
538+
>
539+
{connecting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Link2 className="w-4 h-4" />}
540+
{connecting ? 'Conectando…' : 'Conectar a delivery'}
541+
</button>
542+
)}
511543
{selectedBrand.catalogSource === 'folvy' && (
512544
<button
513545
onClick={handlePublish}
@@ -888,6 +920,68 @@ export default function KitchenMenuPage() {
888920
</div>
889921
)}
890922

923+
{connectResult && (
924+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => setConnectResult(null)}>
925+
<div className="bg-white rounded-xl shadow-lg w-full max-w-lg border border-gray-200" onClick={(e) => e.stopPropagation()}>
926+
<div className="px-5 py-3.5 border-b border-gray-200 flex items-center gap-2">
927+
{connectResult.ok
928+
? <CheckCircle2 className="w-5 h-5 text-green-600" />
929+
: <AlertTriangle className="w-5 h-5 text-red-600" />}
930+
<h3 className="text-base font-medium text-gray-900">
931+
{connectResult.ok ? 'Marca conectada a delivery' : 'No se pudo conectar'}
932+
</h3>
933+
</div>
934+
<div className="px-5 py-4 text-sm text-gray-700 space-y-3 max-h-[60vh] overflow-auto">
935+
{connectResult.error && <p className="text-red-700">{connectResult.error}</p>}
936+
{connectResult.locations.length > 0 && (
937+
<div>
938+
<div className="text-xs font-medium text-gray-500 mb-1">Por local</div>
939+
<ul className="space-y-1">
940+
{connectResult.locations.map((l, i) => (
941+
<li key={i} className="flex items-start gap-2">
942+
{l.status === 'error'
943+
? <AlertTriangle className="w-4 h-4 text-red-600 mt-0.5 shrink-0" />
944+
: <CheckCircle2 className="w-4 h-4 text-green-600 mt-0.5 shrink-0" />}
945+
<span>
946+
<span className="font-medium">{l.location_name}</span>
947+
{' · '}
948+
{l.status === 'ya_conectada' && 'ya conectada'}
949+
{l.status === 'creada' && 'catálogo creado'}
950+
{l.status === 'reusada_por_nombre' && 'catálogo reusado'}
951+
{l.status === 'error' && 'error'}
952+
{l.external_catalog_id && (
953+
<span className="text-gray-400"> ({l.external_catalog_id})</span>
954+
)}
955+
{l.status === 'error' && l.error && (
956+
<span className="block text-xs text-red-600">{l.error}</span>
957+
)}
958+
</span>
959+
</li>
960+
))}
961+
</ul>
962+
</div>
963+
)}
964+
{connectResult.publish && (
965+
<div>
966+
<div className="text-xs font-medium text-gray-500 mb-1">Publicación de la carta</div>
967+
{connectResult.publish.ok ? (
968+
<p className="text-gray-600">
969+
{connectResult.publish.products ?? 0} producto{(connectResult.publish.products ?? 0) === 1 ? '' : 's'} · {connectResult.publish.deals ?? 0} combo{(connectResult.publish.deals ?? 0) === 1 ? '' : 's'}
970+
</p>
971+
) : (
972+
<p className="text-red-700">{connectResult.publish.error ?? 'No se pudo publicar la carta.'}</p>
973+
)}
974+
</div>
975+
)}
976+
</div>
977+
<div className="flex items-center justify-end gap-2 px-5 py-3.5 border-t border-gray-200 bg-gray-50 rounded-b-xl">
978+
<button onClick={() => setConnectResult(null)}
979+
className="px-3.5 py-1.5 text-sm rounded-lg font-medium bg-accent text-text-on-accent hover:opacity-90">Entendido</button>
980+
</div>
981+
</div>
982+
</div>
983+
)}
984+
891985
{showNewCategory && activeAccountId && selectedBrand && (
892986
<NewCategoryModal
893987
accountId={activeAccountId}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// src/modules/kitchen/services/hubriseBrandConnectService.ts
2+
//
3+
// Servicio del ASISTENTE "Conectar a delivery" (front -> Edge
4+
// hubrise-brand-connect). El Edge hace el trabajo (crear/reusar catálogo por
5+
// local con el token escritor, guardar brand_hubrise_catalog, publicar la
6+
// carta reusando hubrise-catalog-publish). Aquí solo invocamos y normalizamos
7+
// el resultado. La sesión del usuario viaja sola en functions.invoke (su JWT).
8+
//
9+
// El Edge devuelve 200 también en fallos de negocio (sin token escritor, sin
10+
// locales HubRise), con { ok:false, error }, para que la UI los muestre sin
11+
// tratarlos como errores de red.
12+
13+
import { supabase, isSupabaseEnabled } from '../../../lib/supabase'
14+
import type { PublishResult } from './catalogPublishService'
15+
16+
export interface ConnectLocationResult {
17+
location_id: string
18+
external_location_id: string
19+
location_name: string
20+
status: 'ya_conectada' | 'reusada_por_nombre' | 'creada' | 'error'
21+
external_catalog_id?: string
22+
error?: string
23+
}
24+
25+
export interface ConnectResult {
26+
ok: boolean
27+
error?: string
28+
locations: ConnectLocationResult[]
29+
publish: Partial<PublishResult> | null
30+
}
31+
32+
export async function connectBrandToDelivery(brandId: string): Promise<ConnectResult> {
33+
if (!isSupabaseEnabled || !supabase) {
34+
throw new Error('Supabase no está configurado.')
35+
}
36+
37+
const { data, error } = await supabase.functions.invoke('hubrise-brand-connect', {
38+
body: { brand_id: brandId },
39+
})
40+
41+
// Error de transporte / no-2xx (auth, crash). Intentamos leer el cuerpo si lo hay.
42+
if (error) {
43+
let msg = error.message ?? 'Error conectando la marca a delivery.'
44+
try {
45+
const ctx = (error as unknown as { context?: Response }).context
46+
if (ctx && typeof ctx.json === 'function') {
47+
const j = await ctx.json()
48+
if (j?.error) msg = j.error
49+
}
50+
} catch { /* ignore */ }
51+
return { ok: false, error: msg, locations: [], publish: null }
52+
}
53+
54+
const d = (data ?? {}) as Partial<ConnectResult>
55+
return {
56+
ok: d.ok === true,
57+
error: d.error,
58+
locations: d.locations ?? [],
59+
publish: d.publish ?? null,
60+
}
61+
}

0 commit comments

Comments
 (0)