From d84d6ff02284a69b1e3a0143e08b203dc892b3f1 Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Mon, 15 Dec 2025 16:12:02 -0300 Subject: [PATCH] feat(fot): implementa cadastro e listagem de FOT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refatora CourseManagement para listar dados de /api/cadastro-fot - Cria componente FotForm para novo cadastro de turmas - Adiciona validação de unicidade para número FOT - Integra dropdowns com endpoints /api/cursos e /api/anos-formaturas - Corrige duplicidade no registro de profissionais no backend --- backend/internal/auth/handler.go | 4 +- frontend/components/FotForm.tsx | 336 +++++++++++++++++++++++ frontend/pages/CourseManagement.tsx | 403 +++++++++++++++------------- frontend/services/apiService.ts | 79 +++++- 4 files changed, 631 insertions(+), 191 deletions(-) create mode 100644 frontend/components/FotForm.tsx diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 878f724..66615fa 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -55,7 +55,9 @@ func (h *Handler) Register(c *gin.Context) { // If role is Photographer, the Service code checks `profissionalData`. // I should probably populate `profissionalData` if it's a professional. - if req.Role == "PHOTOGRAPHER" || req.Role == "BUSINESS_OWNER" { + // PHOTOGRAPHER role is handled by a separate flow (ProfessionalRegister) that calls CreateProfissional after Register. + // We skip creating the partial profile here to avoid duplicates. + if req.Role == "BUSINESS_OWNER" { profData = &profissionais.CreateProfissionalInput{ Nome: req.Nome, Whatsapp: &req.Telefone, diff --git a/frontend/components/FotForm.tsx b/frontend/components/FotForm.tsx new file mode 100644 index 0000000..023c4e1 --- /dev/null +++ b/frontend/components/FotForm.tsx @@ -0,0 +1,336 @@ +import React, { useState, useEffect } from "react"; +import { X, AlertTriangle, Save, Loader } from "lucide-react"; +import { Button } from "./Button"; +import { getCompanies, getAvailableCourses, getGraduationYears, createCadastroFot } from "../services/apiService"; + +interface FotFormProps { + onCancel: () => void; + onSubmit: (success: boolean) => void; + token: string; + existingFots: number[]; // List of existing FOT numbers for validation +} + +export const FotForm: React.FC = ({ onCancel, onSubmit, token, existingFots }) => { + const [formData, setFormData] = useState({ + fot: "", + empresa_id: "", + curso_id: "", + ano_formatura_id: "", + instituicao: "", + cidade: "", + estado: "", + observacoes: "", + gastos_captacao: "", + pre_venda: false, + }); + + const [companies, setCompanies] = useState([]); + const [coursesList, setCoursesList] = useState([]); + const [years, setYears] = useState([]); + + const [loadingDependencies, setLoadingDependencies] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const [fotError, setFotError] = useState(null); + + // Fetch dependencies + useEffect(() => { + const loadData = async () => { + try { + const [companiesRes, coursesRes, yearsRes] = await Promise.all([ + getCompanies(), + getAvailableCourses(), + getGraduationYears() + ]); + + if (companiesRes.data) setCompanies(companiesRes.data); + if (coursesRes.data) setCoursesList(coursesRes.data); + if (yearsRes.data) setYears(yearsRes.data); + } catch (err) { + console.error("Failed to load dependency data", err); + setError("Falha ao carregar opções. Tente novamente."); + } finally { + setLoadingDependencies(false); + } + }; + loadData(); + }, []); + + // Validate FOT uniqueness + const handleFotChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setFormData({ ...formData, fot: val }); + + if (val && existingFots.includes(parseInt(val))) { + setFotError(`O FOT ${val} já existe!`); + } else { + setFotError(null); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (fotError) return; + + setIsSubmitting(true); + setError(null); + + try { + const payload = { + fot: parseInt(formData.fot), + empresa_id: formData.empresa_id, + curso_id: formData.curso_id, + ano_formatura_id: formData.ano_formatura_id, // Assuming string UUID from dropdown + instituicao: formData.instituicao, + cidade: formData.cidade, + estado: formData.estado, + observacoes: formData.observacoes, + gastos_captacao: parseFloat(formData.gastos_captacao) || 0, + pre_venda: formData.pre_venda, + }; + + const result = await createCadastroFot(payload, token); + + if (result.error) { + throw new Error(result.error); + } + + onSubmit(true); + } catch (err: any) { + setError(err.message || "Erro ao salvar FOT."); + } finally { + setIsSubmitting(false); + } + }; + + if (loadingDependencies) { + return ( +
+ +

Carregando opções...

+
+ ); + } + + return ( +
+
+

Cadastro FOT

+ +
+ +
+ {/* FOT number - First and prominent */} +
+ + + {fotError && ( +
+ + {fotError} +
+ )} +
+ +
+ {/* Empresa */} +
+ + +
+ + {/* Curso */} +
+ + +
+ + {/* Ano Formatura */} +
+ + +
+ + {/* Instituição */} +
+ + setFormData({ ...formData, instituicao: 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="Nome da Instituição" + /> +
+ + {/* Cidade */} +
+ + setFormData({ ...formData, cidade: 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="Cidade" + /> +
+ + {/* Estado */} +
+ + +
+ + {/* Gastos Captação */} +
+ +
+ R$ + setFormData({ ...formData, gastos_captacao: e.target.value })} + className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" + placeholder="0,00" + /> +
+
+ + {/* Pre Venda Checkbox */} +
+ +
+
+ + {/* Observações */} +
+ +