- 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
202 lines
8.5 KiB
TypeScript
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>
|
|
);
|
|
};
|