import * as React from 'react'; import { Brand, HotelUnit, Suite, PricingData, SuiteCategoryImage, ExtraItem } from '../types.ts'; import { brandService } from '../services/brandService.ts'; import { suiteService } from '../services/suiteService.ts'; import { hotelUnitService } from '../services/hotelUnitService.ts'; import { pricingService } from '../services/pricingService.ts'; import { extraService } from '../services/extraService.ts'; import { supabase } from '../supabaseClient.ts'; import FormField from './FormField.tsx'; import { Button } from './ui/button.tsx'; import { cn } from '../lib/utils.ts'; import SelectField from './SelectField.tsx'; interface AdminPageProps { onNavigateToReservation: () => void; currentConfig: { title: string; subtitle: string }; onSaveConfig: (config: { title: string; subtitle: string }) => void; } type AdminTab = 'brands' | 'units' | 'suites' | 'prices' | 'extras' | 'settings'; const AdminPage: React.FC = ({ onNavigateToReservation, currentConfig, onSaveConfig }) => { const [isAuthenticated, setIsAuthenticated] = React.useState(false); const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [error, setError] = React.useState(''); const [activeTab, setActiveTab] = React.useState('brands'); const [isLoading, setIsLoading] = React.useState(true); // Start as true to check session const [isSaving, setIsSaving] = React.useState(false); const [saveStatus, setSaveStatus] = React.useState<{ message: string; type: 'success' | 'error'} | null>(null); // Local settings state for the form const [localSettings, setLocalSettings] = React.useState(currentConfig); // Brands state const [brands, setBrands] = React.useState([]); const [editingBrand, setEditingBrand] = React.useState(null); const [currentBrand, setCurrentBrand] = React.useState>>({ name: '', suite_categories: [], stay_durations: [] }); const [brandFormVisible, setBrandFormVisible] = React.useState(false); // Unit state const [units, setUnits] = React.useState([]); const [editingUnit, setEditingUnit] = React.useState(null); const [currentUnit, setCurrentUnit] = React.useState> & { visible_suite_categories?: string[], suite_category_images?: SuiteCategoryImage[] }>({ name: '', brandId: undefined, visible_suite_categories: [], suite_category_images: [] }); const [unitFormVisible, setUnitFormVisible] = React.useState(false); // Suite state const [suites, setSuites] = React.useState([]); const [editingSuite, setEditingSuite] = React.useState(null); const [currentSuite, setCurrentSuite] = React.useState>>({ api_id: undefined, name: '', category: '', unitIds: [] }); const [suiteFormVisible, setSuiteFormVisible] = React.useState(false); // Pricing state const [selectedBrandForPricing, setSelectedBrandForPricing] = React.useState(''); const [pricingData, setPricingData] = React.useState(null); const [isPricingLoading, setIsPricingLoading] = React.useState(false); // Extras state const [extras, setExtras] = React.useState([]); const [editingExtra, setEditingExtra] = React.useState(null); const [extraFormVisible, setExtraFormVisible] = React.useState(false); const [currentExtra, setCurrentExtra] = React.useState>({ title: '', price: 0, description: '', image: '', category: '', tag: '', active: true, order: 0 }); // Check for active session on mount React.useEffect(() => { const checkSession = async () => { const { data: { session } } = await supabase.auth.getSession(); if (session) { setIsAuthenticated(true); } setIsLoading(false); // Stop loading after checking session }; checkSession(); }, []); const loadData = React.useCallback(async () => { setIsLoading(true); try { const [brandsData, unitsData, suitesData] = await Promise.all([ brandService.getAllBrands(), hotelUnitService.getAllUnits(), suiteService.getAllSuites() ]); setBrands(brandsData); setUnits(unitsData); setSuites(suitesData); setExtras(extraService.getExtras().sort((a, b) => a.order - b.order)); // Load extras from LocalStorage if (brandsData.length > 0 && !selectedBrandForPricing) { setSelectedBrandForPricing(String(brandsData[0].id)); } } catch (err) { setError(err instanceof Error ? err.message : 'Falha ao carregar dados.'); } finally { setIsLoading(false); } }, [selectedBrandForPricing]); React.useEffect(() => { if (isAuthenticated) { loadData(); } }, [isAuthenticated, loadData]); // Load pricing data when selected brand changes React.useEffect(() => { if (selectedBrandForPricing && activeTab === 'prices' && isAuthenticated) { const brandId = parseInt(selectedBrandForPricing, 10); const fetchPricing = async () => { setIsPricingLoading(true); setPricingData(null); try { const data = await pricingService.getPricingData(brandId); setPricingData(data); } catch (err) { setError(err instanceof Error ? err.message : 'Falha ao carregar preços.'); } finally { setIsPricingLoading(false); } }; fetchPricing(); } }, [selectedBrandForPricing, activeTab, isAuthenticated]); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(''); try { const { error } = await supabase.auth.signInWithPassword({ email: email, password: password, }); if (error) throw error; setIsAuthenticated(true); setEmail(''); setPassword(''); } catch (err: any) { setError(err.error_description || err.message || 'E-mail ou senha inválidos.'); } finally { setIsLoading(false); } }; const handleLogout = async () => { await supabase.auth.signOut(); setIsAuthenticated(false); }; const showSaveStatus = (message: string, type: 'success' | 'error') => { setSaveStatus({ message, type }); setTimeout(() => setSaveStatus(null), 3000); }; const handleSettingsSubmit = (e: React.FormEvent) => { e.preventDefault(); onSaveConfig(localSettings); showSaveStatus('Configurações salvas localmente!', 'success'); }; // --- Brand Handlers --- const handleBrandFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!currentBrand.name) { setError('O nome da marca é obrigatório.'); return; } setIsSaving(true); setError(''); try { const payload = { name: currentBrand.name, suite_categories: Array.isArray(currentBrand.suite_categories) ? currentBrand.suite_categories.filter(c => c) : [], stay_durations: Array.isArray(currentBrand.stay_durations) ? currentBrand.stay_durations.filter(d => d) : [], }; if (editingBrand) { await brandService.updateBrand({ ...editingBrand, ...payload }); showSaveStatus('Marca atualizada com sucesso!', 'success'); } else { await brandService.addBrand(payload); showSaveStatus('Marca adicionada com sucesso!', 'success'); } setBrandFormVisible(false); setEditingBrand(null); await loadData(); } catch(err) { setError(err instanceof Error ? err.message : 'Erro ao salvar marca.'); } finally { setIsSaving(false); } }; const handleEditBrand = (brand: Brand) => { setEditingBrand(brand); setCurrentBrand({ name: brand.name, suite_categories: brand.suite_categories, stay_durations: brand.stay_durations, }); setBrandFormVisible(true); setError(''); }; const handleDeleteBrand = async (brandId: number) => { if (window.confirm('Tem certeza? Remover uma marca também removerá suas unidades e preços.')) { try { await brandService.deleteBrand(brandId); showSaveStatus('Marca removida com sucesso.', 'success'); await loadData(); } catch (err) { setError(err instanceof Error ? err.message : 'Erro ao remover marca.'); } } }; // --- Unit Handlers --- const handleCategoryImageURLChange = (category: string, url: string, index: number) => { setCurrentUnit(prev => { const updatedImages = [...(prev.suite_category_images || [])]; let categoryImages = updatedImages.find(ci => ci.category === category); if (!categoryImages) { categoryImages = { category: category, imageUrls: [] }; updatedImages.push(categoryImages); } const newImageUrls = [...categoryImages.imageUrls]; newImageUrls[index] = url; categoryImages.imageUrls = newImageUrls; return { ...prev, suite_category_images: updatedImages }; }); }; const handleUnitFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!currentUnit.name || !currentUnit.brandId) { setError('Nome da unidade e seleção de marca são obrigatórios.'); return; } setIsSaving(true); setError(''); try { const payload = { name: currentUnit.name, brandId: currentUnit.brandId, visible_suite_categories: currentUnit.visible_suite_categories || [], suite_category_images: currentUnit.suite_category_images || [] }; if (editingUnit) { await hotelUnitService.updateUnit({ ...editingUnit, ...payload }); showSaveStatus('Unidade atualizada!', 'success'); } else { await hotelUnitService.addUnit(payload); showSaveStatus('Unidade adicionada!', 'success'); } setUnitFormVisible(false); setEditingUnit(null); await loadData(); } catch(err) { setError(err instanceof Error ? err.message : 'Erro ao salvar unidade.'); } finally { setIsSaving(false); } }; const handleEditUnit = (unit: HotelUnit) => { setEditingUnit(unit); setCurrentUnit({ name: unit.name, brandId: unit.brandId, visible_suite_categories: unit.visible_suite_categories || [], suite_category_images: unit.suite_category_images || [] }); setUnitFormVisible(true); setError(''); }; const handleDeleteUnit = async (unitId: number) => { if (window.confirm('Tem certeza?')) { try { await hotelUnitService.deleteUnit(unitId); showSaveStatus('Unidade removida.', 'success'); await loadData(); } catch (err) { setError(err instanceof Error ? err.message : 'Erro ao remover unidade.'); } } }; const handleCategoryVisibilityChange = (category: string, isChecked: boolean) => { setCurrentUnit(prev => { const currentCategories = prev.visible_suite_categories || []; if (isChecked) { return { ...prev, visible_suite_categories: [...currentCategories, category] }; } else { return { ...prev, visible_suite_categories: currentCategories.filter(c => c !== category) }; } }); }; // --- Suite Handlers --- const handleSuiteUnitAssignmentChange = (unitId: number, isChecked: boolean) => { setCurrentSuite(prev => { const currentUnitIds = prev.unitIds || []; if (isChecked) { return { ...prev, unitIds: [...currentUnitIds, unitId] }; } else { return { ...prev, unitIds: currentUnitIds.filter(id => id !== unitId) }; } }); }; const handleSuiteFormSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!currentSuite.api_id || !currentSuite.name || !currentSuite.category) { setError('Todos os campos da suíte são obrigatórios.'); return; } setIsSaving(true); setError(''); try { const payload = { api_id: currentSuite.api_id, name: currentSuite.name, category: currentSuite.category, unitIds: currentSuite.unitIds || [] }; if (editingSuite) { await suiteService.updateSuite({ ...editingSuite, ...payload }); showSaveStatus('Suíte atualizada!', 'success'); } else { await suiteService.addSuite(payload); showSaveStatus('Suíte adicionada!', 'success'); } setSuiteFormVisible(false); setEditingSuite(null); await loadData(); } catch(err) { setError(err instanceof Error ? err.message : 'Erro ao salvar suíte.'); } finally { setIsSaving(false); } }; const handleEditSuite = (suite: Suite) => { setEditingSuite(suite); setCurrentSuite({ api_id: suite.api_id, name: suite.name, category: suite.category, unitIds: suite.unitIds || [] }); setSuiteFormVisible(true); setError(''); }; const handleDeleteSuite = async (suiteId: number) => { if (window.confirm('Tem certeza?')) { try { await suiteService.deleteSuite(suiteId); showSaveStatus('Suíte removida.', 'success'); await loadData(); } catch(err) { setError(err instanceof Error ? err.message : 'Erro ao remover suíte.'); } } }; // --- Pricing Handlers --- const handlePriceChange = (dayRange: string, category: string, duration: string, value: string) => { setPricingData(prevData => { if (!prevData) return null; const newData = JSON.parse(JSON.stringify(prevData)); const price = parseFloat(value); newData[dayRange][category][duration] = isNaN(price) ? 0 : price; return newData; }); }; const handleSavePrices = async () => { if (pricingData && selectedBrandForPricing) { setIsSaving(true); try { await pricingService.savePricingData(parseInt(selectedBrandForPricing, 10), pricingData); showSaveStatus('Preços salvos com sucesso!', 'success'); } catch (err) { setError(err instanceof Error ? err.message : 'Erro ao salvar preços.'); } finally { setIsSaving(false); } } }; const renderPricingContent = () => { const selectedBrand = selectedBrandForPricing ? brands.find(b => b.id === parseInt(selectedBrandForPricing, 10)) : null; if (!selectedBrand) { return

Por favor, selecione uma marca para gerenciar os preços.

; } if (isPricingLoading) { return

Carregando tabela de preços...

} if (!pricingData) { return

Não foi possível carregar os dados de preços. Tente selecionar a marca novamente.

; } if (!selectedBrand.suite_categories || selectedBrand.suite_categories.length === 0 || !selectedBrand.stay_durations || selectedBrand.stay_durations.length === 0) { return

Para definir os preços, primeiro adicione 'Categorias de Suíte' e 'Tipos de Permanência' na aba de 'Marcas'.

; } return (
{Object.entries(pricingData).map(([dayRange, categories]) => (

{dayRange}

{Object.entries(categories).map(([category, durations]) => (

{category}

{Object.entries(durations).map(([duration, price]) => (
handlePriceChange(dayRange, category, duration, e.target.value)} label="" inputPrefix="R$" />
))}
))}
))}
); }; // --- Extras Handlers --- const handleExtraFormSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!currentExtra.title || currentExtra.price === undefined) { setError('Título e preço são obrigatórios.'); return; } try { const payload: any = { ...currentExtra }; if (editingExtra) { extraService.updateExtra({ ...editingExtra, ...payload }); showSaveStatus('Item extra atualizado!', 'success'); } else { extraService.addExtra(payload); showSaveStatus('Item extra adicionado!', 'success'); } setExtraFormVisible(false); setEditingExtra(null); setExtras(extraService.getExtras().sort((a, b) => a.order - b.order)); } catch (err) { setError(err instanceof Error ? err.message : 'Erro ao salvar extra.'); } }; const handleEditExtra = (extra: ExtraItem) => { setEditingExtra(extra); setCurrentExtra({ ...extra }); setExtraFormVisible(true); setError(''); }; const handleDeleteExtra = (id: string) => { if (window.confirm('Tem certeza que deseja excluir este item?')) { extraService.deleteExtra(id); setExtras(extraService.getExtras().sort((a, b) => a.order - b.order)); showSaveStatus('Item removido.', 'success'); } }; const handleToggleExtraStatus = (id: string) => { extraService.toggleStatus(id); setExtras(extraService.getExtras().sort((a, b) => a.order - b.order)); }; // --- Login/Logout --- if (isLoading) { return (

