photum/frontend/pages/UniversityManagement.tsx
João Vitor 7fc96d77d2 feat: add photographer finance page and UI improvements
- 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
2025-12-12 16:26:12 -03:00

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;