feat(fase4-c): tenant resolver + theming dinamico
- src/lib/tenant.ts: resolve slug por subdominio (fallback env) - src/lib/appConfig.ts: carrega tenant + app_config do Supabase - TenantProvider: aplica CSS vars, Google Fonts dinamico, title/favicon - useAppConfig + useTenantId hooks - App.tsx renderiza titulo/subtitulo/tagline/footer do config - catalogoService filtra todas queries por tenant_id - useReservationForm aguarda tenantId antes de buscar - Testes mockam TenantProvider + useAppConfig - vite.config: host: true + allowedHosts (tunnel-friendly) - chatwootApi: header ngrok-skip-browser-warning
This commit is contained in:
parent
3cb5ecf47d
commit
f4980f026c
@ -6,3 +6,6 @@ VITE_SUPABASE_SCHEMA=reserva_hotel
|
|||||||
# Chatwoot — token de integração (Fase 2)
|
# Chatwoot — token de integração (Fase 2)
|
||||||
VITE_CHATWOOT_API_URL=https://chatwoot.fazer.ai
|
VITE_CHATWOOT_API_URL=https://chatwoot.fazer.ai
|
||||||
VITE_CHATWOOT_API_TOKEN=
|
VITE_CHATWOOT_API_TOKEN=
|
||||||
|
|
||||||
|
# Multi-tenant: slug default quando rodando em localhost/tunnel (sem subdomínio)
|
||||||
|
VITE_DEFAULT_TENANT_SLUG=grupo-1001
|
||||||
|
|||||||
47
src/App.tsx
47
src/App.tsx
@ -1,25 +1,52 @@
|
|||||||
import { ReservationFlow } from '@/components/reservation/ReservationFlow'
|
import { ReservationFlow } from '@/components/reservation/ReservationFlow'
|
||||||
|
import { useTenant } from '@/contexts/TenantProvider'
|
||||||
|
import { useAppConfig } from '@/hooks/useAppConfig'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { loading, error } = useTenant()
|
||||||
|
const config = useAppConfig()
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<p className="text-slate font-sans">Carregando...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !config) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center px-6">
|
||||||
|
<div className="rounded-xl border border-ruby/40 bg-ruby/10 p-6 text-ivory text-center max-w-md">
|
||||||
|
{error ?? 'Configuração não encontrada'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen flex flex-col items-center px-6 py-12">
|
<main className="min-h-screen flex flex-col items-center px-6 py-12">
|
||||||
<header className="text-center mb-10">
|
<header className="text-center mb-10">
|
||||||
<p className="font-sans text-sm uppercase tracking-[0.3em] text-rose-gold mb-4">
|
{config.subtitulo_hero && (
|
||||||
Experiência exclusiva
|
<p className="font-sans text-sm uppercase tracking-[0.3em] text-rose-gold mb-4">
|
||||||
</p>
|
{config.subtitulo_hero}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<h1 className="font-serif text-5xl md:text-6xl text-gradient-gold mb-3">
|
<h1 className="font-serif text-5xl md:text-6xl text-gradient-gold mb-3">
|
||||||
Reserva Rede 1001
|
{config.titulo_hero}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-sans text-slate text-lg">
|
{config.tagline && (
|
||||||
Escolha, confirme e receba seu PIX na hora.
|
<p className="font-sans text-slate text-lg">{config.tagline}</p>
|
||||||
</p>
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<ReservationFlow />
|
<ReservationFlow />
|
||||||
|
|
||||||
<footer className="mt-16 text-slate text-xs uppercase tracking-widest">
|
{config.footer_text && (
|
||||||
© 2026 Reserva Rede 1001 · Experiência Exclusiva
|
<footer className="mt-16 text-slate text-xs uppercase tracking-widest">
|
||||||
</footer>
|
{config.footer_text}
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,55 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { describe, it, expect, vi } from 'vitest'
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('@/contexts/TenantProvider', () => ({
|
||||||
|
TenantProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
useTenant: () => ({
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
context: {
|
||||||
|
tenant: {
|
||||||
|
id: 1,
|
||||||
|
slug: 'test',
|
||||||
|
nome: 'Test',
|
||||||
|
ativo: true,
|
||||||
|
created_at: '',
|
||||||
|
updated_at: '',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
id: 1,
|
||||||
|
tenant_id: 1,
|
||||||
|
nome_rede: 'Test Rede',
|
||||||
|
titulo_hero: 'Reserva Rede 1001',
|
||||||
|
subtitulo_hero: 'Teste',
|
||||||
|
tagline: null,
|
||||||
|
footer_text: null,
|
||||||
|
logo_url: null,
|
||||||
|
favicon_url: null,
|
||||||
|
cor_primaria: '#C9A961',
|
||||||
|
cor_secundaria: '#E8B4A0',
|
||||||
|
cor_fundo: '#0B0D12',
|
||||||
|
cor_superficie: '#0F1A2E',
|
||||||
|
cor_texto: '#F5F1E8',
|
||||||
|
fonte_display: 'Fraunces',
|
||||||
|
fonte_corpo: 'Inter',
|
||||||
|
created_at: '',
|
||||||
|
updated_at: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refresh: vi.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/hooks/useAppConfig', () => ({
|
||||||
|
useAppConfig: () => ({
|
||||||
|
titulo_hero: 'Reserva Rede 1001',
|
||||||
|
subtitulo_hero: 'Teste',
|
||||||
|
tagline: null,
|
||||||
|
footer_text: null,
|
||||||
|
}),
|
||||||
|
useTenantId: () => 1,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/hooks/useReservationForm', () => ({
|
vi.mock('@/hooks/useReservationForm', () => ({
|
||||||
useReservationForm: () => ({
|
useReservationForm: () => ({
|
||||||
form: {
|
form: {
|
||||||
|
|||||||
97
src/contexts/TenantProvider.tsx
Normal file
97
src/contexts/TenantProvider.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
||||||
|
import type { LoadedTenantContext } from '@/lib/appConfig'
|
||||||
|
import { loadTenantContext } from '@/lib/appConfig'
|
||||||
|
import { resolveTenantSlug } from '@/lib/tenant'
|
||||||
|
|
||||||
|
interface TenantContextValue {
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
context: LoadedTenantContext | null
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const TenantContext = createContext<TenantContextValue | null>(null)
|
||||||
|
|
||||||
|
export function TenantProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [context, setContext] = useState<LoadedTenantContext | null>(null)
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const slug = resolveTenantSlug()
|
||||||
|
const ctx = await loadTenantContext(slug)
|
||||||
|
if (!ctx) {
|
||||||
|
setError(`Tenant "${slug}" não encontrado ou inativo.`)
|
||||||
|
} else {
|
||||||
|
setContext(ctx)
|
||||||
|
applyTheme(ctx)
|
||||||
|
applyMetadata(ctx)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Erro ao carregar tenant')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TenantContext.Provider value={{ loading, error, context, refresh: load }}>
|
||||||
|
{children}
|
||||||
|
</TenantContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export function useTenant() {
|
||||||
|
const ctx = useContext(TenantContext)
|
||||||
|
if (!ctx) throw new Error('useTenant must be used inside <TenantProvider>')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(ctx: LoadedTenantContext) {
|
||||||
|
const { config } = ctx
|
||||||
|
const root = document.documentElement
|
||||||
|
root.style.setProperty('--color-champagne', config.cor_primaria)
|
||||||
|
root.style.setProperty('--color-rose-gold', config.cor_secundaria)
|
||||||
|
root.style.setProperty('--color-obsidian', config.cor_fundo)
|
||||||
|
root.style.setProperty('--color-midnight', config.cor_superficie)
|
||||||
|
root.style.setProperty('--color-ivory', config.cor_texto)
|
||||||
|
|
||||||
|
loadGoogleFont(config.fonte_display)
|
||||||
|
loadGoogleFont(config.fonte_corpo)
|
||||||
|
root.style.setProperty('--font-serif', `'${config.fonte_display}', Georgia, serif`)
|
||||||
|
root.style.setProperty('--font-sans', `'${config.fonte_corpo}', system-ui, sans-serif`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadGoogleFont(family: string) {
|
||||||
|
if (!family) return
|
||||||
|
const id = `gfont-${family.replace(/\s+/g, '-').toLowerCase()}`
|
||||||
|
if (document.getElementById(id)) return
|
||||||
|
const link = document.createElement('link')
|
||||||
|
link.id = id
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400;500;600;700&display=swap`
|
||||||
|
document.head.appendChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMetadata(ctx: LoadedTenantContext) {
|
||||||
|
const { config } = ctx
|
||||||
|
document.title = config.titulo_hero
|
||||||
|
if (config.favicon_url) {
|
||||||
|
let link = document.querySelector<HTMLLinkElement>("link[rel='icon']")
|
||||||
|
if (!link) {
|
||||||
|
link = document.createElement('link')
|
||||||
|
link.rel = 'icon'
|
||||||
|
document.head.appendChild(link)
|
||||||
|
}
|
||||||
|
link.href = config.favicon_url
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/hooks/useAppConfig.ts
Normal file
11
src/hooks/useAppConfig.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useTenant } from '@/contexts/TenantProvider'
|
||||||
|
|
||||||
|
export function useAppConfig() {
|
||||||
|
const { context } = useTenant()
|
||||||
|
return context?.config ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTenantId() {
|
||||||
|
const { context } = useTenant()
|
||||||
|
return context?.tenant.id ?? null
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { catalogoService } from '@/services/catalogoService'
|
|||||||
import type { Database } from '@/types/database'
|
import type { Database } from '@/types/database'
|
||||||
import type { PrefillData } from '@/lib/prefill'
|
import type { PrefillData } from '@/lib/prefill'
|
||||||
import { prefillSimpleFields } from '@/lib/prefill'
|
import { prefillSimpleFields } from '@/lib/prefill'
|
||||||
|
import { useTenantId } from '@/hooks/useAppConfig'
|
||||||
|
|
||||||
type Marca = Database['reserva_hotel']['Tables']['marcas']['Row']
|
type Marca = Database['reserva_hotel']['Tables']['marcas']['Row']
|
||||||
type Unidade = Database['reserva_hotel']['Tables']['unidades']['Row']
|
type Unidade = Database['reserva_hotel']['Tables']['unidades']['Row']
|
||||||
@ -36,6 +37,8 @@ const empty: ReservationFormState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useReservationForm(initialPrefill?: PrefillData) {
|
export function useReservationForm(initialPrefill?: PrefillData) {
|
||||||
|
const tenantId = useTenantId()
|
||||||
|
|
||||||
const [form, setForm] = useState<ReservationFormState>(() => ({
|
const [form, setForm] = useState<ReservationFormState>(() => ({
|
||||||
...empty,
|
...empty,
|
||||||
...prefillSimpleFields(initialPrefill ?? {}),
|
...prefillSimpleFields(initialPrefill ?? {}),
|
||||||
@ -51,44 +54,48 @@ export function useReservationForm(initialPrefill?: PrefillData) {
|
|||||||
const unidadePrefillAppliedRef = useRef(false)
|
const unidadePrefillAppliedRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!tenantId) return
|
||||||
catalogoService
|
catalogoService
|
||||||
.listMarcas()
|
.listMarcas(tenantId)
|
||||||
.then(setMarcas)
|
.then(setMarcas)
|
||||||
.catch((err: Error) => setError(err.message))
|
.catch((err: Error) => setError(err.message))
|
||||||
}, [])
|
}, [tenantId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!tenantId) return
|
||||||
if (!form.marcaId) {
|
if (!form.marcaId) {
|
||||||
setUnidades([])
|
setUnidades([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catalogoService
|
catalogoService
|
||||||
.listUnidades(form.marcaId)
|
.listUnidades(tenantId, form.marcaId)
|
||||||
.then(setUnidades)
|
.then(setUnidades)
|
||||||
.catch((err: Error) => setError(err.message))
|
.catch((err: Error) => setError(err.message))
|
||||||
}, [form.marcaId])
|
}, [tenantId, form.marcaId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!tenantId) return
|
||||||
if (!form.marcaId || !form.categoria || !form.permanencia) {
|
if (!form.marcaId || !form.categoria || !form.permanencia) {
|
||||||
setPreco(null)
|
setPreco(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catalogoService
|
catalogoService
|
||||||
.findPreco(form.marcaId, form.categoria, form.permanencia)
|
.findPreco(tenantId, form.marcaId, form.categoria, form.permanencia)
|
||||||
.then(setPreco)
|
.then(setPreco)
|
||||||
.catch((err: Error) => setError(err.message))
|
.catch((err: Error) => setError(err.message))
|
||||||
}, [form.marcaId, form.categoria, form.permanencia])
|
}, [tenantId, form.marcaId, form.categoria, form.permanencia])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!tenantId) return
|
||||||
if (!form.unidadeId || !form.categoria) {
|
if (!form.unidadeId || !form.categoria) {
|
||||||
setFotos([])
|
setFotos([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catalogoService
|
catalogoService
|
||||||
.listFotos(form.unidadeId, form.categoria)
|
.listFotos(tenantId, form.unidadeId, form.categoria)
|
||||||
.then(setFotos)
|
.then(setFotos)
|
||||||
.catch((err: Error) => setError(err.message))
|
.catch((err: Error) => setError(err.message))
|
||||||
}, [form.unidadeId, form.categoria])
|
}, [tenantId, form.unidadeId, form.categoria])
|
||||||
|
|
||||||
// Resolve prefill: marcaNome -> marcaId quando marcas carregam
|
// Resolve prefill: marcaNome -> marcaId quando marcas carregam
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
33
src/lib/appConfig.ts
Normal file
33
src/lib/appConfig.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { supabase } from './supabase'
|
||||||
|
import type { Database } from '@/types/database'
|
||||||
|
|
||||||
|
export type Tenant = Database['reserva_hotel']['Tables']['tenants']['Row']
|
||||||
|
export type AppConfig = Database['reserva_hotel']['Tables']['app_config']['Row']
|
||||||
|
|
||||||
|
export interface LoadedTenantContext {
|
||||||
|
tenant: Tenant
|
||||||
|
config: AppConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTenantContext(slug: string): Promise<LoadedTenantContext | null> {
|
||||||
|
const { data: tenant, error: tenantError } = await supabase
|
||||||
|
.from('tenants')
|
||||||
|
.select('*')
|
||||||
|
.eq('slug', slug)
|
||||||
|
.eq('ativo', true)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (tenantError) throw new Error(tenantError.message)
|
||||||
|
if (!tenant) return null
|
||||||
|
|
||||||
|
const { data: config, error: configError } = await supabase
|
||||||
|
.from('app_config')
|
||||||
|
.select('*')
|
||||||
|
.eq('tenant_id', tenant.id)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (configError) throw new Error(configError.message)
|
||||||
|
if (!config) return null
|
||||||
|
|
||||||
|
return { tenant, config }
|
||||||
|
}
|
||||||
@ -43,6 +43,8 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Reserva-Token': API_TOKEN,
|
'X-Reserva-Token': API_TOKEN,
|
||||||
|
// Pula a pagina de aviso do ngrok quando o chatwoot esta atras do tunnel
|
||||||
|
'ngrok-skip-browser-warning': 'true',
|
||||||
...(init.headers ?? {}),
|
...(init.headers ?? {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
25
src/lib/tenant.ts
Normal file
25
src/lib/tenant.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Resolve o tenant slug a partir do hostname.
|
||||||
|
// Prod: https://prime.reserva.fazer.ai → slug = "prime"
|
||||||
|
// Dev: http://localhost:5180 → fallback VITE_DEFAULT_TENANT_SLUG
|
||||||
|
// Cloudflared tunnel: *.trycloudflare.com → fallback VITE_DEFAULT_TENANT_SLUG
|
||||||
|
|
||||||
|
export function resolveTenantSlug(): string {
|
||||||
|
const defaultSlug = import.meta.env.VITE_DEFAULT_TENANT_SLUG || 'grupo-1001'
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return defaultSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = window.location.hostname
|
||||||
|
|
||||||
|
if (host === 'localhost' || host.startsWith('127.') || host.endsWith('.local')) {
|
||||||
|
return defaultSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.endsWith('.trycloudflare.com') || host.endsWith('.loca.lt') || host.endsWith('.ngrok-free.dev')) {
|
||||||
|
return defaultSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = host.split('.')
|
||||||
|
return parts[0] || defaultSlug
|
||||||
|
}
|
||||||
@ -2,9 +2,12 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { TenantProvider } from '@/contexts/TenantProvider'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<TenantProvider>
|
||||||
|
<App />
|
||||||
|
</TenantProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,20 +8,22 @@ type Foto = Database['reserva_hotel']['Tables']['fotos_categoria']['Row']
|
|||||||
type Extra = Database['reserva_hotel']['Tables']['extras']['Row']
|
type Extra = Database['reserva_hotel']['Tables']['extras']['Row']
|
||||||
|
|
||||||
export const catalogoService = {
|
export const catalogoService = {
|
||||||
async listMarcas(): Promise<Marca[]> {
|
async listMarcas(tenantId: number): Promise<Marca[]> {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('marcas')
|
.from('marcas')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
.eq('ativa', true)
|
.eq('ativa', true)
|
||||||
.order('nome')
|
.order('nome')
|
||||||
if (error) throw new Error(error.message)
|
if (error) throw new Error(error.message)
|
||||||
return data ?? []
|
return data ?? []
|
||||||
},
|
},
|
||||||
|
|
||||||
async listUnidades(marcaId: string): Promise<Unidade[]> {
|
async listUnidades(tenantId: number, marcaId: string): Promise<Unidade[]> {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('unidades')
|
.from('unidades')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
.eq('id_marca', marcaId)
|
.eq('id_marca', marcaId)
|
||||||
.eq('ativa', true)
|
.eq('ativa', true)
|
||||||
.order('nome')
|
.order('nome')
|
||||||
@ -30,6 +32,7 @@ export const catalogoService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async findPreco(
|
async findPreco(
|
||||||
|
tenantId: number,
|
||||||
marcaId: string,
|
marcaId: string,
|
||||||
categoria: string,
|
categoria: string,
|
||||||
permanencia: string,
|
permanencia: string,
|
||||||
@ -38,6 +41,7 @@ export const catalogoService = {
|
|||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('precos')
|
.from('precos')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
.eq('id_marca', marcaId)
|
.eq('id_marca', marcaId)
|
||||||
.eq('categoria', categoria)
|
.eq('categoria', categoria)
|
||||||
.eq('permanencia', permanencia)
|
.eq('permanencia', permanencia)
|
||||||
@ -48,10 +52,11 @@ export const catalogoService = {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
async listFotos(unidadeId: string, categoria: string): Promise<Foto[]> {
|
async listFotos(tenantId: number, unidadeId: string, categoria: string): Promise<Foto[]> {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('fotos_categoria')
|
.from('fotos_categoria')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
.eq('id_unidade', unidadeId)
|
.eq('id_unidade', unidadeId)
|
||||||
.eq('categoria', categoria)
|
.eq('categoria', categoria)
|
||||||
.eq('ativa', true)
|
.eq('ativa', true)
|
||||||
@ -60,10 +65,11 @@ export const catalogoService = {
|
|||||||
return data ?? []
|
return data ?? []
|
||||||
},
|
},
|
||||||
|
|
||||||
async listExtras(marcaId: string): Promise<Extra[]> {
|
async listExtras(tenantId: number, marcaId: string): Promise<Extra[]> {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('extras')
|
.from('extras')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
.eq('id_marca', marcaId)
|
.eq('id_marca', marcaId)
|
||||||
.eq('ativo', true)
|
.eq('ativo', true)
|
||||||
.order('ordem')
|
.order('ordem')
|
||||||
|
|||||||
@ -901,5 +901,3 @@ export const Constants = {
|
|||||||
Enums: {},
|
Enums: {},
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
A new version of Supabase CLI is available: v2.90.0 (currently installed v2.48.3)
|
|
||||||
We recommend updating regularly for new features and bug fixes: https://supabase.com/docs/guides/cli/getting-started#updating-the-supabase-cli
|
|
||||||
|
|||||||
@ -13,10 +13,15 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 5180,
|
port: 5180,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
host: true,
|
||||||
|
// Aceita qualquer host — usado quando rodamos atrás de um tunnel (loca.lt, ngrok, cloudflared)
|
||||||
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
port: 5180,
|
port: 5180,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
host: true,
|
||||||
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user