Verificando sessão...

); } if (!isAuthenticated) { return (

Login Administrativo

setEmail(e.target.value)} required autoComplete="email" /> setPassword(e.target.value)} required autoComplete="current-password" /> {error &&

{error}

}
); } const brandOptions = brands.map(b => ({ value: String(b.id), label: b.name })); // --- RENDER --- return (

Painel Administrativo

Marcas Unidades Suítes Preços Extras & Pacotes Configurações
{error &&
{error}
} {saveStatus &&
{saveStatus.message}
} {isLoading ?

Carregando...

: <> {/* Brands Tab */}

Gerenciar Marcas

{!brandFormVisible && } {brandFormVisible && (

{editingBrand ? 'Editar Marca' : 'Nova Marca'}

setCurrentBrand(p => ({...p, name: e.target.value}))} required /> setCurrentBrand(p => ({...p, suite_categories: e.target.value.split(',').map(s => s.trim())}))} /> setCurrentBrand(p => ({...p, stay_durations: e.target.value.split(',').map(s => s.trim())}))} />
)} ({ id: b.id, cells: [ b.name, b.suite_categories.join(', '), b.stay_durations.join(', '), handleEditBrand(b)} onDelete={() => handleDeleteBrand(b.id)} /> ] }))} />
{/* Units Tab */}

