feat(settings): implementa aba sistema e vincula perfil à api

- adiciona aba 'Sistema' nas configurações para gestão de tabelas auxiliares (CRUD) e tabela de preços
- vincula formulário de perfil com dados do usuário logado (API /api/profissionais)
- oculta abas 'Notificações' e 'Aparência'
- corrige layout e bugs de estado na página de configurações
This commit is contained in:
NANDO9322 2026-01-31 12:48:42 -03:00
parent c71095b5f3
commit b497ea8c72
4 changed files with 626 additions and 288 deletions

View file

@ -0,0 +1,202 @@
import React, { useState, useEffect } from "react";
import { Save, Search, DollarSign } from "lucide-react";
import { Button } from "../Button";
import { useAuth } from "../../contexts/AuthContext";
import { toast } from "react-hot-toast";
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080";
interface Role {
id: string;
nome: string;
}
interface EventType {
id: string;
nome: string;
}
interface Price {
id?: string;
funcao_profissional_id: string;
tipo_evento_id: string;
valor: number;
}
export const PriceTableEditor: React.FC = () => {
const { token } = useAuth();
const [eventTypes, setEventTypes] = useState<EventType[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const [prices, setPrices] = useState<Record<string, number>>({}); // roleId -> value
const [selectedEventId, setSelectedEventId] = useState<string>("");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
// Initial Load
useEffect(() => {
const loadInitialData = async () => {
try {
const [eventsRes, rolesRes] = await Promise.all([
fetch(`${API_BASE_URL}/api/tipos-eventos`, { headers: { Authorization: `Bearer ${token}` } }),
fetch(`${API_BASE_URL}/api/funcoes`, { headers: { Authorization: `Bearer ${token}` } })
]);
const eventsData = await eventsRes.json();
const rolesData = await rolesRes.json();
// Adjust for data wrapper if exists
const events = (eventsData.data || eventsData) as EventType[];
const roles = (rolesData.data || rolesData) as Role[];
setEventTypes(events);
setRoles(roles);
if (events.length > 0) setSelectedEventId(events[0].id);
} catch (error) {
console.error(error);
toast.error("Erro ao carregar dados iniciais");
}
};
if (token) loadInitialData();
}, [token]);
// Load Prices when Event Selected
useEffect(() => {
if (!selectedEventId || !token) return;
const loadPrices = async () => {
setLoading(true);
try {
// Assuming we use type-events/:id/precos or similar
// Based on routes: GET /api/tipos-eventos/:id/precos
const res = await fetch(`${API_BASE_URL}/api/tipos-eventos/${selectedEventId}/precos`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error("Erro ao carregar preços");
const data = await res.json();
const priceList = (data.data || data) as Price[];
// Map to dictionary
const priceMap: Record<string, number> = {};
priceList.forEach(p => {
priceMap[p.funcao_profissional_id] = p.valor;
});
setPrices(priceMap);
} catch (error) {
toast.error("Erro ao carregar tabela de preços");
} finally {
setLoading(false);
}
};
loadPrices();
}, [selectedEventId, token]);
const handlePriceChange = (roleId: string, value: string) => {
const numValue = parseFloat(value) || 0;
setPrices(prev => ({ ...prev, [roleId]: numValue }));
};
const handleSave = async () => {
setSaving(true);
try {
// Need to save each price. Backend has POST /api/tipos-eventos/precos
// Body: { tipo_evento_id, funcao_profissional_id, valor }
// We can do parallel requests or backend bulk?
// Existing route seems singular or accepts array?
// "api.POST("/tipos-eventos/precos", tiposEventosHandler.SetPrice)"
// I'll assume singular for safety, or create a loop.
const promises = roles.map(role => {
const valor = prices[role.id];
// If undefined, maybe skip? But maybe we want to save 0?
if (valor === undefined) return Promise.resolve();
return fetch(`${API_BASE_URL}/api/tipos-eventos/precos`, {
method: "POST", // or PUT check handlers
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
tipo_evento_id: selectedEventId,
funcao_profissional_id: role.id,
valor: valor
})
});
});
await Promise.all(promises);
toast.success("Preços atualizados com sucesso!");
} catch (error) {
console.error(error);
toast.error("Erro ao salvar preços");
} finally {
setSaving(false);
}
};
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-6 flex items-center gap-2">
<DollarSign className="text-brand-gold" />
Tabela de Preços (Cachês Base)
</h2>
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">Selecione o Tipo de Evento</label>
<select
className="w-full sm:w-1/3 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#492E61] focus:outline-none"
value={selectedEventId}
onChange={(e) => setSelectedEventId(e.target.value)}
>
{eventTypes.map(ev => (
<option key={ev.id} value={ev.id}>{ev.nome}</option>
))}
</select>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Função</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor do Cachê (R$)</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{loading ? (
<tr><td colSpan={2} className="text-center py-4">Carregando...</td></tr>
) : roles.map(role => (
<tr key={role.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{role.nome}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div className="relative w-48">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">R$</span>
<input
type="number"
step="0.01"
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-purple focus:outline-none"
value={prices[role.id] ?? ""} // Use ?? "" to control input
onChange={(e) => handlePriceChange(role.id, e.target.value)}
placeholder="0,00"
/>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-6 flex justify-end">
<Button onClick={handleSave} disabled={saving}>
<Save size={18} className="mr-2" />
{saving ? "Salvando..." : "Salvar Tabela"}
</Button>
</div>
</div>
);
};

View file

@ -0,0 +1,217 @@
import React, { useState, useEffect } from "react";
import { Plus, Edit2, Trash2, X, Save, Search } from "lucide-react";
import { Button } from "../Button";
import { useAuth } from "../../contexts/AuthContext";
import { toast } from "react-hot-toast";
interface SimpleCrudProps {
title: string;
endpoint: string;
columns?: { key: string; label: string }[];
transformData?: (data: any) => any[];
}
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080";
export const SimpleCrud: React.FC<SimpleCrudProps> = ({
title,
endpoint,
columns = [{ key: "nome", label: "Nome" }],
transformData
}) => {
const { token } = useAuth();
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState<any | null>(null);
const [formData, setFormData] = useState<any>({});
const [searchTerm, setSearchTerm] = useState("");
const fetchItems = async () => {
try {
setLoading(true);
const res = await fetch(`${API_BASE_URL}${endpoint}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error("Falha ao carregar dados");
let data = await res.json();
// Handle different API response structures if needed
if (data.data && Array.isArray(data.data)) data = data.data; // Standard ApiResponse structure
if (transformData) data = transformData(data);
setItems(Array.isArray(data) ? data : []);
} catch (error) {
console.error(error);
toast.error("Erro ao carregar lista");
} finally {
setLoading(false);
}
};
useEffect(() => {
if (token) fetchItems();
}, [token, endpoint]);
const handleDelete = async (id: string) => {
if (!window.confirm("Tem certeza que deseja excluir?")) return;
try {
const res = await fetch(`${API_BASE_URL}${endpoint}/${id}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) throw new Error("Erro ao excluir");
toast.success("Item excluído com sucesso");
fetchItems();
} catch (error) {
toast.error("Erro ao excluir item");
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const url = editingItem
? `${API_BASE_URL}${endpoint}/${editingItem.id}`
: `${API_BASE_URL}${endpoint}`;
const method = editingItem ? "PUT" : "POST";
const res = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(formData)
});
if (!res.ok) throw new Error("Erro ao salvar");
toast.success(editingItem ? "Atualizado com sucesso" : "Criado com sucesso");
setIsModalOpen(false);
fetchItems();
} catch (error) {
console.error(error);
toast.error("Erro ao salvar dados");
}
};
const openModal = (item?: any) => {
setEditingItem(item || null);
setFormData(item || {});
setIsModalOpen(true);
};
const filteredItems = items.filter(item =>
columns.some(col =>
String(item[col.key] || "").toLowerCase().includes(searchTerm.toLowerCase())
)
);
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex flex-col sm:flex-row justify-between items-center mb-6 gap-4">
<h2 className="text-xl font-semibold text-gray-800">{title}</h2>
<div className="flex w-full sm:w-auto gap-2">
<div className="relative flex-1 sm:w-64">
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Buscar..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#492E61] focus:outline-none"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<Button onClick={() => openModal()}>
<Plus size={18} className="mr-2" />
Adicionar
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{columns.map(col => (
<th key={col.key} className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{col.label}
</th>
))}
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{loading ? (
<tr><td colSpan={columns.length + 1} className="text-center py-4">Carregando...</td></tr>
) : filteredItems.length === 0 ? (
<tr><td colSpan={columns.length + 1} className="text-center py-4 text-gray-500">Nenhum item encontrado.</td></tr>
) : (
filteredItems.map((item) => (
<tr key={item.id} className="hover:bg-gray-50">
{columns.map(col => (
<td key={col.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{item[col.key]}
</td>
))}
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onClick={() => openModal(item)} className="text-indigo-600 hover:text-indigo-900 mr-4">
<Edit2 size={18} />
</button>
<button onClick={() => handleDelete(item.id)} className="text-red-600 hover:text-red-900">
<Trash2 size={18} />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6 animate-in fade-in zoom-in-95 duration-200">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-bold text-gray-900">
{editingItem ? `Editar ${title}` : `Novo ${title}`}
</h3>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-gray-600">
<X size={24} />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{columns.map(col => (
<div key={col.key}>
<label className="block text-sm font-medium text-gray-700 mb-1">{col.label}</label>
<input
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#492E61] focus:outline-none"
value={formData[col.key] || ""}
onChange={e => setFormData({...formData, [col.key]: e.target.value})}
required
/>
</div>
))}
<div className="flex justify-end gap-3 pt-4">
<Button variant="secondary" onClick={() => setIsModalOpen(false)} type="button">
Cancelar
</Button>
<Button type="submit">
<Save size={18} className="mr-2" />
Salvar
</Button>
</div>
</form>
</div>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,77 @@
import React, { useState } from "react";
import { SimpleCrud } from "./SimpleCrud";
import { PriceTableEditor } from "./PriceTableEditor";
import { Building2, GraduationCap, Calendar, DollarSign, Database } from "lucide-react";
export const SystemSettings: React.FC = () => {
const [activeTab, setActiveTab] = useState<"empresas" | "cursos" | "tipos_evento" | "anos_formatura" | "precos">("empresas");
const tabs = [
{ id: "empresas", label: "Empresas", icon: Building2 },
{ id: "cursos", label: "Cursos", icon: GraduationCap },
{ id: "tipos_evento", label: "Tipos de Evento", icon: Calendar },
{ id: "anos_formatura", label: "Anos de Formatura", icon: Database },
{ id: "precos", label: "Tabela de Preços", icon: DollarSign },
];
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-2 overflow-x-auto">
<nav className="flex space-x-2">
{tabs.map(tab => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`
flex items-center px-4 py-2 rounded-md text-sm font-medium transition-colors whitespace-nowrap
${activeTab === tab.id
? "bg-brand-gold text-white"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
`}
>
<Icon size={18} className="mr-2" />
{tab.label}
</button>
)
})}
</nav>
</div>
<div className="animate-in fade-in duration-300 slide-in-from-bottom-2">
{activeTab === "empresas" && (
<SimpleCrud
title="Gerenciar Empresas"
endpoint="/api/empresas"
columns={[{ key: "nome", label: "Nome da Empresa" }]}
/>
)}
{activeTab === "cursos" && (
<SimpleCrud
title="Gerenciar Cursos"
endpoint="/api/cursos"
columns={[{ key: "nome", label: "Nome do Curso" }]}
/>
)}
{activeTab === "tipos_evento" && (
<SimpleCrud
title="Tipos de Evento"
endpoint="/api/tipos-eventos"
columns={[{ key: "nome", label: "Tipo de Evento" }]}
/>
)}
{activeTab === "anos_formatura" && (
<SimpleCrud
title="Anos de Formatura"
endpoint="/api/anos-formaturas"
columns={[{ key: "ano_semestre", label: "Ano/Semestre (Ex: 2024.1)" }]}
/>
)}
{activeTab === "precos" && (
<PriceTableEditor />
)}
</div>
</div>
);
};

View file

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { SystemSettings } from "../components/System/SystemSettings";
import {
User,
Mail,
@ -11,7 +12,9 @@ import {
Save,
Camera,
GraduationCap,
Database,
} from "lucide-react";
import toast from "react-hot-toast";
import { Button } from "../components/Button";
import { useAuth } from "../contexts/AuthContext";
import { UserRole } from "../types";
@ -22,17 +25,32 @@ export const SettingsPage: React.FC = () => {
user?.role === UserRole.SUPERADMIN ||
user?.role === UserRole.BUSINESS_OWNER;
const [activeTab, setActiveTab] = useState<
"profile" | "account" | "notifications" | "appearance" | "courses"
"profile" | "account" | "notifications" | "appearance" | "system"
>("profile");
const [profileData, setProfileData] = useState({
name: "João Silva",
email: "joao.silva@photum.com",
phone: "(41) 99999-0000",
location: "Curitiba, PR",
bio: "Fotógrafo profissional especializado em eventos e formaturas há mais de 10 anos.",
name: "",
email: "",
phone: "",
location: "",
bio: "",
avatar: "https://i.pravatar.cc/150?img=68",
});
// Effect to sync state with user data
React.useEffect(() => {
if (user) {
setProfileData({
name: user.name || "",
email: user.email || "",
phone: user.phone || "",
location: user.profissional?.cidade || "",
bio: user.profissional?.observacao || "",
avatar: user.profissional?.avatar_url || "https://i.pravatar.cc/150?img=68",
});
}
}, [user]);
const [notificationSettings, setNotificationSettings] = useState({
emailNotifications: true,
pushNotifications: true,
@ -49,16 +67,83 @@ export const SettingsPage: React.FC = () => {
currency: "BRL",
});
const handleSaveProfile = () => {
alert("Perfil atualizado com sucesso!");
const handleSaveProfile = async () => {
try {
if (!user?.profissional?.id) {
toast.error("Perfil profissional não encontrado para atualização.");
return;
}
const token = localStorage.getItem("token") || document.cookie.replace(/(?:(?:^|.*;\s*)access_token\s*\=\s*([^;]*).*$)|^.*$/, "$1");
// Prepare payload for Professional Update
// Note: The backend expects specific fields. We map what we have.
const payload = {
nome: profileData.name,
email: profileData.email, // Note: Prof service might not update email if it doesn't match User table sync logic, but we send it.
whatsapp: profileData.phone,
cidade: profileData.location,
observacao: profileData.bio,
avatar_url: profileData.avatar,
// Maintain other required fields if necessary, but PUT usually allows partial or defaults?
// Checking backend: It uses 'toPgText', so missing fields become NULL?
// We should fetch existing professional data first to merge?
// Or assume the backend handles partial updates?
// The backend implementation 'CreateProfissionalInput' struct in Update assumes REPLACING values.
// Be careful. ideally we should GET /me/profissional first.
// But we have 'user.profissional' which might be incomplete.
// Let's rely on what we have.
};
// Fetch current professional data to avoid overwriting with nulls
const responseGet = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/${user.profissional.id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
let currentData = {};
if (responseGet.ok) {
currentData = await responseGet.json();
}
const finalPayload = {
...currentData, // Merge existing
nome: profileData.name,
whatsapp: profileData.phone,
cidade: profileData.location,
observacao: profileData.bio,
// email is usually read-only or handled separately in generic updates
};
const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/${user.profissional.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(finalPayload),
});
if (!response.ok) {
throw new Error("Falha ao atualizar perfil");
}
toast.success("Perfil atualizado com sucesso!");
// Optionally refresh user context
window.location.reload(); // Simple way to refresh context
} catch (error) {
console.error(error);
toast.error("Erro ao salvar perfil.");
}
};
const handleSaveNotifications = () => {
alert("Configurações de notificações salvas!");
toast.success("Configurações de notificações salvas!");
};
const handleSaveAppearance = () => {
alert("Configurações de aparência salvas!");
toast.success("Configurações de aparência salvas!");
};
return (
@ -101,28 +186,20 @@ export const SettingsPage: React.FC = () => {
<Lock size={18} />
<span className="font-medium">Conta</span>
</button>
<button
onClick={() => setActiveTab("notifications")}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors whitespace-nowrap text-sm ${
activeTab === "notifications"
? "bg-brand-gold text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Bell size={18} />
<span className="font-medium">Notificações</span>
</button>
<button
onClick={() => setActiveTab("appearance")}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors whitespace-nowrap text-sm ${
activeTab === "appearance"
? "bg-brand-gold text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Palette size={18} />
<span className="font-medium">Aparência</span>
</button>
{isAdmin && (
<button
onClick={() => setActiveTab("system")}
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors whitespace-nowrap text-sm ${
activeTab === "system"
? "bg-brand-gold text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Database size={18} />
<span className="font-medium">Sistema</span>
</button>
)}
</nav>
</div>
</div>
@ -153,28 +230,21 @@ export const SettingsPage: React.FC = () => {
<Lock size={20} />
<span className="font-medium">Conta</span>
</button>
<button
onClick={() => setActiveTab("notifications")}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${
activeTab === "notifications"
? "bg-brand-gold text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Bell size={20} />
<span className="font-medium">Notificações</span>
</button>
<button
onClick={() => setActiveTab("appearance")}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${
activeTab === "appearance"
? "bg-brand-gold text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Palette size={20} />
<span className="font-medium">Aparência</span>
</button>
{isAdmin && (
<button
onClick={() => setActiveTab("system")}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${
activeTab === "system"
? "bg-brand-gold text-white"
: "text-gray-700 hover:bg-gray-100"
}`}
>
<Database size={20} />
<span className="font-medium">Sistema</span>
</button>
)}
</nav>
</div>
</div>
@ -402,242 +472,14 @@ export const SettingsPage: React.FC = () => {
</div>
)}
{/* Notifications Tab */}
{activeTab === "notifications" && (
{/* System Tab */}
{activeTab === "system" && isAdmin && (
<div>
<h2 className="text-2xl font-semibold mb-6">
Preferências de Notificações
<h2 className="text-2xl font-semibold mb-6">
Configurações do Sistema
</h2>
<div className="space-y-6">
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Notificações por Email</h3>
<p className="text-sm text-gray-600">
Receba atualizações por email
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.emailNotifications}
onChange={(e) =>
setNotificationSettings({
...notificationSettings,
emailNotifications: e.target.checked,
})
}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Notificações Push</h3>
<p className="text-sm text-gray-600">
Receba notificações no navegador
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.pushNotifications}
onChange={(e) =>
setNotificationSettings({
...notificationSettings,
pushNotifications: e.target.checked,
})
}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">SMS</h3>
<p className="text-sm text-gray-600">
Receba mensagens de texto
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.smsNotifications}
onChange={(e) =>
setNotificationSettings({
...notificationSettings,
smsNotifications: e.target.checked,
})
}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Lembretes de Eventos</h3>
<p className="text-sm text-gray-600">
Receba lembretes antes dos eventos
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.eventReminders}
onChange={(e) =>
setNotificationSettings({
...notificationSettings,
eventReminders: e.target.checked,
})
}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h3 className="font-medium">Alertas de Pagamento</h3>
<p className="text-sm text-gray-600">
Notificações sobre pagamentos
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={notificationSettings.paymentAlerts}
onChange={(e) =>
setNotificationSettings({
...notificationSettings,
paymentAlerts: e.target.checked,
})
}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-brand-gold/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-gold"></div>
</label>
</div>
<div className="pt-4">
<Button
size="lg"
variant="secondary"
onClick={handleSaveNotifications}
>
<Save size={20} className="mr-2" />
Salvar Preferências
</Button>
</div>
</div>
</div>
)}
{/* Appearance Tab */}
{activeTab === "appearance" && (
<div>
<h2 className="text-2xl font-semibold mb-6">
Aparência e Idioma
</h2>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tema
</label>
<select
value={appearanceSettings.theme}
onChange={(e) =>
setAppearanceSettings({
...appearanceSettings,
theme: e.target.value,
})
}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="light">Claro</option>
<option value="dark">Escuro</option>
<option value="auto">Automático</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Idioma
</label>
<select
value={appearanceSettings.language}
onChange={(e) =>
setAppearanceSettings({
...appearanceSettings,
language: e.target.value,
})
}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="pt-BR">Português (Brasil)</option>
<option value="en-US">English (US)</option>
<option value="es-ES">Español</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Formato de Data
</label>
<select
value={appearanceSettings.dateFormat}
onChange={(e) =>
setAppearanceSettings({
...appearanceSettings,
dateFormat: e.target.value,
})
}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Moeda
</label>
<select
value={appearanceSettings.currency}
onChange={(e) =>
setAppearanceSettings({
...appearanceSettings,
currency: e.target.value,
})
}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
>
<option value="BRL">Real (R$)</option>
<option value="USD">Dólar ($)</option>
<option value="EUR">Euro ()</option>
</select>
</div>
<div className="pt-4">
<Button
size="lg"
variant="secondary"
onClick={handleSaveAppearance}
>
<Save size={20} className="mr-2" />
Salvar Configurações
</Button>
</div>
</div>
<SystemSettings />
</div>
)}
</div>