feat: multi-periodo de precos com CRUD na aba Precos
- PrecosTab: lista periodos como chips clicaveis (editar/excluir inline) - Modal de CRUD de periodo: nome + checkboxes dos 7 dias da semana - Grid de precos filtrado pelo periodo selecionado - Salvar afeta so o periodo ativo (delete + insert scoped) - Nao permite excluir o ultimo periodo - catalogoService.findPrecoForDate resolve o periodo pelo dia da semana - useReservationForm usa findPrecoForDate quando checkinAt esta preenchido Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09c7903a9c
commit
337229ce22
@ -74,16 +74,35 @@ export function useReservationForm(initialPrefill?: PrefillData) {
|
|||||||
}, [tenantId, form.marcaId])
|
}, [tenantId, form.marcaId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tenantId) return
|
if (!tenantId || !form.marcaId || !form.categoria || !form.permanencia) {
|
||||||
if (!form.marcaId || !form.categoria || !form.permanencia) {
|
|
||||||
setPreco(null)
|
setPreco(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
catalogoService
|
const fetchPreco = async () => {
|
||||||
.findPreco(tenantId, form.marcaId, form.categoria, form.permanencia)
|
try {
|
||||||
.then(setPreco)
|
const checkinDate = form.checkinAt ? new Date(form.checkinAt) : null
|
||||||
.catch((err: Error) => setError(err.message))
|
const p =
|
||||||
}, [tenantId, form.marcaId, form.categoria, form.permanencia])
|
checkinDate && !isNaN(checkinDate.getTime())
|
||||||
|
? await catalogoService.findPrecoForDate(
|
||||||
|
tenantId,
|
||||||
|
form.marcaId,
|
||||||
|
form.categoria,
|
||||||
|
form.permanencia,
|
||||||
|
checkinDate
|
||||||
|
)
|
||||||
|
: await catalogoService.findPreco(
|
||||||
|
tenantId,
|
||||||
|
form.marcaId,
|
||||||
|
form.categoria,
|
||||||
|
form.permanencia
|
||||||
|
)
|
||||||
|
setPreco(p)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erro ao carregar preço')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void fetchPreco()
|
||||||
|
}, [tenantId, form.marcaId, form.categoria, form.permanencia, form.checkinAt])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tenantId) return
|
if (!tenantId) return
|
||||||
|
|||||||
@ -1,26 +1,59 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useMemo } from 'react'
|
||||||
import { supabase } from '@/lib/supabase'
|
import { supabase } from '@/lib/supabase'
|
||||||
import { useTenantId } from '@/hooks/useAppConfig'
|
import { useTenantId } from '@/hooks/useAppConfig'
|
||||||
import type { Database } from '@/types/database'
|
import type { Database } from '@/types/database'
|
||||||
import { SelectField } from '@/components/SelectField'
|
import { SelectField } from '@/components/SelectField'
|
||||||
|
import { FormField } from '@/components/FormField'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Modal } from '@/components/admin/Modal'
|
||||||
|
|
||||||
type Marca = Database['reserva_hotel']['Tables']['marcas']['Row']
|
type Marca = Database['reserva_hotel']['Tables']['marcas']['Row']
|
||||||
type Preco = Database['reserva_hotel']['Tables']['precos']['Row']
|
type Preco = Database['reserva_hotel']['Tables']['precos']['Row']
|
||||||
|
type Periodo = Database['reserva_hotel']['Tables']['marca_periodos']['Row']
|
||||||
|
|
||||||
// key: `${categoria}|${permanencia}` → valor em reais (string for input)
|
// key: `${categoria}|${permanencia}` → valor em reais (string for input)
|
||||||
type PriceMap = Record<string, string>
|
type PriceMap = Record<string, string>
|
||||||
|
|
||||||
|
const DIAS_SEMANA = [
|
||||||
|
{ value: 0, label: 'Dom' },
|
||||||
|
{ value: 1, label: 'Seg' },
|
||||||
|
{ value: 2, label: 'Ter' },
|
||||||
|
{ value: 3, label: 'Qua' },
|
||||||
|
{ value: 4, label: 'Qui' },
|
||||||
|
{ value: 5, label: 'Sex' },
|
||||||
|
{ value: 6, label: 'Sáb' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function slugify(text: string): string {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/(^-|-$)/g, '')
|
||||||
|
.slice(0, 50) || 'periodo'
|
||||||
|
}
|
||||||
|
|
||||||
export function PrecosTab() {
|
export function PrecosTab() {
|
||||||
const tenantId = useTenantId()
|
const tenantId = useTenantId()
|
||||||
const [marcas, setMarcas] = useState<Marca[]>([])
|
const [marcas, setMarcas] = useState<Marca[]>([])
|
||||||
const [selectedMarcaId, setSelectedMarcaId] = useState<string>('')
|
const [selectedMarcaId, setSelectedMarcaId] = useState<string>('')
|
||||||
|
const [periodos, setPeriodos] = useState<Periodo[]>([])
|
||||||
|
const [selectedPeriodoId, setSelectedPeriodoId] = useState<string>('')
|
||||||
const [priceMap, setPriceMap] = useState<PriceMap>({})
|
const [priceMap, setPriceMap] = useState<PriceMap>({})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [successMsg, setSuccessMsg] = useState<string | null>(null)
|
const [successMsg, setSuccessMsg] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Period management modal state
|
||||||
|
const [periodoModalOpen, setPeriodoModalOpen] = useState(false)
|
||||||
|
const [editingPeriodo, setEditingPeriodo] = useState<Periodo | null>(null)
|
||||||
|
const [periodoForm, setPeriodoForm] = useState({ nome: '', dias: [] as number[] })
|
||||||
|
const [periodoFormError, setPeriodoFormError] = useState<string | null>(null)
|
||||||
|
const [periodoSaving, setPeriodoSaving] = useState(false)
|
||||||
|
|
||||||
|
// ----- Load marcas -----
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tenantId) return
|
if (!tenantId) return
|
||||||
supabase
|
supabase
|
||||||
@ -30,19 +63,49 @@ export function PrecosTab() {
|
|||||||
.order('nome')
|
.order('nome')
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
setMarcas(data ?? [])
|
setMarcas(data ?? [])
|
||||||
if (data && data.length > 0 && !selectedMarcaId) setSelectedMarcaId(data[0].id)
|
if (data && data.length > 0 && !selectedMarcaId) {
|
||||||
|
setSelectedMarcaId(data[0].id)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [tenantId])
|
}, [tenantId])
|
||||||
|
|
||||||
|
// ----- Load periodos when marca changes -----
|
||||||
|
const loadPeriodos = async (marcaId: string) => {
|
||||||
|
if (!marcaId) return
|
||||||
|
const { data } = await supabase
|
||||||
|
.from('marca_periodos')
|
||||||
|
.select('*')
|
||||||
|
.eq('id_marca', marcaId)
|
||||||
|
.eq('ativo', true)
|
||||||
|
.order('ordem')
|
||||||
|
const list = data ?? []
|
||||||
|
setPeriodos(list)
|
||||||
|
// Seleciona o primeiro período se nenhum está selecionado ou o atual sumiu
|
||||||
|
if (list.length > 0 && !list.find((p) => p.id === selectedPeriodoId)) {
|
||||||
|
setSelectedPeriodoId(list[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedMarcaId) return
|
if (selectedMarcaId) void loadPeriodos(selectedMarcaId)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedMarcaId])
|
||||||
|
|
||||||
|
// ----- Load precos when marca+periodo selected -----
|
||||||
|
const selectedPeriodo = periodos.find((p) => p.id === selectedPeriodoId) ?? null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedMarcaId || !selectedPeriodo) {
|
||||||
|
setPriceMap({})
|
||||||
|
return
|
||||||
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
supabase
|
supabase
|
||||||
.from('precos')
|
.from('precos')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id_marca', selectedMarcaId)
|
.eq('id_marca', selectedMarcaId)
|
||||||
.eq('periodo_semana', 'default')
|
.eq('periodo_semana', selectedPeriodo.slug)
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
const map: PriceMap = {}
|
const map: PriceMap = {}
|
||||||
;(data ?? []).forEach((p: Preco) => {
|
;(data ?? []).forEach((p: Preco) => {
|
||||||
@ -51,18 +114,19 @@ export function PrecosTab() {
|
|||||||
setPriceMap(map)
|
setPriceMap(map)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [selectedMarcaId])
|
}, [selectedMarcaId, selectedPeriodo])
|
||||||
|
|
||||||
const marca = marcas.find((m) => m.id === selectedMarcaId)
|
const marca = marcas.find((m) => m.id === selectedMarcaId)
|
||||||
const categorias = marca?.categorias ?? []
|
const categorias = useMemo(() => marca?.categorias ?? [], [marca])
|
||||||
const permanencias = marca?.permanencias ?? []
|
const permanencias = useMemo(() => marca?.permanencias ?? [], [marca])
|
||||||
|
|
||||||
const setPrice = (categoria: string, permanencia: string, value: string) => {
|
const setPrice = (categoria: string, permanencia: string, value: string) => {
|
||||||
setPriceMap({ ...priceMap, [`${categoria}|${permanencia}`]: value })
|
setPriceMap({ ...priceMap, [`${categoria}|${permanencia}`]: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Save prices for selected period -----
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!selectedMarcaId || !tenantId) return
|
if (!selectedMarcaId || !tenantId || !selectedPeriodo) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setSuccessMsg(null)
|
setSuccessMsg(null)
|
||||||
@ -77,7 +141,7 @@ export function PrecosTab() {
|
|||||||
id_marca: selectedMarcaId,
|
id_marca: selectedMarcaId,
|
||||||
categoria: cat,
|
categoria: cat,
|
||||||
permanencia: perm,
|
permanencia: perm,
|
||||||
periodo_semana: 'default',
|
periodo_semana: selectedPeriodo.slug,
|
||||||
valor,
|
valor,
|
||||||
ativo: true,
|
ativo: true,
|
||||||
}
|
}
|
||||||
@ -97,7 +161,7 @@ export function PrecosTab() {
|
|||||||
.from('precos')
|
.from('precos')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('id_marca', selectedMarcaId)
|
.eq('id_marca', selectedMarcaId)
|
||||||
.eq('periodo_semana', 'default')
|
.eq('periodo_semana', selectedPeriodo.slug)
|
||||||
if (delErr) throw new Error(delErr.message)
|
if (delErr) throw new Error(delErr.message)
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
@ -105,7 +169,7 @@ export function PrecosTab() {
|
|||||||
if (insErr) throw new Error(insErr.message)
|
if (insErr) throw new Error(insErr.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuccessMsg('Preços salvos!')
|
setSuccessMsg(`Preços de "${selectedPeriodo.nome}" salvos!`)
|
||||||
setTimeout(() => setSuccessMsg(null), 2500)
|
setTimeout(() => setSuccessMsg(null), 2500)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Erro ao salvar')
|
setError(e instanceof Error ? e.message : 'Erro ao salvar')
|
||||||
@ -114,11 +178,99 @@ export function PrecosTab() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Period CRUD -----
|
||||||
|
const openCreatePeriodo = () => {
|
||||||
|
setEditingPeriodo(null)
|
||||||
|
setPeriodoForm({ nome: '', dias: [] })
|
||||||
|
setPeriodoFormError(null)
|
||||||
|
setPeriodoModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditPeriodo = (p: Periodo) => {
|
||||||
|
setEditingPeriodo(p)
|
||||||
|
setPeriodoForm({ nome: p.nome, dias: p.dias ?? [] })
|
||||||
|
setPeriodoFormError(null)
|
||||||
|
setPeriodoModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDia = (dia: number) => {
|
||||||
|
setPeriodoForm((f) => ({
|
||||||
|
...f,
|
||||||
|
dias: f.dias.includes(dia) ? f.dias.filter((d) => d !== dia) : [...f.dias, dia].sort(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePeriodoSave = async () => {
|
||||||
|
if (!tenantId || !selectedMarcaId) return
|
||||||
|
if (!periodoForm.nome.trim()) return setPeriodoFormError('Nome é obrigatório')
|
||||||
|
if (periodoForm.dias.length === 0) return setPeriodoFormError('Selecione pelo menos um dia')
|
||||||
|
|
||||||
|
setPeriodoSaving(true)
|
||||||
|
setPeriodoFormError(null)
|
||||||
|
try {
|
||||||
|
if (editingPeriodo) {
|
||||||
|
const { error: err } = await supabase
|
||||||
|
.from('marca_periodos')
|
||||||
|
.update({
|
||||||
|
nome: periodoForm.nome.trim(),
|
||||||
|
dias: periodoForm.dias,
|
||||||
|
})
|
||||||
|
.eq('id', editingPeriodo.id)
|
||||||
|
if (err) throw new Error(err.message)
|
||||||
|
} else {
|
||||||
|
const slug = slugify(periodoForm.nome)
|
||||||
|
const ordem = periodos.length
|
||||||
|
const { error: err } = await supabase.from('marca_periodos').insert({
|
||||||
|
tenant_id: tenantId,
|
||||||
|
id_marca: selectedMarcaId,
|
||||||
|
slug,
|
||||||
|
nome: periodoForm.nome.trim(),
|
||||||
|
dias: periodoForm.dias,
|
||||||
|
ordem,
|
||||||
|
ativo: true,
|
||||||
|
})
|
||||||
|
if (err) throw new Error(err.message)
|
||||||
|
}
|
||||||
|
await loadPeriodos(selectedMarcaId)
|
||||||
|
setPeriodoModalOpen(false)
|
||||||
|
} catch (e) {
|
||||||
|
setPeriodoFormError(e instanceof Error ? e.message : 'Erro ao salvar')
|
||||||
|
} finally {
|
||||||
|
setPeriodoSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePeriodoDelete = async (p: Periodo) => {
|
||||||
|
if (periodos.length <= 1) {
|
||||||
|
alert('Não pode excluir o último período. Crie outro antes.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!confirm(`Excluir o período "${p.nome}"? Os preços cadastrados serão removidos junto.`))
|
||||||
|
return
|
||||||
|
try {
|
||||||
|
// deleta precos do período primeiro
|
||||||
|
await supabase
|
||||||
|
.from('precos')
|
||||||
|
.delete()
|
||||||
|
.eq('id_marca', selectedMarcaId)
|
||||||
|
.eq('periodo_semana', p.slug)
|
||||||
|
// depois o período
|
||||||
|
const { error: err } = await supabase.from('marca_periodos').delete().eq('id', p.id)
|
||||||
|
if (err) throw new Error(err.message)
|
||||||
|
if (selectedPeriodoId === p.id) setSelectedPeriodoId('')
|
||||||
|
await loadPeriodos(selectedMarcaId)
|
||||||
|
} catch (e) {
|
||||||
|
alert(e instanceof Error ? e.message : 'Erro ao excluir período')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl space-y-6">
|
<div className="max-w-5xl space-y-6">
|
||||||
<header>
|
<header>
|
||||||
<h1 className="font-serif text-3xl text-gradient-gold mb-2">Preços</h1>
|
<h1 className="font-serif text-3xl text-gradient-gold mb-2">Preços</h1>
|
||||||
<p className="text-slate text-sm">Grid categoria × permanência. Valores em reais.</p>
|
<p className="text-slate text-sm">
|
||||||
|
Grid categoria × permanência por período da semana. Valores em reais.
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
@ -128,6 +280,64 @@ export function PrecosTab() {
|
|||||||
options={marcas.map((m) => ({ value: m.id, label: m.nome }))}
|
options={marcas.map((m) => ({ value: m.id, label: m.nome }))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{selectedMarcaId && (
|
||||||
|
<section className="space-y-3 rounded-2xl border border-champagne/20 bg-midnight/40 p-4">
|
||||||
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
|
<h2 className="font-serif text-lg text-champagne">Período da semana</h2>
|
||||||
|
<Button variant="secondary" size="sm" onClick={openCreatePeriodo}>
|
||||||
|
+ Novo período
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{periodos.length === 0 && (
|
||||||
|
<p className="text-slate text-sm">Nenhum período cadastrado.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{periodos.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{periodos.map((p) => (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className={`group rounded-lg border px-3 py-2 text-sm font-sans transition cursor-pointer ${
|
||||||
|
p.id === selectedPeriodoId
|
||||||
|
? 'bg-champagne text-obsidian border-champagne font-semibold'
|
||||||
|
: 'bg-midnight/60 text-ivory border-champagne/30 hover:border-champagne'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedPeriodoId(p.id)}
|
||||||
|
>
|
||||||
|
<span>{p.nome}</span>
|
||||||
|
<span className="ml-2 text-xs opacity-70">
|
||||||
|
({p.dias.map((d) => DIAS_SEMANA[d]?.label).join(' ')})
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
openEditPeriodo(p)
|
||||||
|
}}
|
||||||
|
className="ml-2 opacity-60 hover:opacity-100"
|
||||||
|
aria-label="Editar período"
|
||||||
|
>
|
||||||
|
✎
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
void handlePeriodoDelete(p)
|
||||||
|
}}
|
||||||
|
className="ml-1 opacity-60 hover:opacity-100"
|
||||||
|
aria-label="Excluir período"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading && <p className="text-slate">Carregando preços...</p>}
|
{loading && <p className="text-slate">Carregando preços...</p>}
|
||||||
|
|
||||||
{!loading && marca && (categorias.length === 0 || permanencias.length === 0) && (
|
{!loading && marca && (categorias.length === 0 || permanencias.length === 0) && (
|
||||||
@ -136,14 +346,19 @@ export function PrecosTab() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && categorias.length > 0 && permanencias.length > 0 && (
|
{!loading && selectedPeriodo && categorias.length > 0 && permanencias.length > 0 && (
|
||||||
<div className="overflow-x-auto rounded-2xl border border-champagne/20 bg-midnight/40">
|
<div className="overflow-x-auto rounded-2xl border border-champagne/20 bg-midnight/40">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="border-b border-champagne/20">
|
<thead className="border-b border-champagne/20">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left font-sans text-xs uppercase tracking-widest text-champagne">Categoria</th>
|
<th className="px-4 py-3 text-left font-sans text-xs uppercase tracking-widest text-champagne">
|
||||||
|
Categoria
|
||||||
|
</th>
|
||||||
{permanencias.map((p) => (
|
{permanencias.map((p) => (
|
||||||
<th key={p} className="px-4 py-3 text-left font-sans text-xs uppercase tracking-widest text-champagne">
|
<th
|
||||||
|
key={p}
|
||||||
|
className="px-4 py-3 text-left font-sans text-xs uppercase tracking-widest text-champagne"
|
||||||
|
>
|
||||||
{p}
|
{p}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
@ -172,12 +387,76 @@ export function PrecosTab() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <div className="rounded-xl border border-ruby/40 bg-ruby/10 p-4 text-ivory">{error}</div>}
|
{error && (
|
||||||
{successMsg && <div className="rounded-xl border border-emerald/40 bg-emerald/10 p-4 text-ivory">{successMsg}</div>}
|
<div className="rounded-xl border border-ruby/40 bg-ruby/10 p-4 text-ivory">{error}</div>
|
||||||
|
)}
|
||||||
|
{successMsg && (
|
||||||
|
<div className="rounded-xl border border-emerald/40 bg-emerald/10 p-4 text-ivory">
|
||||||
|
{successMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button variant="primary" size="lg" onClick={handleSave} disabled={saving || !selectedMarcaId}>
|
{selectedPeriodo && (
|
||||||
{saving ? 'Salvando...' : 'Salvar preços'}
|
<Button variant="primary" size="lg" onClick={handleSave} disabled={saving}>
|
||||||
</Button>
|
{saving ? 'Salvando...' : `Salvar preços de "${selectedPeriodo.nome}"`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Period CRUD modal */}
|
||||||
|
<Modal
|
||||||
|
open={periodoModalOpen}
|
||||||
|
title={editingPeriodo ? 'Editar período' : 'Novo período'}
|
||||||
|
onClose={() => setPeriodoModalOpen(false)}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="ghost" onClick={() => setPeriodoModalOpen(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handlePeriodoSave} disabled={periodoSaving}>
|
||||||
|
{periodoSaving ? 'Salvando...' : 'Salvar'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
label="Nome do período"
|
||||||
|
required
|
||||||
|
value={periodoForm.nome}
|
||||||
|
onChange={(e) => setPeriodoForm({ ...periodoForm, nome: e.target.value })}
|
||||||
|
placeholder="Ex: Segunda a Quarta"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="font-sans text-xs uppercase tracking-widest text-champagne">
|
||||||
|
Dias da semana
|
||||||
|
</label>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{DIAS_SEMANA.map((dia) => {
|
||||||
|
const selected = periodoForm.dias.includes(dia.value)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={dia.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleDia(dia.value)}
|
||||||
|
className={`rounded-lg border px-4 py-2 text-sm font-sans transition ${
|
||||||
|
selected
|
||||||
|
? 'bg-champagne text-obsidian border-champagne font-semibold'
|
||||||
|
: 'bg-midnight/60 text-ivory border-champagne/30 hover:border-champagne'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{dia.label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{periodoFormError && (
|
||||||
|
<div className="rounded-xl border border-ruby/40 bg-ruby/10 p-3 text-ivory text-sm">
|
||||||
|
{periodoFormError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,44 @@ export const catalogoService = {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async findPrecoForDate(
|
||||||
|
tenantId: number,
|
||||||
|
marcaId: string,
|
||||||
|
categoria: string,
|
||||||
|
permanencia: string,
|
||||||
|
checkinDate: Date
|
||||||
|
): Promise<Preco | null> {
|
||||||
|
const dayOfWeek = checkinDate.getDay() // 0=dom..6=sab
|
||||||
|
|
||||||
|
// Resolve o periodo que contem esse dia
|
||||||
|
const { data: periodos } = await supabase
|
||||||
|
.from('marca_periodos')
|
||||||
|
.select('*')
|
||||||
|
.eq('id_marca', marcaId)
|
||||||
|
.eq('ativo', true)
|
||||||
|
.order('ordem')
|
||||||
|
|
||||||
|
const matched = (periodos ?? []).find(
|
||||||
|
(p) => Array.isArray(p.dias) && p.dias.includes(dayOfWeek)
|
||||||
|
)
|
||||||
|
|
||||||
|
const slug = matched?.slug ?? 'default'
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('precos')
|
||||||
|
.select('*')
|
||||||
|
.eq('tenant_id', tenantId)
|
||||||
|
.eq('id_marca', marcaId)
|
||||||
|
.eq('categoria', categoria)
|
||||||
|
.eq('permanencia', permanencia)
|
||||||
|
.eq('periodo_semana', slug)
|
||||||
|
.eq('ativo', true)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (error) throw new Error(error.message)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
async listFotos(tenantId: number, 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')
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user