diff --git a/frontend/components/System/PriceTableEditor.tsx b/frontend/components/System/PriceTableEditor.tsx new file mode 100644 index 0000000..125b072 --- /dev/null +++ b/frontend/components/System/PriceTableEditor.tsx @@ -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([]); + const [roles, setRoles] = useState([]); + const [prices, setPrices] = useState>({}); // roleId -> value + const [selectedEventId, setSelectedEventId] = useState(""); + 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 = {}; + 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 ( +
+

+ + Tabela de Preços (Cachês Base) +

+ +
+ + +
+ +
+ + + + + + + + + {loading ? ( + + ) : roles.map(role => ( + + + + + ))} + +
FunçãoValor do Cachê (R$)
Carregando...
+ {role.nome} + +
+ R$ + handlePriceChange(role.id, e.target.value)} + placeholder="0,00" + /> +
+
+
+ +
+ +
+
+ ); +}; diff --git a/frontend/components/System/SimpleCrud.tsx b/frontend/components/System/SimpleCrud.tsx new file mode 100644 index 0000000..266a341 --- /dev/null +++ b/frontend/components/System/SimpleCrud.tsx @@ -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 = ({ + title, + endpoint, + columns = [{ key: "nome", label: "Nome" }], + transformData +}) => { + const { token } = useAuth(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [formData, setFormData] = useState({}); + 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 ( +
+
+

{title}

+
+
+ + setSearchTerm(e.target.value)} + /> +
+ +
+
+ +
+ + + + {columns.map(col => ( + + ))} + + + + + {loading ? ( + + ) : filteredItems.length === 0 ? ( + + ) : ( + filteredItems.map((item) => ( + + {columns.map(col => ( + + ))} + + + )) + )} + +
+ {col.label} + + Ações +
Carregando...
Nenhum item encontrado.
+ {item[col.key]} + + + +
+
+ + {isModalOpen && ( +
+
+
+

+ {editingItem ? `Editar ${title}` : `Novo ${title}`} +

+ +
+ +
+ {columns.map(col => ( +
+ + setFormData({...formData, [col.key]: e.target.value})} + required + /> +
+ ))} + +
+ + +
+
+
+
+ )} +
+ ); +}; diff --git a/frontend/components/System/SystemSettings.tsx b/frontend/components/System/SystemSettings.tsx new file mode 100644 index 0000000..c4f314c --- /dev/null +++ b/frontend/components/System/SystemSettings.tsx @@ -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 ( +
+
+ +
+ +
+ {activeTab === "empresas" && ( + + )} + {activeTab === "cursos" && ( + + )} + {activeTab === "tipos_evento" && ( + + )} + {activeTab === "anos_formatura" && ( + + )} + {activeTab === "precos" && ( + + )} +
+
+ ); +}; diff --git a/frontend/pages/Settings.tsx b/frontend/pages/Settings.tsx index fcb620d..424fa9d 100644 --- a/frontend/pages/Settings.tsx +++ b/frontend/pages/Settings.tsx @@ -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 = () => { Conta - - + + {isAdmin && ( + + )} @@ -153,28 +230,21 @@ export const SettingsPage: React.FC = () => { Conta - - + + + {isAdmin && ( + + )} @@ -402,242 +472,14 @@ export const SettingsPage: React.FC = () => { )} - {/* Notifications Tab */} - {activeTab === "notifications" && ( + + {/* System Tab */} + {activeTab === "system" && isAdmin && (
-

- Preferências de Notificações +

+ Configurações do Sistema

- -
-
-
-

Notificações por Email

-

- Receba atualizações por email -

-
- -
- -
-
-

Notificações Push

-

- Receba notificações no navegador -

-
- -
- -
-
-

SMS

-

- Receba mensagens de texto -

-
- -
- -
-
-

Lembretes de Eventos

-

- Receba lembretes antes dos eventos -

-
- -
- -
-
-

Alertas de Pagamento

-

- Notificações sobre pagamentos -

-
- -
- -
- -
-
-
- )} - - {/* Appearance Tab */} - {activeTab === "appearance" && ( -
-

- Aparência e Idioma -

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
+
)}