Gerenciar Unidades

{!unitFormVisible && } {brands.length === 0 &&

Crie uma marca antes de adicionar unidades.

} {unitFormVisible && (

{editingUnit ? 'Editar Unidade' : 'Nova Unidade'}

setCurrentUnit(p => ({...p, brandId: parseInt(e.target.value, 10), visible_suite_categories: [] }))} options={brandOptions} required /> setCurrentUnit(p => ({...p, name: e.target.value}))} required /> {currentUnit.brandId && (

Categorias e Imagens

Selecione as categorias visíveis e cole as URLs públicas das imagens (do Backblaze B2).

{(brands.find(b => b.id === currentUnit.brandId)?.suite_categories || []).map(category => { const imageInfo = currentUnit.suite_category_images?.find(ci => ci.category === category); return (
handleCategoryVisibilityChange(category, e.target.checked)} className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
handleCategoryImageURLChange(category, e.target.value, 0)} placeholder="https://.../imagem1.jpg" /> handleCategoryImageURLChange(category, e.target.value, 1)} placeholder="https://.../imagem2.jpg" />
)})}
)}
)} ({ id: u.id, cells: [ u.name, brands.find(b=>b.id === u.brandId)?.name || 'N/A', (u.visible_suite_categories || []).join(', '), handleEditUnit(u)} onDelete={() => handleDeleteUnit(u.id)} /> ] }))} />
{/* Suites Tab */}

