photum/frontend/components/System/SimpleCrud.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

217 lines
7.9 KiB
TypeScript

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