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:
parent
c71095b5f3
commit
b497ea8c72
4 changed files with 626 additions and 288 deletions
202
frontend/components/System/PriceTableEditor.tsx
Normal file
202
frontend/components/System/PriceTableEditor.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
217
frontend/components/System/SimpleCrud.tsx
Normal file
217
frontend/components/System/SimpleCrud.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
77
frontend/components/System/SystemSettings.tsx
Normal file
77
frontend/components/System/SystemSettings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { SystemSettings } from "../components/System/SystemSettings";
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Mail,
|
Mail,
|
||||||
|
|
@ -11,7 +12,9 @@ import {
|
||||||
Save,
|
Save,
|
||||||
Camera,
|
Camera,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
|
Database,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { UserRole } from "../types";
|
import { UserRole } from "../types";
|
||||||
|
|
@ -22,17 +25,32 @@ export const SettingsPage: React.FC = () => {
|
||||||
user?.role === UserRole.SUPERADMIN ||
|
user?.role === UserRole.SUPERADMIN ||
|
||||||
user?.role === UserRole.BUSINESS_OWNER;
|
user?.role === UserRole.BUSINESS_OWNER;
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
"profile" | "account" | "notifications" | "appearance" | "courses"
|
"profile" | "account" | "notifications" | "appearance" | "system"
|
||||||
>("profile");
|
>("profile");
|
||||||
|
|
||||||
const [profileData, setProfileData] = useState({
|
const [profileData, setProfileData] = useState({
|
||||||
name: "João Silva",
|
name: "",
|
||||||
email: "joao.silva@photum.com",
|
email: "",
|
||||||
phone: "(41) 99999-0000",
|
phone: "",
|
||||||
location: "Curitiba, PR",
|
location: "",
|
||||||
bio: "Fotógrafo profissional especializado em eventos e formaturas há mais de 10 anos.",
|
bio: "",
|
||||||
avatar: "https://i.pravatar.cc/150?img=68",
|
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({
|
const [notificationSettings, setNotificationSettings] = useState({
|
||||||
emailNotifications: true,
|
emailNotifications: true,
|
||||||
pushNotifications: true,
|
pushNotifications: true,
|
||||||
|
|
@ -49,16 +67,83 @@ export const SettingsPage: React.FC = () => {
|
||||||
currency: "BRL",
|
currency: "BRL",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSaveProfile = () => {
|
const handleSaveProfile = async () => {
|
||||||
alert("Perfil atualizado com sucesso!");
|
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 = () => {
|
const handleSaveNotifications = () => {
|
||||||
alert("Configurações de notificações salvas!");
|
toast.success("Configurações de notificações salvas!");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveAppearance = () => {
|
const handleSaveAppearance = () => {
|
||||||
alert("Configurações de aparência salvas!");
|
toast.success("Configurações de aparência salvas!");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -101,28 +186,20 @@ export const SettingsPage: React.FC = () => {
|
||||||
<Lock size={18} />
|
<Lock size={18} />
|
||||||
<span className="font-medium">Conta</span>
|
<span className="font-medium">Conta</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("notifications")}
|
{isAdmin && (
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors whitespace-nowrap text-sm ${
|
<button
|
||||||
activeTab === "notifications"
|
onClick={() => setActiveTab("system")}
|
||||||
? "bg-brand-gold text-white"
|
className={`flex items-center gap-2 px-3 py-2 rounded-md transition-colors whitespace-nowrap text-sm ${
|
||||||
: "text-gray-700 hover:bg-gray-100"
|
activeTab === "system"
|
||||||
}`}
|
? "bg-brand-gold text-white"
|
||||||
>
|
: "text-gray-700 hover:bg-gray-100"
|
||||||
<Bell size={18} />
|
}`}
|
||||||
<span className="font-medium">Notificações</span>
|
>
|
||||||
</button>
|
<Database size={18} />
|
||||||
<button
|
<span className="font-medium">Sistema</span>
|
||||||
onClick={() => setActiveTab("appearance")}
|
</button>
|
||||||
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>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -153,28 +230,21 @@ export const SettingsPage: React.FC = () => {
|
||||||
<Lock size={20} />
|
<Lock size={20} />
|
||||||
<span className="font-medium">Conta</span>
|
<span className="font-medium">Conta</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("notifications")}
|
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${
|
{isAdmin && (
|
||||||
activeTab === "notifications"
|
<button
|
||||||
? "bg-brand-gold text-white"
|
onClick={() => setActiveTab("system")}
|
||||||
: "text-gray-700 hover:bg-gray-100"
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${
|
||||||
}`}
|
activeTab === "system"
|
||||||
>
|
? "bg-brand-gold text-white"
|
||||||
<Bell size={20} />
|
: "text-gray-700 hover:bg-gray-100"
|
||||||
<span className="font-medium">Notificações</span>
|
}`}
|
||||||
</button>
|
>
|
||||||
<button
|
<Database size={20} />
|
||||||
onClick={() => setActiveTab("appearance")}
|
<span className="font-medium">Sistema</span>
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-md transition-colors ${
|
</button>
|
||||||
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>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -402,242 +472,14 @@ export const SettingsPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notifications Tab */}
|
|
||||||
{activeTab === "notifications" && (
|
{/* System Tab */}
|
||||||
|
{activeTab === "system" && isAdmin && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-semibold mb-6">
|
<h2 className="text-2xl font-semibold mb-6">
|
||||||
Preferências de Notificações
|
Configurações do Sistema
|
||||||
</h2>
|
</h2>
|
||||||
|
<SystemSettings />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue