- 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
556 lines
17 KiB
TypeScript
556 lines
17 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Course, Institution } from "../types";
|
|
import { Input, Select } from "./Input";
|
|
import { Button } from "./Button";
|
|
import {
|
|
GraduationCap,
|
|
X,
|
|
Check,
|
|
AlertCircle,
|
|
AlertTriangle,
|
|
} from "lucide-react";
|
|
import {
|
|
getInstitutions,
|
|
getGraduationYears,
|
|
getCompanies,
|
|
getEducationLevels,
|
|
getUniversities,
|
|
} from "../services/apiService";
|
|
|
|
interface CourseFormProps {
|
|
onCancel: () => void;
|
|
onSubmit: (data: Partial<Course>) => void;
|
|
initialData?: Course;
|
|
userId: string;
|
|
institutions: Institution[];
|
|
}
|
|
|
|
const UFS = [
|
|
"AC",
|
|
"AL",
|
|
"AP",
|
|
"AM",
|
|
"BA",
|
|
"CE",
|
|
"DF",
|
|
"ES",
|
|
"GO",
|
|
"MA",
|
|
"MT",
|
|
"MS",
|
|
"MG",
|
|
"PA",
|
|
"PB",
|
|
"PR",
|
|
"PE",
|
|
"PI",
|
|
"RJ",
|
|
"RN",
|
|
"RS",
|
|
"RO",
|
|
"RR",
|
|
"SC",
|
|
"SP",
|
|
"SE",
|
|
"TO",
|
|
];
|
|
|
|
export const CourseForm: React.FC<CourseFormProps> = ({
|
|
onCancel,
|
|
onSubmit,
|
|
initialData,
|
|
userId,
|
|
institutions,
|
|
}) => {
|
|
const currentYear = new Date().getFullYear();
|
|
const [formData, setFormData] = useState<Partial<Course>>(
|
|
initialData || {
|
|
name: "",
|
|
institutionId: "",
|
|
year: currentYear,
|
|
semester: 1,
|
|
graduationType: "",
|
|
createdAt: new Date().toISOString(),
|
|
createdBy: userId,
|
|
isActive: true,
|
|
}
|
|
);
|
|
|
|
// Novos campos
|
|
const [fotId, setFotId] = useState("");
|
|
const [empresaId, setEmpresaId] = useState("");
|
|
const [educationLevel, setEducationLevel] = useState("");
|
|
const [observacoes, setObservacoes] = useState("");
|
|
const [instituicao, setInstituicao] = useState("");
|
|
const [anoFormatura, setAnoFormatura] = useState(currentYear.toString());
|
|
const [cidade, setCidade] = useState("");
|
|
const [estado, setEstado] = useState("");
|
|
const [gastosCaptacao, setGastosCaptacao] = useState("");
|
|
const [preVenda, setPreVenda] = useState("nao");
|
|
|
|
const [showToast, setShowToast] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [companies, setCompanies] = useState<
|
|
Array<{ id: string; nome: string }>
|
|
>([]);
|
|
const [educationLevels, setEducationLevels] = useState<
|
|
Array<{ id: string; nome: string }>
|
|
>([]);
|
|
const [universities, setUniversities] = useState<
|
|
Array<{ id: string; nome: string }>
|
|
>([]);
|
|
const [graduationYears, setGraduationYears] = useState<number[]>([]);
|
|
const [isBackendDown, setIsBackendDown] = useState(false);
|
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
|
|
|
// Buscar dados do backend
|
|
useEffect(() => {
|
|
const fetchBackendData = async () => {
|
|
setIsLoadingData(true);
|
|
|
|
const [
|
|
companiesResponse,
|
|
yearsResponse,
|
|
levelsResponse,
|
|
universitiesResponse,
|
|
] = await Promise.all([
|
|
getCompanies(),
|
|
getGraduationYears(),
|
|
getEducationLevels(),
|
|
getUniversities(),
|
|
]);
|
|
|
|
if (
|
|
companiesResponse.isBackendDown ||
|
|
yearsResponse.isBackendDown ||
|
|
levelsResponse.isBackendDown ||
|
|
universitiesResponse.isBackendDown
|
|
) {
|
|
setIsBackendDown(true);
|
|
} else {
|
|
setIsBackendDown(false);
|
|
if (companiesResponse.data) {
|
|
setCompanies(companiesResponse.data);
|
|
}
|
|
if (yearsResponse.data) {
|
|
setGraduationYears(yearsResponse.data);
|
|
}
|
|
if (levelsResponse.data) {
|
|
setEducationLevels(levelsResponse.data);
|
|
}
|
|
if (universitiesResponse.data) {
|
|
setUniversities(universitiesResponse.data);
|
|
}
|
|
}
|
|
|
|
setIsLoadingData(false);
|
|
};
|
|
|
|
fetchBackendData();
|
|
}, []);
|
|
|
|
// Popular campos quando estiver editando
|
|
useEffect(() => {
|
|
if (initialData) {
|
|
setFotId((initialData as any).fotId || "");
|
|
setEmpresaId((initialData as any).empresaId || "");
|
|
setEducationLevel((initialData as any).educationLevel || "");
|
|
setObservacoes((initialData as any).observacoes || "");
|
|
setInstituicao((initialData as any).instituicaoId || "");
|
|
setAnoFormatura(
|
|
(initialData as any).anoFormatura?.toString() || currentYear.toString()
|
|
);
|
|
setCidade((initialData as any).cidade || "");
|
|
setEstado((initialData as any).estado || "");
|
|
setGastosCaptacao((initialData as any).gastosCaptacao || "");
|
|
setPreVenda((initialData as any).preVenda || "nao");
|
|
}
|
|
}, [initialData]);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// Validações
|
|
if (!fotId || fotId.trim().length < 1) {
|
|
setError("FOT é obrigatório");
|
|
return;
|
|
}
|
|
|
|
if (!empresaId) {
|
|
setError("Selecione uma empresa");
|
|
return;
|
|
}
|
|
|
|
if (!educationLevel) {
|
|
setError("Selecione o nível (EF I / EF II)");
|
|
return;
|
|
}
|
|
|
|
if (!instituicao) {
|
|
setError("Selecione uma instituição");
|
|
return;
|
|
}
|
|
|
|
if (!anoFormatura) {
|
|
setError("Selecione o ano de formatura");
|
|
return;
|
|
}
|
|
|
|
if (!cidade || cidade.trim().length < 2) {
|
|
setError("Cidade é obrigatória");
|
|
return;
|
|
}
|
|
|
|
if (!estado) {
|
|
setError("Selecione um estado");
|
|
return;
|
|
}
|
|
|
|
// Montar objeto de dados com os novos campos
|
|
const submitData = {
|
|
...formData,
|
|
fotId,
|
|
empresaId,
|
|
educationLevel,
|
|
observacoes,
|
|
instituicao,
|
|
anoFormatura: parseInt(anoFormatura),
|
|
cidade,
|
|
estado,
|
|
gastosCaptacao,
|
|
preVenda,
|
|
};
|
|
|
|
setShowToast(true);
|
|
setTimeout(() => {
|
|
onSubmit(submitData);
|
|
}, 1000);
|
|
};
|
|
|
|
const formatCurrency = (value: string) => {
|
|
// Remove tudo que não for número
|
|
const numbers = value.replace(/\D/g, "");
|
|
|
|
// Converte para número e formata
|
|
const amount = parseFloat(numbers) / 100;
|
|
return amount.toLocaleString("pt-BR", {
|
|
style: "currency",
|
|
currency: "BRL",
|
|
});
|
|
};
|
|
|
|
const handleGastosCaptacaoChange = (value: string) => {
|
|
if (value === "") {
|
|
setGastosCaptacao("");
|
|
return;
|
|
}
|
|
setGastosCaptacao(formatCurrency(value));
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-2xl max-h-[85vh] overflow-y-auto mx-auto border border-gray-100 slide-up relative">
|
|
{/* Success Toast */}
|
|
{showToast && (
|
|
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-6 py-4 rounded shadow-2xl flex items-center space-x-3 fade-in">
|
|
<Check className="text-brand-gold h-6 w-6" />
|
|
<div>
|
|
<h4 className="font-bold text-sm">Sucesso!</h4>
|
|
<p className="text-xs text-gray-300">
|
|
Curso cadastrado com sucesso.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form Header */}
|
|
<div className="bg-gray-50 border-b px-8 py-6 flex justify-between items-center">
|
|
<div className="flex items-center space-x-3">
|
|
<GraduationCap className="text-brand-gold h-8 w-8" />
|
|
<div>
|
|
<h2 className="text-2xl font-serif text-brand-black">
|
|
{initialData ? "Editar Turma FOT" : "Cadastrar Turma FOT"}
|
|
</h2>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Registre as informações da turma FOT
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onCancel}
|
|
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
|
>
|
|
<X size={20} className="text-gray-600" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
|
{/* Erro global */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3 flex items-start">
|
|
<AlertCircle
|
|
size={18}
|
|
className="text-red-500 mr-2 flex-shrink-0 mt-0.5"
|
|
/>
|
|
<p className="text-sm text-red-700">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Informações da Turma FOT */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-sm font-semibold text-gray-700 tracking-wide uppercase">
|
|
Informações da Turma FOT
|
|
</h3>
|
|
|
|
{/* FOT ID */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
FOT*
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={fotId}
|
|
onChange={(e) => {
|
|
const value = e.target.value.replace(/\D/g, "").slice(0, 5);
|
|
setFotId(value);
|
|
setError("");
|
|
}}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
placeholder="Digite o identificador FOT"
|
|
maxLength={5}
|
|
/>
|
|
</div>
|
|
|
|
{/* Empresa */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Empresa*
|
|
</label>
|
|
<select
|
|
required
|
|
value={empresaId}
|
|
onChange={(e) => {
|
|
setEmpresaId(e.target.value);
|
|
setError("");
|
|
}}
|
|
disabled={isLoadingData || isBackendDown}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
|
|
>
|
|
<option value="">Selecione uma empresa</option>
|
|
{companies.map((company) => (
|
|
<option key={company.id} value={company.id}>
|
|
{company.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{isBackendDown && (
|
|
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
|
|
<AlertTriangle size={16} />
|
|
<span>
|
|
Backend não está rodando. Não é possível carregar empresas.
|
|
</span>
|
|
</div>
|
|
)}
|
|
{isLoadingData && !isBackendDown && (
|
|
<div className="mt-2 text-sm text-gray-500">
|
|
Carregando empresas...
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* EF I / EF II */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Cursos*
|
|
</label>
|
|
<select
|
|
required
|
|
value={educationLevel}
|
|
onChange={(e) => {
|
|
setEducationLevel(e.target.value);
|
|
setError("");
|
|
}}
|
|
disabled={isLoadingData || isBackendDown}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
|
|
>
|
|
<option value="">Selecione o nível</option>
|
|
{educationLevels.map((level) => (
|
|
<option key={level.id} value={level.id}>
|
|
{level.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{isBackendDown && (
|
|
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
|
|
<AlertTriangle size={16} />
|
|
<span>
|
|
Backend não está rodando. Não é possível carregar níveis.
|
|
</span>
|
|
</div>
|
|
)}
|
|
{isLoadingData && !isBackendDown && (
|
|
<div className="mt-2 text-sm text-gray-500">
|
|
Carregando níveis...
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Observações */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Observações
|
|
</label>
|
|
<textarea
|
|
value={observacoes}
|
|
onChange={(e) => setObservacoes(e.target.value)}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
rows={3}
|
|
placeholder="Adicione observações sobre a turma..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Instituição */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Instituição (Universidade)*
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={instituicao}
|
|
onChange={(e) => {
|
|
setInstituicao(e.target.value);
|
|
setError("");
|
|
}}
|
|
placeholder="Digite o nome da universidade"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
/>
|
|
{isBackendDown && (
|
|
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
|
|
<AlertTriangle size={16} />
|
|
<span>Backend não está rodando.</span>
|
|
</div>
|
|
)}
|
|
{isLoadingData && !isBackendDown && (
|
|
<div className="mt-2 text-sm text-gray-500">
|
|
Carregando universidades...
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Ano Formatura */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ano de Formatura*
|
|
</label>
|
|
<select
|
|
required
|
|
value={anoFormatura}
|
|
onChange={(e) => {
|
|
setAnoFormatura(e.target.value);
|
|
setError("");
|
|
}}
|
|
disabled={isLoadingData || isBackendDown}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
|
|
>
|
|
<option value="">Selecione o ano</option>
|
|
{graduationYears.map((year) => (
|
|
<option key={year} value={year}>
|
|
{year}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{isBackendDown && (
|
|
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
|
|
<AlertTriangle size={16} />
|
|
<span>Backend offline</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Cidade e Estado */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Cidade*
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={cidade}
|
|
onChange={(e) => {
|
|
setCidade(e.target.value);
|
|
setError("");
|
|
}}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
placeholder="Cidade"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Estado*
|
|
</label>
|
|
<select
|
|
required
|
|
value={estado}
|
|
onChange={(e) => {
|
|
setEstado(e.target.value);
|
|
setError("");
|
|
}}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
>
|
|
<option value="">UF</option>
|
|
{UFS.map((uf) => (
|
|
<option key={uf} value={uf}>
|
|
{uf}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Gastos Captação */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Gastos Captação
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={gastosCaptacao}
|
|
onChange={(e) => handleGastosCaptacaoChange(e.target.value)}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
placeholder="R$ 0,00"
|
|
/>
|
|
</div>
|
|
|
|
{/* Pré Venda */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Pré Venda*
|
|
</label>
|
|
<select
|
|
required
|
|
value={preVenda}
|
|
onChange={(e) => setPreVenda(e.target.value)}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold"
|
|
>
|
|
<option value="sim">Sim</option>
|
|
<option value="nao">Não</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
|
<Button variant="outline" onClick={onCancel} type="button">
|
|
Cancelar
|
|
</Button>
|
|
<Button type="submit" variant="secondary">
|
|
{initialData ? "Salvar Alterações" : "Cadastrar Turma"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|