feat(frontend): consumir tipos dinâmicos

- remove listas fixas em Dashboard, Header e FilterBar
- busca opções via API e reutiliza nos selects e métricas
- fallback dos labels agora trata tipos desconhecidos
master
Artur Oliveira 2025-12-16 18:09:54 -03:00
parent a43fc58ff7
commit d4d65ad0f9
6 changed files with 85 additions and 72 deletions

View File

@ -19,6 +19,9 @@ interface HeaderProps {
userError: string | null; userError: string | null;
onServerCreated?: () => Promise<void> | void; onServerCreated?: () => Promise<void> | void;
onProfileUpdated?: (user: User) => void; onProfileUpdated?: (user: User) => void;
serverTypeOptions?: ServersType[];
applicationOptions?: Applications[];
databaseOptions?: DatabaseType[];
} }
const defaultServerForm: ServerFormState = { const defaultServerForm: ServerFormState = {
@ -39,20 +42,15 @@ const defaultProfileForm: ProfileFormState = {
password: "", password: "",
}; };
const serverTypeOptions: ServersType[] = ["PRODUCTION", "HOMOLOGATION", "DATABASE"]; export const Header = ({
const applicationOptions: Applications[] = [ currentUser,
"ASTERISK", userError,
"HITMANAGER", onServerCreated,
"HITMANAGER_V2", onProfileUpdated,
"OMNIHIT", serverTypeOptions = [],
"HITPHONE", applicationOptions = [],
"CDR", databaseOptions = [],
"FUNCIONALIDADE", }: HeaderProps) => {
"VOICEMAIL",
];
const databaseOptions: DatabaseType[] = ["MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"];
export const Header = ({ currentUser, userError, onServerCreated, onProfileUpdated }: HeaderProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isMenuOpen, setMenuOpen] = useState(false); const [isMenuOpen, setMenuOpen] = useState(false);
const [activeModal, setActiveModal] = useState<ModalType>(null); const [activeModal, setActiveModal] = useState<ModalType>(null);

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import api from "../Api"; import api from "../Api";
import { ServerTypeLabels, type ServersType } from "../types/enums"; import { ServerTypeLabels } from "../types/enums";
import { Code, Database, FlaskConical, Server } from "lucide-icons-react"; import { Code, Database, FlaskConical, Server } from "lucide-icons-react";
interface ServerCount { interface ServerCount {
type: ServersType; type: string;
total: number; total: number;
} }
@ -15,9 +15,9 @@ export const ServerCardMetrics = () => {
useEffect(() => { useEffect(() => {
const fetchCounts = async () => { const fetchCounts = async () => {
try { try {
const { data } = await api.get<Record<ServersType, number>>("/api/servers/type"); const { data } = await api.get<Record<string, number>>("/api/servers/type");
const normalized = Object.entries(data).map(([type, total]) => ({ const normalized = Object.entries(data).map(([type, total]) => ({
type: type as ServersType, type,
total, total,
})); }));
setCounts(normalized); setCounts(normalized);
@ -38,7 +38,7 @@ export const ServerCardMetrics = () => {
return <div className={Styles.placeholder}>Carregando métricas...</div>; return <div className={Styles.placeholder}>Carregando métricas...</div>;
} }
const handleCardIcon = (type: ServersType) => { const handleCardIcon = (type: string) => {
const iconProps = { size: 32, strokeWidth: 1.5 }; const iconProps = { size: 32, strokeWidth: 1.5 };
switch (type) { switch (type) {
@ -64,7 +64,7 @@ export const ServerCardMetrics = () => {
</div> </div>
<div className={Styles.textGroup}> <div className={Styles.textGroup}>
<p className={Styles.value}>{formatTotal(total)}</p> <p className={Styles.value}>{formatTotal(total)}</p>
<p className={Styles.label}>{ServerTypeLabels[type]}</p> <p className={Styles.label}>{ServerTypeLabels[type] ?? type}</p>
</div> </div>
</div> </div>
))} ))}

View File

@ -12,21 +12,18 @@ interface Props {
onTypeChange: (value: ServersType | OptionAll) => void; onTypeChange: (value: ServersType | OptionAll) => void;
onApplicationChange: (value: Applications | OptionAll) => void; onApplicationChange: (value: Applications | OptionAll) => void;
onDbTypeChange: (value: DatabaseType | OptionAll) => void; onDbTypeChange: (value: DatabaseType | OptionAll) => void;
serverTypeOptions?: ServersType[];
applicationOptions?: Applications[];
databaseOptions?: DatabaseType[];
} }
const typeOptions: Array<ServersType | OptionAll> = ["ALL", "PRODUCTION", "HOMOLOGATION", "DATABASE"]; const withAllOption = <T extends string>(options?: T[]): Array<T | OptionAll> => {
const applicationOptions: Array<Applications | OptionAll> = [ if (!options || options.length === 0) {
"ALL", return ["ALL"];
"ASTERISK", }
"HITMANAGER", const unique = Array.from(new Set(options));
"HITMANAGER_V2", return ["ALL", ...unique];
"OMNIHIT", };
"HITPHONE",
"CDR",
"FUNCIONALIDADE",
"VOICEMAIL",
];
const databaseOptions: Array<DatabaseType | OptionAll> = ["ALL", "MYSQL", "POSTGRESQL", "SQLSERVER", "ORACLE", "REDIS", "MONGODB", "MARIADB", "NONE"];
export const ServersFilterBar = ({ export const ServersFilterBar = ({
search, search,
@ -37,7 +34,14 @@ export const ServersFilterBar = ({
onTypeChange, onTypeChange,
onApplicationChange, onApplicationChange,
onDbTypeChange, onDbTypeChange,
serverTypeOptions,
applicationOptions,
databaseOptions,
}: Props) => { }: Props) => {
const typeOptions = withAllOption(serverTypeOptions);
const applicationOptionsList = withAllOption(applicationOptions);
const databaseOptionsList = withAllOption(databaseOptions);
return ( return (
<div className={Styles.wrapper}> <div className={Styles.wrapper}>
<div className={Styles.searchGroup}> <div className={Styles.searchGroup}>
@ -61,13 +65,13 @@ export const ServersFilterBar = ({
label="Aplicação" label="Aplicação"
value={application} value={application}
onChange={(event) => onApplicationChange(event.target.value as Applications | OptionAll)} onChange={(event) => onApplicationChange(event.target.value as Applications | OptionAll)}
options={applicationOptions} options={applicationOptionsList}
/> />
<Select <Select
label="Banco" label="Banco"
value={dbType} value={dbType}
onChange={(event) => onDbTypeChange(event.target.value as DatabaseType | OptionAll)} onChange={(event) => onDbTypeChange(event.target.value as DatabaseType | OptionAll)}
options={databaseOptions} options={databaseOptionsList}
/> />
</div> </div>
); );

View File

@ -1,14 +1,12 @@
import type { Applications, DatabaseType, ServersType } from "../../types/enums";
export type ServerFormState = { export type ServerFormState = {
name: string; name: string;
ip: string; ip: string;
port: string; port: string;
user: string; user: string;
password: string; password: string;
type: ServersType; type: string;
application: Applications; application: string;
dbType: DatabaseType; dbType: string;
}; };
export type ProfileFormState = { export type ProfileFormState = {

View File

@ -8,7 +8,6 @@ import { ServersTable } from "../components/ServersTable";
import { ServersFilterBar } from "../components/ServersFilterBar"; import { ServersFilterBar } from "../components/ServersFilterBar";
import type { Server } from "../types/Server"; import type { Server } from "../types/Server";
import type { User } from "../types/User"; import type { User } from "../types/User";
import type { Applications, DatabaseType, ServersType } from "../types/enums";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
export const Dashboard = () => { export const Dashboard = () => {
@ -19,9 +18,12 @@ export const Dashboard = () => {
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [userError, setUserError] = useState<string | null>(null); const [userError, setUserError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState<ServersType | "ALL">("ALL"); const [typeFilter, setTypeFilter] = useState<string>("ALL");
const [applicationFilter, setApplicationFilter] = useState<Applications | "ALL">("ALL"); const [applicationFilter, setApplicationFilter] = useState<string>("ALL");
const [dbFilter, setDbFilter] = useState<DatabaseType | "ALL">("ALL"); const [dbFilter, setDbFilter] = useState<string>("ALL");
const [serverTypeOptions, setServerTypeOptions] = useState<string[]>([]);
const [applicationOptions, setApplicationOptions] = useState<string[]>([]);
const [databaseOptions, setDatabaseOptions] = useState<string[]>([]);
const fetchServers = useCallback(async () => { const fetchServers = useCallback(async () => {
setLoading(true); setLoading(true);
@ -31,13 +33,13 @@ export const Dashboard = () => {
if (trimmedQuery.length > 0) { if (trimmedQuery.length > 0) {
params.set("query", trimmedQuery); params.set("query", trimmedQuery);
} }
if (typeFilter !== "ALL") { if (typeFilter !== "ALL" && typeFilter.trim().length > 0) {
params.set("type", typeFilter); params.set("type", typeFilter);
} }
if (applicationFilter !== "ALL") { if (applicationFilter !== "ALL" && applicationFilter.trim().length > 0) {
params.set("application", applicationFilter); params.set("application", applicationFilter);
} }
if (dbFilter !== "ALL") { if (dbFilter !== "ALL" && dbFilter.trim().length > 0) {
params.set("dbType", dbFilter); params.set("dbType", dbFilter);
} }
const endpoint = params.toString() ? `/api/servers?${params.toString()}` : "/api/servers"; const endpoint = params.toString() ? `/api/servers?${params.toString()}` : "/api/servers";
@ -76,10 +78,33 @@ export const Dashboard = () => {
fetchCurrentUser(); fetchCurrentUser();
}, [fetchCurrentUser]); }, [fetchCurrentUser]);
const fetchTypeOptions = useCallback(async () => {
try {
const [typesResponse, applicationsResponse, databasesResponse] = await Promise.all([
api.get<string[]>("/api/type-options/SERVER_TYPE"),
api.get<string[]>("/api/type-options/APPLICATION"),
api.get<string[]>("/api/type-options/DATABASE"),
]);
setServerTypeOptions(typesResponse.data ?? []);
setApplicationOptions(applicationsResponse.data ?? []);
setDatabaseOptions(databasesResponse.data ?? []);
} catch (err: any) {
const message = err?.response?.data?.message || "Falha ao carregar opções padrão.";
toast.error(message);
setServerTypeOptions([]);
setApplicationOptions([]);
setDatabaseOptions([]);
}
}, []);
useEffect(() => { useEffect(() => {
fetchServers(); fetchServers();
}, [fetchServers]); }, [fetchServers]);
useEffect(() => {
fetchTypeOptions();
}, [fetchTypeOptions]);
return ( return (
<Layout className="h-screen py-10"> <Layout className="h-screen py-10">
<div className="space-y-6"> <div className="space-y-6">
@ -88,6 +113,9 @@ export const Dashboard = () => {
userError={userError} userError={userError}
onServerCreated={fetchServers} onServerCreated={fetchServers}
onProfileUpdated={setCurrentUser} onProfileUpdated={setCurrentUser}
serverTypeOptions={serverTypeOptions}
applicationOptions={applicationOptions}
databaseOptions={databaseOptions}
/> />
<ServerCardMetrics /> <ServerCardMetrics />
<ServersFilterBar <ServersFilterBar
@ -99,6 +127,9 @@ export const Dashboard = () => {
onTypeChange={setTypeFilter} onTypeChange={setTypeFilter}
onApplicationChange={setApplicationFilter} onApplicationChange={setApplicationFilter}
onDbTypeChange={setDbFilter} onDbTypeChange={setDbFilter}
serverTypeOptions={serverTypeOptions}
applicationOptions={applicationOptions}
databaseOptions={databaseOptions}
/> />
<ServersTable servers={servers} loading={loading} error={error} /> <ServersTable servers={servers} loading={loading} error={error} />
</div> </div>

View File

@ -1,27 +1,9 @@
export type DatabaseType = export type ServersType = string;
| 'MYSQL' export type Applications = string;
| 'POSTGRESQL' export type DatabaseType = string;
| 'SQLSERVER'
| 'ORACLE'
| 'REDIS'
| 'MONGODB'
| 'MARIADB'
| 'NONE';
export type Applications = export const ServerTypeLabels: Record<string, string> = {
| 'ASTERISK' PRODUCTION: "Produção",
| 'HITMANAGER' HOMOLOGATION: "Homologação",
| 'HITMANAGER_V2' DATABASE: "Banco de Dados",
| 'OMNIHIT'
| 'HITPHONE'
| 'CDR'
| 'FUNCIONALIDADE'
| 'VOICEMAIL';
export type ServersType = 'PRODUCTION' | 'HOMOLOGATION' | 'DATABASE';
export const ServerTypeLabels: Record<ServersType, string> = {
PRODUCTION: "Produção",
HOMOLOGATION: "Homologação",
DATABASE: "Banco de Dados",
}; };