feat:(agenda) implementado filtro e busca de fot no cadastro de evento

This commit is contained in:
NANDO9322 2026-02-10 19:24:28 -03:00
parent f26d40dcff
commit c9e34af619
2 changed files with 183 additions and 30 deletions

View file

@ -27,6 +27,7 @@ import { UserRole } from "../types";
import { InstitutionForm } from "./InstitutionForm";
import { MapboxMap } from "./MapboxMap";
import { getEventTypes, EventTypeResponse, getCadastroFot, createAgenda, getCompanies } from "../services/apiService";
import { SearchableSelect, SearchableSelectOption } from "./SearchableSelect";
interface EventFormProps {
onCancel: () => void;
@ -855,39 +856,31 @@ export const EventForm: React.FC<EventFormProps> = ({
{/* Consolidated Turma Selection */}
<div className="mb-0">
<label className="block text-sm text-gray-600 mb-1">Selecione a Turma</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
<SearchableSelect
label="Selecione a Turma"
placeholder="Selecione a Turma (Curso - Instituição - Ano)"
value={formData.fotId || ""}
onChange={e => {
const selectedFotId = e.target.value;
const selectedFot = availableFots.find(f => f.id === selectedFotId);
onChange={(value) => {
const selectedFot = availableFots.find(f => f.id === value);
if (selectedFot) {
setFormData({
...formData,
fotId: selectedFotId,
// Optional: You might want to store denormalized data if needed, but ID is usually enough
fotId: value,
});
} else {
setFormData({ ...formData, fotId: "" });
}
}}
disabled={loadingFots || ((user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && !selectedCompanyId)}
>
<option value="">Selecione a Turma (Curso - Instituição - Ano)</option>
{availableFots.map(f => (
<option
key={f.id}
value={f.id}
disabled={f.finalizada}
style={{ color: f.finalizada ? 'red' : 'inherit', fontWeight: f.finalizada ? 'bold' : 'normal' }}
className={f.finalizada ? "text-red-600 bg-red-50" : ""}
>
{f.curso_nome} - {f.instituicao} - {f.ano_formatura_label} (FOT {f.fot}){f.finalizada ? " (FINALIZADA)" : ""}
</option>
))}
</select>
options={availableFots.map(f => ({
value: f.id,
label: `${f.curso_nome} - ${f.instituicao} - ${f.ano_formatura_label} (FOT ${f.fot})${f.finalizada ? " (FINALIZADA)" : ""}`,
disabled: f.finalizada,
className: f.finalizada ? "text-red-600 bg-red-50 font-bold" : "",
style: f.finalizada ? { color: 'red', fontWeight: 'bold' } : undefined
}))}
/>
{loadingFots && <p className="text-xs text-gray-500 mt-1">Carregando turmas...</p>}
</div>
</>

View file

@ -0,0 +1,160 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import { ChevronDown, Search, X } from "lucide-react";
export interface SearchableSelectOption {
value: string;
label: string;
disabled?: boolean;
className?: string; // For custom styling like red text
style?: React.CSSProperties; // For custom inline styles
}
interface SearchableSelectProps {
label?: string;
placeholder?: string;
options: SearchableSelectOption[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
}
export const SearchableSelect: React.FC<SearchableSelectProps> = ({
label,
placeholder = "Selecione...",
options,
value,
onChange,
disabled = false,
className = "",
error,
required = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
// Close when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Focus search input when opening
useEffect(() => {
if (isOpen && searchInputRef.current) {
setTimeout(() => {
searchInputRef.current?.focus();
}, 50);
}
}, [isOpen]);
// Handle Selection
const handleSelect = (optionValue: string) => {
onChange(optionValue);
setIsOpen(false);
setSearchTerm("");
};
const selectedOption = options.find((opt) => opt.value === value);
// Filter options
const filteredOptions = useMemo(() => {
if (!searchTerm) return options;
const lowerTerm = searchTerm.toLowerCase();
return options.filter((opt) => opt.label.toLowerCase().includes(lowerTerm));
}, [options, searchTerm]);
return (
<div className={`w-full ${className}`} ref={containerRef}>
{label && (
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
{label} {required && "*"}
</label>
)}
<div className="relative">
{/* Trigger Button */}
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className={`w-full px-3 py-2 text-left border rounded-sm flex items-center justify-between transition-colors bg-white
${error ? "border-red-500" : "border-gray-300"}
${disabled ? "bg-gray-100 cursor-not-allowed text-gray-400" : "hover:border-gray-400 focus:ring-1 focus:ring-brand-gold focus:border-brand-gold"}
`}
>
<span className={`block truncate ${!selectedOption ? "text-gray-500" : "text-gray-900"}`}>
{selectedOption ? selectedOption.label : placeholder}
</span>
<ChevronDown size={16} className={`text-gray-500 transition-transform ${isOpen ? "transform rotate-180" : ""}`} />
</button>
{/* Dropdown Menu */}
{isOpen && (
<div className="absolute z-50 w-full mb-1 bottom-full bg-white border border-gray-200 rounded-sm shadow-lg max-h-60 flex flex-col">
{/* Search Input */}
<div className="p-2 border-b border-gray-100 sticky top-0 bg-white z-10">
<div className="relative">
<Search size={14} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
<input
ref={searchInputRef}
type="text"
className="w-full pl-8 pr-8 py-1.5 text-xs sm:text-sm border border-gray-200 rounded-sm focus:outline-none focus:border-brand-gold"
placeholder="Buscar..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{searchTerm && (
<button
onClick={() => setSearchTerm("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
<X size={14} />
</button>
)}
</div>
</div>
{/* Options List */}
<div className="overflow-y-auto flex-1">
{filteredOptions.length > 0 ? (
filteredOptions.map((opt) => (
<button
key={opt.value}
type="button"
disabled={opt.disabled}
onClick={() => handleSelect(opt.value)}
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-50 flex items-center justify-between
${opt.value === value ? "bg-blue-50 text-brand-gold font-medium" : "text-gray-700"}
${opt.disabled ? "opacity-50 cursor-not-allowed bg-gray-50/50" : "cursor-pointer"}
${opt.className || ""}
`}
style={opt.style}
>
<span className="truncate">{opt.label}</span>
{opt.value === value && <span className="text-brand-gold text-xs ml-2"></span>}
</button>
))
) : (
<div className="px-4 py-3 text-center text-sm text-gray-500">
Nenhum resultado encontrado
</div>
)}
</div>
</div>
)}
</div>
{error && <span className="text-xs text-red-500 mt-1">{error}</span>}
</div>
);
};