- Add photographer finance page at /meus-pagamentos with payment history table - Remove university management page and related routes - Update Finance and UserApproval pages with consistent spacing and typography - Fix Dashboard background color to match other pages (bg-gray-50) - Standardize navbar logo sizing across all pages - Change institution field in course form from dropdown to text input - Add year and semester fields for university graduation dates - Improve header spacing on all pages to pt-20 sm:pt-24 md:pt-28 lg:pt-32 - Apply font-serif styling consistently across page headers
490 lines
24 KiB
TypeScript
490 lines
24 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Download, Plus, ArrowUpDown, ArrowUp, ArrowDown, X, AlertCircle } from 'lucide-react';
|
|
|
|
interface University {
|
|
id: string;
|
|
empresa: string;
|
|
nomeUniversidade: string;
|
|
anoFormatura: number;
|
|
semestre: number;
|
|
cursos: string[];
|
|
}
|
|
|
|
const UniversityManagement: React.FC = () => {
|
|
const [universities, setUniversities] = useState<University[]>([
|
|
{
|
|
id: '1',
|
|
empresa: 'PhotoPro Studio',
|
|
nomeUniversidade: 'UFPR - Universidade Federal do Paraná',
|
|
anoFormatura: 2025,
|
|
semestre: 2,
|
|
cursos: ['Medicina', 'Direito', 'Engenharia Civil']
|
|
},
|
|
{
|
|
id: '2',
|
|
empresa: 'Lens & Art',
|
|
nomeUniversidade: 'PUC-PR - Pontifícia Universidade Católica do Paraná',
|
|
anoFormatura: 2025,
|
|
semestre: 1,
|
|
cursos: ['Administração', 'Arquitetura', 'Psicologia']
|
|
}
|
|
]);
|
|
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [selectedUniversity, setSelectedUniversity] = useState<University | null>(null);
|
|
const [sortConfig, setSortConfig] = useState<{ key: keyof University; direction: 'asc' | 'desc' } | null>(null);
|
|
|
|
// Estados para dados da API
|
|
const [cursos, setCursos] = useState<any[]>([]);
|
|
const [empresas, setEmpresas] = useState<any[]>([]);
|
|
const [apiError, setApiError] = useState<string>('');
|
|
const [loadingApi, setLoadingApi] = useState(false);
|
|
|
|
// Form state
|
|
const [formData, setFormData] = useState<Partial<University>>({
|
|
empresa: '',
|
|
nomeUniversidade: '',
|
|
anoFormatura: new Date().getFullYear(),
|
|
semestre: 1,
|
|
cursos: []
|
|
});
|
|
|
|
// Estados para seleção de cursos
|
|
const [selectedCursos, setSelectedCursos] = useState<string[]>([]);
|
|
const [cursoInput, setCursoInput] = useState('');
|
|
|
|
// Carregar dados da API
|
|
const loadApiData = async () => {
|
|
setLoadingApi(true);
|
|
setApiError('');
|
|
try {
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';
|
|
|
|
// Carregar cursos
|
|
try {
|
|
const cursosRes = await fetch(`${API_BASE_URL}/api/cursos`);
|
|
if (cursosRes.ok) {
|
|
const cursosData = await cursosRes.json();
|
|
setCursos(cursosData);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar cursos:', error);
|
|
}
|
|
|
|
// Carregar empresas
|
|
try {
|
|
const empRes = await fetch(`${API_BASE_URL}/api/empresas`);
|
|
if (empRes.ok) {
|
|
const empData = await empRes.json();
|
|
setEmpresas(empData);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar empresas:', error);
|
|
}
|
|
|
|
if (cursos.length === 0 && empresas.length === 0) {
|
|
setApiError('Backend não está rodando. Alguns campos podem não estar disponíveis.');
|
|
}
|
|
|
|
} catch (error) {
|
|
setApiError('Backend não está rodando. Alguns campos podem não estar disponíveis.');
|
|
} finally {
|
|
setLoadingApi(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (showAddModal || showEditModal) {
|
|
loadApiData();
|
|
}
|
|
}, [showAddModal, showEditModal]);
|
|
|
|
// Ordenação
|
|
const handleSort = (key: keyof University) => {
|
|
if (sortConfig && sortConfig.key === key) {
|
|
if (sortConfig.direction === 'asc') {
|
|
setSortConfig({ key, direction: 'desc' });
|
|
} else {
|
|
setSortConfig(null);
|
|
}
|
|
} else {
|
|
setSortConfig({ key, direction: 'asc' });
|
|
}
|
|
};
|
|
|
|
const sortedUniversities = React.useMemo(() => {
|
|
let sortableUniversities = [...universities];
|
|
if (sortConfig !== null) {
|
|
sortableUniversities.sort((a, b) => {
|
|
const aValue = a[sortConfig.key];
|
|
const bValue = b[sortConfig.key];
|
|
|
|
if (aValue < bValue) {
|
|
return sortConfig.direction === 'asc' ? -1 : 1;
|
|
}
|
|
if (aValue > bValue) {
|
|
return sortConfig.direction === 'asc' ? 1 : -1;
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
return sortableUniversities;
|
|
}, [universities, sortConfig]);
|
|
|
|
const getSortIcon = (key: keyof University) => {
|
|
if (sortConfig?.key !== key) {
|
|
return <ArrowUpDown size={14} className="opacity-0 group-hover:opacity-50 transition-opacity" />;
|
|
}
|
|
if (sortConfig.direction === 'asc') {
|
|
return <ArrowUp size={14} className="text-brand-gold" />;
|
|
}
|
|
return <ArrowDown size={14} className="text-brand-gold" />;
|
|
};
|
|
|
|
// Handlers
|
|
const handleAddUniversity = () => {
|
|
setFormData({
|
|
empresa: '',
|
|
nomeUniversidade: '',
|
|
semestre: 1,
|
|
anoFormatura: new Date().getFullYear(),
|
|
cursos: []
|
|
});
|
|
setSelectedCursos([]);
|
|
setShowAddModal(true);
|
|
};
|
|
|
|
const handleEditUniversity = (university: University) => {
|
|
setSelectedUniversity(university);
|
|
setFormData(university);
|
|
setSelectedCursos(university.cursos || []);
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
const handleAddCurso = () => {
|
|
if (cursoInput && !selectedCursos.includes(cursoInput)) {
|
|
setSelectedCursos([...selectedCursos, cursoInput]);
|
|
setCursoInput('');
|
|
}
|
|
};
|
|
|
|
const handleRemoveCurso = (curso: string) => {
|
|
setSelectedCursos(selectedCursos.filter(c => c !== curso));
|
|
};
|
|
|
|
const handleSaveUniversity = () => {
|
|
const updatedData = { ...formData, cursos: selectedCursos };
|
|
|
|
if (showEditModal && selectedUniversity) {
|
|
setUniversities(universities.map(u =>
|
|
u.id === selectedUniversity.id
|
|
? { ...updatedData, id: selectedUniversity.id } as University
|
|
: u
|
|
));
|
|
setShowEditModal(false);
|
|
} else {
|
|
const newUniversity: University = {
|
|
...updatedData,
|
|
id: Date.now().toString()
|
|
} as University;
|
|
setUniversities([...universities, newUniversity]);
|
|
setShowAddModal(false);
|
|
}
|
|
setSelectedUniversity(null);
|
|
setSelectedCursos([]);
|
|
};
|
|
|
|
const handleExport = () => {
|
|
const headers = ['Empresa', 'Nome da Universidade', 'Ano Formatura', 'Cursos'];
|
|
|
|
const csvContent = [
|
|
headers.join(','),
|
|
...universities.map(u => [
|
|
u.empresa,
|
|
`"${u.nomeUniversidade}"`,
|
|
`${u.anoFormatura}.${u.semestre}`,
|
|
`"${u.cursos.join(', ')}"`
|
|
].join(','))
|
|
].join('\n');
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = `universidades_${new Date().toISOString().split('T')[0]}.csv`;
|
|
link.click();
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 pb-8 sm:pb-12">
|
|
<div className="max-w-[95%] mx-auto px-3 sm:px-4">
|
|
{/* Header */}
|
|
<div className="mb-4 sm:mb-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-0">
|
|
<div>
|
|
<h1 className="text-xl sm:text-2xl md:text-3xl font-serif font-bold text-brand-black mb-1 sm:mb-2">
|
|
Gestão de Universidades
|
|
</h1>
|
|
<p className="text-xs sm:text-sm text-gray-600">
|
|
Cadastro e gerenciamento de universidades
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2 sm:gap-3 w-full sm:w-auto">
|
|
<button
|
|
onClick={handleExport}
|
|
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors font-medium text-xs sm:text-sm flex-1 sm:flex-none justify-center"
|
|
>
|
|
<Download size={16} className="sm:w-[18px] sm:h-[18px]" />
|
|
<span className="hidden sm:inline">Exportar</span>
|
|
</button>
|
|
<button
|
|
onClick={handleAddUniversity}
|
|
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium text-xs sm:text-sm flex-1 sm:flex-none justify-center"
|
|
>
|
|
<Plus size={16} className="sm:w-[18px] sm:h-[18px]" />
|
|
<span className="hidden sm:inline">Cadastrar</span>
|
|
<span className="sm:hidden">Nova Universidade</span>
|
|
<span className="hidden sm:inline">Universidade</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabela */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-x-auto">
|
|
<table className="w-full text-xs sm:text-sm min-w-[600px]">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
{[
|
|
{ key: 'empresa', label: 'Empresa' },
|
|
{ key: 'nomeUniversidade', label: 'Nome da Universidade' },
|
|
{ key: 'anoFormatura', label: 'Ano Formatura' },
|
|
{ key: 'cursos', label: 'Cursos' }
|
|
].map((column) => (
|
|
<th
|
|
key={column.key}
|
|
onClick={() => handleSort(column.key as keyof University)}
|
|
className="px-4 py-3 text-left font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors whitespace-nowrap group"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{column.label}
|
|
{getSortIcon(column.key as keyof University)}
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{sortedUniversities.map((university) => (
|
|
<tr
|
|
key={university.id}
|
|
onClick={() => handleEditUniversity(university)}
|
|
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
|
>
|
|
<td className="px-4 py-3 whitespace-nowrap">{university.empresa}</td>
|
|
<td className="px-4 py-3">{university.nomeUniversidade}</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">{university.anoFormatura}.{university.semestre}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex flex-wrap gap-1">
|
|
{university.cursos.map((curso, idx) => (
|
|
<span
|
|
key={idx}
|
|
className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs"
|
|
>
|
|
{curso}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{sortedUniversities.length === 0 && (
|
|
<div className="text-center py-12 text-gray-500">
|
|
Nenhuma universidade cadastrada
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal Adicionar/Editar */}
|
|
{(showAddModal || showEditModal) && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div className="sticky top-0 bg-white border-b border-gray-200 p-6 flex items-center justify-between">
|
|
<h2 className="text-2xl font-bold">
|
|
{showEditModal ? 'Editar Universidade' : 'Cadastrar Universidade'}
|
|
</h2>
|
|
<button
|
|
onClick={() => {
|
|
setShowAddModal(false);
|
|
setShowEditModal(false);
|
|
setSelectedUniversity(null);
|
|
setSelectedCursos([]);
|
|
}}
|
|
className="text-gray-500 hover:text-gray-700"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{apiError && (
|
|
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
|
|
<AlertCircle className="text-yellow-600 flex-shrink-0 mt-0.5" size={20} />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-yellow-800">Aviso</p>
|
|
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-6">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{/* Empresa */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Empresa *
|
|
</label>
|
|
<select
|
|
value={formData.empresa}
|
|
onChange={(e) => setFormData({ ...formData, empresa: e.target.value })}
|
|
disabled={loadingApi}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi ? 'Carregando...' : empresas.length > 0 ? 'Selecione uma empresa' : 'Nenhuma empresa disponível'}
|
|
</option>
|
|
{empresas.map((emp) => (
|
|
<option key={emp.id} value={emp.nome}>
|
|
{emp.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Nome da Universidade */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nome da Universidade *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.nomeUniversidade}
|
|
onChange={(e) => setFormData({ ...formData, nomeUniversidade: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
placeholder="Ex: UFPR - Universidade Federal do Paraná"
|
|
/>
|
|
</div>
|
|
|
|
{/* Ano Formatura */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ano *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={formData.anoFormatura}
|
|
onChange={(e) => setFormData({ ...formData, anoFormatura: parseInt(e.target.value) })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
placeholder="2025"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Semestre *
|
|
</label>
|
|
<select
|
|
value={formData.semestre}
|
|
onChange={(e) => setFormData({ ...formData, semestre: parseInt(e.target.value) })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
>
|
|
<option value={1}>1</option>
|
|
<option value={2}>2</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Cursos */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Cursos *
|
|
</label>
|
|
<div className="flex gap-2 mb-2">
|
|
<select
|
|
value={cursoInput}
|
|
onChange={(e) => setCursoInput(e.target.value)}
|
|
disabled={loadingApi}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi ? 'Carregando...' : cursos.length > 0 ? 'Selecione um curso' : 'Nenhum curso disponível'}
|
|
</option>
|
|
{cursos.map((curso) => (
|
|
<option key={curso.id} value={curso.nome}>
|
|
{curso.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
type="button"
|
|
onClick={handleAddCurso}
|
|
disabled={!cursoInput}
|
|
className="px-4 py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Adicionar
|
|
</button>
|
|
</div>
|
|
|
|
{/* Lista de cursos selecionados */}
|
|
<div className="flex flex-wrap gap-2 mt-3">
|
|
{selectedCursos.map((curso, idx) => (
|
|
<span
|
|
key={idx}
|
|
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm flex items-center gap-2"
|
|
>
|
|
{curso}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveCurso(curso)}
|
|
className="hover:text-blue-900"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
{selectedCursos.length === 0 && (
|
|
<p className="text-sm text-gray-500 mt-2">Nenhum curso selecionado</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botões */}
|
|
<div className="flex gap-3 mt-8">
|
|
<button
|
|
onClick={() => {
|
|
setShowAddModal(false);
|
|
setShowEditModal(false);
|
|
setSelectedUniversity(null);
|
|
setSelectedCursos([]);
|
|
}}
|
|
className="flex-1 px-4 py-3 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors font-medium"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={handleSaveUniversity}
|
|
className="flex-1 px-4 py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium"
|
|
>
|
|
{showEditModal ? 'Salvar Alterações' : 'Cadastrar'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UniversityManagement;
|