photum/frontend/components/System/PriceTableEditor.tsx
NANDO9322 b497ea8c72 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
2026-01-31 12:48:42 -03:00

202 lines
8.5 KiB
TypeScript

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>
);
};