2026-09-16 19:05:02 -03:00
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
|
|
import { useAuthContext } from "@/providers/auth-context";
|
|
|
|
|
import { getPrimaryUserRole, isAdminUser } from "@/lib/auth/user-utils";
|
|
|
|
|
import { useDropdownOptionsByCategory } from "@/domain/settings/dropdown-options/use-cases/use-dropdown-options-by-category";
|
|
|
|
|
import type { Service, ServiceInput, ServiceWorkOrderType } from "@/domain/services/types/service";
|
|
|
|
|
import {
|
|
|
|
|
useCreateService,
|
2026-09-16 19:13:00 -03:00
|
|
|
useCompletionDocTemplates,
|
2026-09-16 19:05:02 -03:00
|
|
|
useDeactivateService,
|
|
|
|
|
useServices,
|
|
|
|
|
useUpdateService,
|
|
|
|
|
} from "@/domain/services/use-cases/use-services";
|
|
|
|
|
|
|
|
|
|
export type ServiceStatus = "all" | "active" | "inactive";
|
|
|
|
|
export type ServiceForm = Omit<ServiceInput, "completionDocTemplateId"> & {
|
|
|
|
|
completionDocTemplateId: string | number | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const EMPTY_SERVICE_FORM: ServiceForm = {
|
|
|
|
|
name: "",
|
|
|
|
|
trade: "",
|
|
|
|
|
iconKey: "",
|
|
|
|
|
requiresCompletionDocument: false,
|
|
|
|
|
completionDocTemplateId: null,
|
|
|
|
|
supportedWorkOrderTypes: ["PM", "Reactive", "Emergency"],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const DEFAULT_ICON_BY_TRADE: Record<string, string> = {
|
|
|
|
|
HVAC: "droplets",
|
|
|
|
|
Plumbing: "droplets",
|
|
|
|
|
Electrical: "zap",
|
|
|
|
|
"General Building": "hammer",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function buildServiceInput(form: ServiceForm): {
|
|
|
|
|
input: ServiceInput | null;
|
|
|
|
|
error: string;
|
|
|
|
|
} {
|
|
|
|
|
const name = form.name.trim();
|
|
|
|
|
if (!name || !form.trade || !form.iconKey) {
|
|
|
|
|
return { input: null, error: "Service Name, Trade, and Icon are required." };
|
|
|
|
|
}
|
|
|
|
|
if (!form.supportedWorkOrderTypes.length) {
|
|
|
|
|
return { input: null, error: "Select at least one Work Order Type." };
|
|
|
|
|
}
|
|
|
|
|
if (form.requiresCompletionDocument && form.completionDocTemplateId == null) {
|
|
|
|
|
return {
|
|
|
|
|
input: null,
|
|
|
|
|
error: "Select a completion document template when the document is required.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const templateId = form.completionDocTemplateId;
|
|
|
|
|
return {
|
|
|
|
|
input: {
|
|
|
|
|
...form,
|
|
|
|
|
name,
|
|
|
|
|
completionDocTemplateId: templateId == null || templateId === "" ? null : Number(templateId),
|
|
|
|
|
},
|
|
|
|
|
error: "",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function emptyForm(): ServiceForm {
|
|
|
|
|
return {
|
|
|
|
|
...EMPTY_SERVICE_FORM,
|
|
|
|
|
supportedWorkOrderTypes: [...EMPTY_SERVICE_FORM.supportedWorkOrderTypes],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formFromService(service: Service): ServiceForm {
|
|
|
|
|
return {
|
|
|
|
|
name: service.name,
|
|
|
|
|
trade: service.trade,
|
|
|
|
|
iconKey: service.iconKey,
|
|
|
|
|
requiresCompletionDocument: service.requiresCompletionDocument,
|
|
|
|
|
completionDocTemplateId: service.completionDocTemplate?.id ?? null,
|
|
|
|
|
supportedWorkOrderTypes: [...service.supportedWorkOrderTypes],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useServicesRegistryController() {
|
|
|
|
|
const { user } = useAuthContext();
|
|
|
|
|
const role = getPrimaryUserRole(user?.userRoles).toLowerCase();
|
|
|
|
|
const canManage = isAdminUser(user?.userRoles) || role === "scheduler";
|
|
|
|
|
const canDeactivate = isAdminUser(user?.userRoles);
|
|
|
|
|
const [status, setStatus] = useState<ServiceStatus>("active");
|
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
const [selected, setSelected] = useState<Service | null>(null);
|
|
|
|
|
const [editorMode, setEditorMode] = useState<"add" | "edit" | null>(null);
|
|
|
|
|
const [form, setForm] = useState<ServiceForm>(emptyForm);
|
|
|
|
|
const [iconTouched, setIconTouched] = useState(false);
|
|
|
|
|
const [formError, setFormError] = useState("");
|
|
|
|
|
const [deactivateTarget, setDeactivateTarget] = useState<Service | null>(null);
|
|
|
|
|
|
|
|
|
|
const queryStatus = status === "active" ? true : status === "inactive" ? false : undefined;
|
|
|
|
|
const servicesQuery = useServices(queryStatus);
|
|
|
|
|
const { data: trades = [] } = useDropdownOptionsByCategory("Trade");
|
2026-09-16 19:13:00 -03:00
|
|
|
const { data: templates = [] } = useCompletionDocTemplates();
|
2026-09-16 19:05:02 -03:00
|
|
|
const createService = useCreateService();
|
|
|
|
|
const updateService = useUpdateService();
|
|
|
|
|
const deactivateService = useDeactivateService();
|
|
|
|
|
|
|
|
|
|
const filteredServices = useMemo(() => {
|
|
|
|
|
const needle = search.trim().toLowerCase();
|
|
|
|
|
if (!needle) return servicesQuery.data ?? [];
|
|
|
|
|
return (servicesQuery.data ?? []).filter(
|
|
|
|
|
(service) =>
|
|
|
|
|
service.name.toLowerCase().includes(needle) || service.trade.toLowerCase().includes(needle),
|
|
|
|
|
);
|
|
|
|
|
}, [search, servicesQuery.data]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!iconTouched && form.trade) {
|
|
|
|
|
setForm((current) => ({
|
|
|
|
|
...current,
|
|
|
|
|
iconKey: DEFAULT_ICON_BY_TRADE[current.trade] ?? "wrench",
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}, [form.trade, iconTouched]);
|
|
|
|
|
|
|
|
|
|
const openCreate = () => {
|
|
|
|
|
setSelected(null);
|
|
|
|
|
setForm(emptyForm());
|
|
|
|
|
setIconTouched(false);
|
|
|
|
|
setFormError("");
|
|
|
|
|
setEditorMode("add");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openEdit = (service: Service) => {
|
|
|
|
|
setSelected(service);
|
|
|
|
|
setForm(formFromService(service));
|
|
|
|
|
setIconTouched(true);
|
|
|
|
|
setFormError("");
|
|
|
|
|
setEditorMode("edit");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeEditor = () => {
|
|
|
|
|
if (!createService.isPending && !updateService.isPending) setEditorMode(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateForm = <K extends keyof ServiceForm>(key: K, value: ServiceForm[K]) => {
|
|
|
|
|
if (key === "trade") setIconTouched(false);
|
|
|
|
|
setFormError("");
|
|
|
|
|
setForm((current) => ({ ...current, [key]: value }));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleType = (type: ServiceWorkOrderType) => {
|
|
|
|
|
setForm((current) => {
|
|
|
|
|
const supported = current.supportedWorkOrderTypes.includes(type)
|
|
|
|
|
? current.supportedWorkOrderTypes.filter((item) => item !== type)
|
|
|
|
|
: [...current.supportedWorkOrderTypes, type];
|
|
|
|
|
return { ...current, supportedWorkOrderTypes: supported };
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const save = () => {
|
|
|
|
|
const validation = buildServiceInput(form);
|
|
|
|
|
if (!validation.input) {
|
|
|
|
|
setFormError(validation.error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const input = validation.input;
|
|
|
|
|
if (editorMode === "add") {
|
|
|
|
|
createService.mutate(input, {
|
|
|
|
|
onSuccess: (created) => {
|
|
|
|
|
setEditorMode(null);
|
|
|
|
|
setSelected(created);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} else if (editorMode === "edit" && selected) {
|
|
|
|
|
updateService.mutate(
|
|
|
|
|
{ id: selected.id, input },
|
|
|
|
|
{
|
|
|
|
|
onSuccess: (updated) => {
|
|
|
|
|
setEditorMode(null);
|
|
|
|
|
setSelected(updated);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const confirmDeactivate = () => {
|
|
|
|
|
if (!deactivateTarget) return;
|
|
|
|
|
deactivateService.mutate(deactivateTarget.id, {
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setDeactivateTarget(null);
|
|
|
|
|
setSelected(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
canManage,
|
|
|
|
|
canDeactivate,
|
|
|
|
|
status,
|
|
|
|
|
setStatus,
|
|
|
|
|
search,
|
|
|
|
|
setSearch,
|
|
|
|
|
selected,
|
|
|
|
|
setSelected,
|
|
|
|
|
editorMode,
|
|
|
|
|
form,
|
|
|
|
|
formError,
|
|
|
|
|
trades,
|
|
|
|
|
templates,
|
|
|
|
|
filteredServices,
|
|
|
|
|
services: servicesQuery.data ?? [],
|
|
|
|
|
isLoading: servicesQuery.isLoading,
|
|
|
|
|
error: servicesQuery.error,
|
|
|
|
|
openCreate,
|
|
|
|
|
openEdit,
|
|
|
|
|
closeEditor,
|
|
|
|
|
updateForm,
|
|
|
|
|
toggleType,
|
|
|
|
|
save,
|
|
|
|
|
deactivateTarget,
|
|
|
|
|
setDeactivateTarget,
|
|
|
|
|
confirmDeactivate,
|
|
|
|
|
isSaving: createService.isPending || updateService.isPending,
|
|
|
|
|
isDeactivating: deactivateService.isPending,
|
|
|
|
|
};
|
|
|
|
|
}
|