reserva_chatmotel/_poc-reference/hotel-1001-noites-prime---reserva/components/AdminPage.tsx

882 lines
45 KiB
TypeScript
Executable File

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';
import { motion, AnimatePresence } from 'motion/react';
import {
Plus,
Edit2,
Trash2,
Save,
X,
LogOut,
LayoutDashboard,
Building2,
Hotel,
BedDouble,
DollarSign,
Sparkles,
Settings,
Loader2,
ChevronLeft,
CheckCircle2,
AlertTriangle
} from 'lucide-react';
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<AdminPageProps> = ({ onNavigateToReservation, currentConfig, onSaveConfig }) => {
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(false);
const [email, setEmail] = React.useState<string>('');
const [password, setPassword] = React.useState<string>('');
const [error, setError] = React.useState<string>('');
const [activeTab, setActiveTab] = React.useState<AdminTab>('brands');
const [isLoading, setIsLoading] = React.useState<boolean>(true); // Start as true to check session
const [isSaving, setIsSaving] = React.useState<boolean>(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<Brand[]>([]);
const [editingBrand, setEditingBrand] = React.useState<Brand | null>(null);
const [currentBrand, setCurrentBrand] = React.useState<Partial<Omit<Brand, 'id'>>>({ name: '', suite_categories: [], stay_durations: [] });
const [brandFormVisible, setBrandFormVisible] = React.useState(false);
// Unit state
const [units, setUnits] = React.useState<HotelUnit[]>([]);
const [editingUnit, setEditingUnit] = React.useState<HotelUnit | null>(null);
const [currentUnit, setCurrentUnit] = React.useState<Partial<Omit<HotelUnit, 'id'>> & { 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<Suite[]>([]);
const [editingSuite, setEditingSuite] = React.useState<Suite | null>(null);
const [currentSuite, setCurrentSuite] = React.useState<Partial<Omit<Suite, 'id'>>>({ api_id: undefined, name: '', category: '', unitIds: [] });
const [suiteFormVisible, setSuiteFormVisible] = React.useState(false);
// Pricing state
const [selectedBrandForPricing, setSelectedBrandForPricing] = React.useState<string>('');
const [pricingData, setPricingData] = React.useState<PricingData | null>(null);
const [isPricingLoading, setIsPricingLoading] = React.useState<boolean>(false);
// Extras state
const [extras, setExtras] = React.useState<ExtraItem[]>([]);
const [editingExtra, setEditingExtra] = React.useState<ExtraItem | null>(null);
const [extraFormVisible, setExtraFormVisible] = React.useState(false);
const [currentExtra, setCurrentExtra] = React.useState<Partial<ExtraItem>>({
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 <p className="mt-4 text-slate-500">Por favor, selecione uma marca para gerenciar os preços.</p>;
}
if (isPricingLoading) {
return <p className="mt-4 text-slate-500">Carregando tabela de preços...</p>
}
if (!pricingData) {
return <p className="mt-4 text-red-500">Não foi possível carregar os dados de preços. Tente selecionar a marca novamente.</p>;
}
if (!selectedBrand.suite_categories || selectedBrand.suite_categories.length === 0 || !selectedBrand.stay_durations || selectedBrand.stay_durations.length === 0) {
return <p className="mt-4 text-slate-500">Para definir os preços, primeiro adicione 'Categorias de Suíte' e 'Tipos de Permanência' na aba de 'Marcas'.</p>;
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-12 mt-6">
{Object.entries(pricingData).map(([dayRange, categories]) => (
<div key={dayRange} className="space-y-6">
<h3 className="text-lg font-bold text-sky-700 border-b pb-2">{dayRange}</h3>
{Object.entries(categories).map(([category, durations]) => (
<div key={category} className="p-4 bg-slate-50 rounded-lg">
<h4 className="font-semibold text-slate-700 mb-4">{category}</h4>
<div className="space-y-3">
{Object.entries(durations).map(([duration, price]) => (
<div key={duration} className="grid grid-cols-3 gap-4 items-center">
<label htmlFor={`${dayRange}-${category}-${duration}`} className="text-sm text-slate-600 col-span-2">{duration}</label>
<FormField id={`${dayRange}-${category}-${duration}`} name="price" type="number" value={String(price)} onChange={(e) => handlePriceChange(dayRange, category, duration, e.target.value)} label="" inputPrefix="R$" />
</div>
))}
</div>
</div>
))}
</div>
))}
</div>
);
};
// --- 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 (
<div className="min-h-screen bg-slate-800 flex items-center justify-center">
<p className="text-white">Verificando sessão...</p>
</div>
);
}
if (!isAuthenticated) {
return (
<div className="min-h-screen bg-slate-800 py-8 px-4 flex flex-col items-center justify-center">
<div className="bg-white p-8 md:p-10 rounded-xl shadow-2xl w-full max-w-md">
<h1 className="text-2xl font-bold text-center text-sky-700 mb-6">Login Administrativo</h1>
<form onSubmit={handleLogin}>
<FormField id="email" name="email" type="email" label="E-mail" value={email} onChange={(e) => setEmail(e.target.value)} required autoComplete="email" />
<FormField id="password" name="password" type="password" label="Senha" value={password} onChange={(e) => setPassword(e.target.value)} required autoComplete="current-password" />
{error && <p className="text-red-500 text-sm mt-2 mb-4">{error}</p>}
<Button type="submit" className="w-full mt-4" isLoading={isSaving}>Entrar</Button>
</form>
<Button onClick={onNavigateToReservation} variant="secondary" className="w-full mt-4">Voltar para Reservas</Button>
</div>
</div>
);
}
const brandOptions = brands.map(b => ({ value: String(b.id), label: b.name }));
// --- RENDER ---
return (
<div className="min-h-screen bg-slate-100 py-8 px-4">
<div className="container mx-auto max-w-6xl">
<div className="flex flex-col sm:flex-row justify-between sm:items-center mb-10 gap-6 border-b border-[#1B3B5F]/10 pb-8">
<div className="flex items-center gap-4">
<div className="p-3 bg-[#1E90FF]/10 rounded-2xl">
<LayoutDashboard className="w-8 h-8 text-[#1E90FF]" />
</div>
<div>
<h1 className="text-3xl font-extrabold text-[#1B3B5F] tracking-tight">Painel Administrativo</h1>
<p className="text-[#9CA3AF] text-sm font-medium">Gerencie marcas, unidades e reservas</p>
</div>
</div>
<div className="flex items-center gap-3">
<Button onClick={handleLogout} variant="secondary" size="sm" className="flex items-center gap-2 text-red-500 hover:bg-red-50 rounded-xl px-4">
<LogOut className="w-4 h-4" />
Sair
</Button>
<Button onClick={onNavigateToReservation} variant="secondary" size="sm" className="flex items-center gap-2 rounded-xl px-4">
<ChevronLeft className="w-4 h-4" />
Voltar ao Site
</Button>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-8">
<TabButton tabId="brands" activeTab={activeTab} onClick={setActiveTab} icon={<Building2 className="w-4 h-4" />}>Marcas</TabButton>
<TabButton tabId="units" activeTab={activeTab} onClick={setActiveTab} icon={<Hotel className="w-4 h-4" />}>Unidades</TabButton>
<TabButton tabId="suites" activeTab={activeTab} onClick={setActiveTab} icon={<BedDouble className="w-4 h-4" />}>Suítes</TabButton>
<TabButton tabId="prices" activeTab={activeTab} onClick={setActiveTab} icon={<DollarSign className="w-4 h-4" />}>Preços</TabButton>
<TabButton tabId="extras" activeTab={activeTab} onClick={setActiveTab} icon={<Sparkles className="w-4 h-4" />}>Extras & Pacotes</TabButton>
<TabButton tabId="settings" activeTab={activeTab} onClick={setActiveTab} icon={<Settings className="w-4 h-4" />}>Configurações</TabButton>
</div>
{error && <div className="p-3 mb-4 rounded-md text-sm bg-red-100 text-red-800">{error}</div>}
{saveStatus && <div className={`p-3 mb-4 rounded-md text-sm ${saveStatus.type === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>{saveStatus.message}</div>}
{isLoading ? (
<div className="text-center py-20 flex flex-col items-center justify-center space-y-4">
<Loader2 className="w-8 h-8 text-[#1E90FF] animate-spin" />
<p className="text-[#9CA3AF] font-medium animate-pulse">Carregando dados do sistema...</p>
</div>
) : (
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
{/* Brands Tab */}
<div className={activeTab === 'brands' ? 'block' : 'hidden'}>
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-xl font-semibold text-slate-800 mb-4">Gerenciar Marcas</h2>
{!brandFormVisible && <Button onClick={() => { setCurrentBrand({ name: '', suite_categories: [], stay_durations: []}); setEditingBrand(null); setBrandFormVisible(true); }} className="mb-6">Adicionar Nova Marca</Button>}
{brandFormVisible && (
<form onSubmit={handleBrandFormSubmit} className="bg-slate-50 p-4 rounded-lg mb-6 border">
<h3 className="text-lg font-semibold mb-4">{editingBrand ? 'Editar Marca' : 'Nova Marca'}</h3>
<FormField label="Nome da Marca" id="brandName" name="name" value={currentBrand.name || ''} onChange={(e) => setCurrentBrand(p => ({...p, name: e.target.value}))} required />
<FormField label="Categorias de Suíte (separadas por vírgula)" id="brandCategories" name="suite_categories" value={currentBrand.suite_categories?.join(', ') || ''} onChange={(e) => setCurrentBrand(p => ({...p, suite_categories: e.target.value.split(',').map(s => s.trim())}))} />
<FormField label="Tipos de Permanência (separadas por vírgula)" id="brandDurations" name="stay_durations" value={currentBrand.stay_durations?.join(', ') || ''} onChange={(e) => setCurrentBrand(p => ({...p, stay_durations: e.target.value.split(',').map(s => s.trim())}))} />
<div className="flex gap-4 mt-6">
<Button type="submit" isLoading={isSaving}>{editingBrand ? 'Salvar' : 'Adicionar'}</Button>
<Button type="button" variant="secondary" onClick={() => setBrandFormVisible(false)}>Cancelar</Button>
</div>
</form>
)}
<DataTable
headers={['Nome', 'Categorias', 'Permanências', 'Ações']}
data={brands.map(b => ({
id: b.id,
cells: [
b.name,
b.suite_categories.join(', '),
b.stay_durations.join(', '),
<ActionButtons onEdit={() => handleEditBrand(b)} onDelete={() => handleDeleteBrand(b.id)} />
]
}))}
/>
</div>
</div>
{/* Units Tab */}
<div className={activeTab === 'units' ? 'block' : 'hidden'}>
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-xl font-semibold text-slate-800 mb-4">Gerenciar Unidades</h2>
{!unitFormVisible && <Button onClick={() => { setCurrentUnit({name: '', brandId: undefined, visible_suite_categories: [], suite_category_images: []}); setEditingUnit(null); setUnitFormVisible(true); }} className="mb-6" disabled={brands.length === 0}>Adicionar Nova Unidade</Button>}
{brands.length === 0 && <p className="text-sm text-slate-500">Crie uma marca antes de adicionar unidades.</p>}
{unitFormVisible && (
<form onSubmit={handleUnitFormSubmit} className="bg-slate-50 p-4 rounded-lg mb-6 border">
<h3 className="text-lg font-semibold mb-4">{editingUnit ? 'Editar Unidade' : 'Nova Unidade'}</h3>
<SelectField label="Marca" id="unitBrand" name="brandId" value={String(currentUnit.brandId || '')} onChange={(e) => setCurrentUnit(p => ({...p, brandId: parseInt(e.target.value, 10), visible_suite_categories: [] }))} options={brandOptions} required />
<FormField label="Nome da Unidade" id="unitName" name="name" value={currentUnit.name || ''} onChange={(e) => setCurrentUnit(p => ({...p, name: e.target.value}))} required />
{currentUnit.brandId && (
<div className="mt-4 pt-4 border-t border-slate-200">
<h4 className="block text-sm font-medium text-slate-700 mb-2">Categorias e Imagens</h4>
<p className="mb-4 text-xs text-slate-500">Selecione as categorias visíveis e cole as URLs públicas das imagens (do Backblaze B2).</p>
<div className="space-y-4">
{(brands.find(b => b.id === currentUnit.brandId)?.suite_categories || []).map(category => {
const imageInfo = currentUnit.suite_category_images?.find(ci => ci.category === category);
return (
<div key={category} className="p-3 bg-white rounded-md border">
<div className="flex items-center mb-3">
<input type="checkbox" id={`cat-${category}`} checked={currentUnit.visible_suite_categories?.includes(category) ?? false} onChange={e => handleCategoryVisibilityChange(category, e.target.checked)} className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
<label htmlFor={`cat-${category}`} className="ml-3 block text-sm font-semibold text-gray-800">{category}</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-7">
<FormField label="URL da Imagem 1" id={`img1-${category}`} name={`img1-${category}`} value={imageInfo?.imageUrls?.[0] || ''} onChange={(e) => handleCategoryImageURLChange(category, e.target.value, 0)} placeholder="https://.../imagem1.jpg" />
<FormField label="URL da Imagem 2" id={`img2-${category}`} name={`img2-${category}`} value={imageInfo?.imageUrls?.[1] || ''} onChange={(e) => handleCategoryImageURLChange(category, e.target.value, 1)} placeholder="https://.../imagem2.jpg" />
</div>
</div>
)})}
</div>
</div>
)}
<div className="flex gap-4 mt-6">
<Button type="submit" isLoading={isSaving}>{editingUnit ? 'Salvar' : 'Adicionar'}</Button>
<Button type="button" variant="secondary" onClick={() => setUnitFormVisible(false)}>Cancelar</Button>
</div>
</form>
)}
<DataTable
headers={['Nome da Unidade', 'Marca', 'Categorias Visíveis', 'Ações']}
data={units.map(u => ({
id: u.id,
cells: [
u.name,
brands.find(b=>b.id === u.brandId)?.name || 'N/A',
(u.visible_suite_categories || []).join(', '),
<ActionButtons onEdit={() => handleEditUnit(u)} onDelete={() => handleDeleteUnit(u.id)} />
]
}))}
/>
</div>
</div>
{/* Suites Tab */}
<div className={activeTab === 'suites' ? 'block' : 'hidden'}>
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-xl font-semibold text-slate-800 mb-4">Gerenciar Suítes</h2>
{!suiteFormVisible && <Button onClick={() => { setCurrentSuite({api_id: undefined, name: '', category: '', unitIds: []}); setEditingSuite(null); setSuiteFormVisible(true); }} className="mb-6">Adicionar Nova Suíte</Button>}
{suiteFormVisible && (
<form onSubmit={handleSuiteFormSubmit} className="bg-slate-50 p-4 rounded-lg mb-6 border">
<h3 className="text-lg font-semibold mb-4">{editingSuite ? 'Editar Suíte' : 'Nova Suíte'}</h3>
<FormField label="ID da API" type="number" id="suiteApiId" name="api_id" value={String(currentSuite.api_id || '')} onChange={(e) => setCurrentSuite(p => ({...p, api_id: parseInt(e.target.value, 10)}))} required />
<FormField label="Nome" id="suiteName" name="name" value={currentSuite.name || ''} onChange={(e) => setCurrentSuite(p => ({...p, name: e.target.value}))} required />
<FormField label="Categoria" id="suiteCategory" name="category" value={currentSuite.category || ''} onChange={(e) => setCurrentSuite(p => ({...p, category: e.target.value}))} required instruction="A categoria deve corresponder a uma das categorias cadastradas na marca." />
<div className="mt-4 pt-4 border-t border-slate-200">
<label className="block text-sm font-medium text-slate-700 mb-2">Disponível nas Unidades</label>
<div className="space-y-2">
{units.length > 0 ? units.map(unit => (
<div key={unit.id} className="flex items-center">
<input
type="checkbox"
id={`suite-unit-${unit.id}`}
checked={currentSuite.unitIds?.includes(unit.id) ?? false}
onChange={e => handleSuiteUnitAssignmentChange(unit.id, e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500" />
<label htmlFor={`suite-unit-${unit.id}`} className="ml-3 block text-sm text-gray-700">{unit.name} ({brands.find(b => b.id === unit.brandId)?.name})</label>
</div>
)) : <p className="text-xs text-slate-500">Nenhuma unidade cadastrada. Crie unidades na aba 'Unidades'.</p>}
</div>
<p className="mt-1.5 text-xs text-slate-500">Marque as unidades onde esta suíte estará disponível.</p>
</div>
<div className="flex gap-4 mt-6">
<Button type="submit" isLoading={isSaving}>{editingSuite ? 'Salvar' : 'Adicionar'}</Button>
<Button type="button" variant="secondary" onClick={() => setSuiteFormVisible(false)}>Cancelar</Button>
</div>
</form>
)}
<DataTable
headers={['ID da API', 'Nome', 'Categoria', 'Unidades', 'Ações']}
data={suites.map(s => ({
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',
<ActionButtons onEdit={() => handleEditSuite(s)} onDelete={() => handleDeleteSuite(s.id)} />
]
}))}
/>
</div>
</div>
{/* Pricing Tab */}
<div className={activeTab === 'prices' ? 'block' : 'hidden'}>
<div className="bg-white p-6 rounded-lg shadow-lg">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-slate-800">Tabela de Preços</h2>
<Button onClick={handleSavePrices} isLoading={isSaving} disabled={!pricingData}>Salvar Preços</Button>
</div>
<SelectField label="Selecione a Marca para editar os preços" id="pricingBrand" name="brandId" value={selectedBrandForPricing} onChange={(e) => setSelectedBrandForPricing(e.target.value)} options={brandOptions} />
{renderPricingContent()}
</div>
</div>
{/* Extras Tab */}
<div className={activeTab === 'extras' ? 'block' : 'hidden'}>
<div className="bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-xl font-semibold text-slate-800 mb-4">Catálogo de Extras & Pacotes</h2>
{!extraFormVisible && <Button onClick={() => { setCurrentExtra({title: '', price: 0, description: '', image: '', category: '', tag: '', active: true, order: 0}); setEditingExtra(null); setExtraFormVisible(true); }} className="mb-6">Adicionar Novo Extra</Button>}
{extraFormVisible && (
<form onSubmit={handleExtraFormSubmit} className="bg-slate-50 p-4 rounded-lg mb-6 border space-y-4">
<h3 className="text-lg font-semibold mb-4">{editingExtra ? 'Editar Extra' : 'Novo Extra'}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField label="Título" id="extraTitle" name="title" value={currentExtra.title || ''} onChange={(e) => setCurrentExtra(p => ({...p, title: e.target.value}))} required />
<FormField label="Preço (R$)" id="extraPrice" name="price" type="number" value={String(currentExtra.price)} onChange={(e) => setCurrentExtra(p => ({...p, price: parseFloat(e.target.value)}))} required />
</div>
<FormField label="Descrição" id="extraDescription" name="description" value={currentExtra.description || ''} onChange={(e) => setCurrentExtra(p => ({...p, description: e.target.value}))} fieldType="textarea" rows={2} />
<FormField label="URL da Imagem" id="extraImage" name="image" value={currentExtra.image || ''} onChange={(e) => setCurrentExtra(p => ({...p, image: e.target.value}))} placeholder="https://..." />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField label="Categoria (Ex: Bebidas, Romântico)" id="extraCategory" name="category" value={currentExtra.category || ''} onChange={(e) => setCurrentExtra(p => ({...p, category: e.target.value}))} />
<FormField label="Tag (Ex: Mais pedido)" id="extraTag" name="tag" value={currentExtra.tag || ''} onChange={(e) => setCurrentExtra(p => ({...p, tag: e.target.value}))} />
<FormField label="Ordem de Exibição" id="extraOrder" name="order" type="number" value={String(currentExtra.order || 0)} onChange={(e) => setCurrentExtra(p => ({...p, order: parseInt(e.target.value)}))} />
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="extraActive"
checked={currentExtra.active ?? true}
onChange={(e) => setCurrentExtra(p => ({...p, active: e.target.checked}))}
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500"
/>
<label htmlFor="extraActive" className="text-sm font-medium text-gray-700">Item Ativo no Formulário</label>
</div>
<div className="flex gap-4 mt-6">
<Button type="submit">{editingExtra ? 'Salvar' : 'Adicionar'}</Button>
<Button type="button" variant="secondary" onClick={() => setExtraFormVisible(false)}>Cancelar</Button>
</div>
</form>
)}
<DataTable
headers={['Imagem', 'Título', 'Categoria', 'Preço', 'Ordem', 'Status', 'Ações']}
data={extras.map(e => ({
id: e.id,
cells: [
e.image ? <img src={e.image} alt={e.title} className="w-12 h-12 object-cover rounded-md" /> : 'Sem img',
e.title,
e.category || '-',
`R$ ${e.price.toFixed(2)}`,
e.order || 0,
<span className={`px-2 py-1 rounded text-xs ${e.active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>{e.active ? 'Ativo' : 'Inativo'}</span>,
<div className="space-x-2">
<Button onClick={() => handleToggleExtraStatus(e.id)} variant="secondary" className="!py-1 !px-2 text-xs">{e.active ? 'Desativar' : 'Ativar'}</Button>
<Button onClick={() => handleEditExtra(e)} variant="secondary" className="text-sky-600 hover:text-sky-900 !py-1 !px-2 text-xs">Editar</Button>
<Button onClick={() => handleDeleteExtra(e.id)} variant="secondary" className="text-red-600 hover:text-red-900 !py-1 !px-2 text-xs">Excluir</Button>
</div>
]
}))}
/>
</div>
</div>
{/* Settings Tab */}
<div className={activeTab === 'settings' ? 'block' : 'hidden'}>
<div className="bg-white p-6 rounded-lg shadow-lg max-w-2xl mx-auto">
<h2 className="text-xl font-semibold text-slate-800 mb-4">Configurações Gerais</h2>
<p className="text-sm text-slate-500 mb-6 bg-yellow-50 p-3 rounded-md border border-yellow-200">
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.
</p>
<form onSubmit={handleSettingsSubmit} className="space-y-4">
<FormField
label="Título Principal"
id="appTitle"
name="title"
value={localSettings.title}
onChange={(e) => setLocalSettings(prev => ({ ...prev, title: e.target.value }))}
placeholder="Ex: Reserva Premium"
required
/>
<FormField
label="Subtítulo / Nome do Hotel"
id="appSubtitle"
name="subtitle"
value={localSettings.subtitle}
onChange={(e) => setLocalSettings(prev => ({ ...prev, subtitle: e.target.value }))}
placeholder="Ex: Hotel 1001 Noites Prime"
required
/>
<Button type="submit">Salvar Alterações</Button>
</form>
</div>
</div>
</motion.div>
</AnimatePresence>
)}
</div>
</div>
);
};
const TabButton: React.FC<{
tabId: AdminTab;
activeTab: AdminTab;
children: React.ReactNode;
onClick: (tabId: AdminTab) => void;
icon?: React.ReactNode;
}> = ({ tabId, activeTab, children, onClick, icon }) => (
<button
onClick={() => onClick(tabId)}
className={cn(
'px-4 py-2.5 text-sm font-semibold rounded-xl transition-all duration-200 flex items-center gap-2',
activeTab === tabId
? 'bg-[#1E90FF] text-white shadow-lg shadow-[#1E90FF]/30 translate-y-[-1px]'
: 'bg-white text-[#1B3B5F] hover:bg-[#F8FAFC] border border-[#1B3B5F]/10'
)}
>
{icon}
{children}
</button>
);
interface DataRow {
id: string | number;
cells: (string | number | React.ReactNode)[];
}
const DataTable: React.FC<{ headers: string[]; data: DataRow[]}> = ({ headers, data }) => (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>{headers.map(h => <th key={h} className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">{h}</th>)}</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-200">
{data.length === 0 ? <tr><td colSpan={headers.length} className="px-6 py-4 text-center text-slate-500">Nenhum dado encontrado.</td></tr> :
data.map(row => <tr key={row.id}>{row.cells.map((cell, j) => <td key={j} className="px-6 py-4 whitespace-nowrap text-sm text-slate-600">{cell}</td>)}</tr>)}
</tbody>
</table>
</div>
);
const ActionButtons: React.FC<{ onEdit: () => void; onDelete: () => void }> = ({ onEdit, onDelete }) => (
<div className="flex items-center gap-2">
<Button onClick={onEdit} variant="secondary" size="sm" className="h-8 w-8 p-0 text-[#1E90FF] hover:bg-[#1E90FF]/10">
<Edit2 className="w-4 h-4" />
</Button>
<Button onClick={onDelete} variant="secondary" size="sm" className="h-8 w-8 p-0 text-red-500 hover:bg-red-50">
<Trash2 className="w-4 h-4" />
</Button>
</div>
);
export default AdminPage;