Gerenciar Suítes

{!suiteFormVisible && } {suiteFormVisible && (

{editingSuite ? 'Editar Suíte' : 'Nova Suíte'}

setCurrentSuite(p => ({...p, api_id: parseInt(e.target.value, 10)}))} required /> setCurrentSuite(p => ({...p, name: e.target.value}))} required /> setCurrentSuite(p => ({...p, category: e.target.value}))} required instruction="A categoria deve corresponder a uma das categorias cadastradas na marca." />
{units.length > 0 ? units.map(unit => (
handleSuiteUnitAssignmentChange(unit.id, e.target.checked)} className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
)) :

Nenhuma unidade cadastrada. Crie unidades na aba 'Unidades'.

}

Marque as unidades onde esta suíte estará disponível.

)} ({ id: s.id, cells: [ s.api_id, s.name, s.category, s.unitIds?.map(unitId => units.find(u => u.id === unitId)?.name).filter(Boolean).join(', ') || 'Nenhuma', handleEditSuite(s)} onDelete={() => handleDeleteSuite(s.id)} /> ] }))} />
{/* Pricing Tab */}

Tabela de Preços

setSelectedBrandForPricing(e.target.value)} options={brandOptions} /> {renderPricingContent()}
{/* Extras Tab */}

Catálogo de Extras & Pacotes

{!extraFormVisible && } {extraFormVisible && (

{editingExtra ? 'Editar Extra' : 'Novo Extra'}

setCurrentExtra(p => ({...p, title: e.target.value}))} required /> setCurrentExtra(p => ({...p, price: parseFloat(e.target.value)}))} required />
setCurrentExtra(p => ({...p, description: e.target.value}))} fieldType="textarea" rows={2} /> setCurrentExtra(p => ({...p, image: e.target.value}))} placeholder="https://..." />
setCurrentExtra(p => ({...p, category: e.target.value}))} /> setCurrentExtra(p => ({...p, tag: e.target.value}))} /> setCurrentExtra(p => ({...p, order: parseInt(e.target.value)}))} />
setCurrentExtra(p => ({...p, active: e.target.checked}))} className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
)} ({ id: e.id, cells: [ e.image ? {e.title} : 'Sem img', e.title, e.category || '-', `R$ ${e.price.toFixed(2)}`, e.order || 0, {e.active ? 'Ativo' : 'Inativo'},
] }))} />
{/* Settings Tab */}

Configurações Gerais

Nota: Essas alterações são salvas localmente neste navegador. Para uma aplicação em produção, estes dados deveriam ser salvos no banco de dados.

setLocalSettings(prev => ({ ...prev, title: e.target.value }))} placeholder="Ex: Reserva Premium" required /> setLocalSettings(prev => ({ ...prev, subtitle: e.target.value }))} placeholder="Ex: Hotel 1001 Noites Prime" required />
}
); }; const TabButton: React.FC<{ tabId: AdminTab; activeTab: AdminTab; children: React.ReactNode; onClick: (tabId: AdminTab) => void; }> = ({ tabId, activeTab, children, onClick }) => ( ); interface DataRow { id: string | number; cells: (string | number | React.ReactNode)[]; } const DataTable: React.FC<{ headers: string[]; data: DataRow[]}> = ({ headers, data }) => (
{headers.map(h => )} {data.length === 0 ? : data.map(row => {row.cells.map((cell, j) => )})}
{h}
Nenhum dado encontrado.
{cell}
); const ActionButtons: React.FC<{ onEdit: () => void; onDelete: () => void }> = ({ onEdit, onDelete }) => (
); export default AdminPage;