mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
413 lines
15 KiB
TypeScript
413 lines
15 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { Edit2Icon, Settings } from "lucide-react";
|
|
import {
|
|
Select,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
SelectContent,
|
|
SelectItem,
|
|
} from "@/shared/common/ui/select";
|
|
import { useUnit } from "@/user-management/hooks/useUnit";
|
|
import { useAuth } from "@/shared/context/AuthContext";
|
|
import { useUnitConfiguration } from "@/shared/hooks/useUnitConfiguration";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/shared/common/ui/table";
|
|
import {
|
|
prefixSuffixService,
|
|
EEscalationNotificationType,
|
|
EEscalationNotificationChannel,
|
|
} from "@/user-management/services/api/prefixSuffixService";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/shared/common/ui/dialog";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { Input } from "@/shared/common/ui/input";
|
|
import { t } from "i18next";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
|
|
const configurationCardShell =
|
|
"relative overflow-hidden rounded-2xl border border-slate-200/80 bg-gradient-to-br from-white via-slate-50 to-slate-100/60 p-4 shadow-sm sm:p-6 dark:border-slate-700/80 dark:bg-gradient-to-br dark:from-slate-950 dark:via-slate-900 dark:to-slate-900";
|
|
|
|
type FormState = {
|
|
escalationNotificationType: EEscalationNotificationType | "";
|
|
escalationNotificationChannel: EEscalationNotificationChannel | "";
|
|
escalationHour: number | undefined;
|
|
urgentLetterEscalationHour: number | undefined;
|
|
onReviewLetterEscalationHour: number | undefined;
|
|
urgentOnReviewLetterEscalationHour: number | undefined;
|
|
};
|
|
|
|
const emptyForm: FormState = {
|
|
escalationNotificationType: "",
|
|
escalationNotificationChannel: "",
|
|
escalationHour: undefined,
|
|
urgentLetterEscalationHour: undefined,
|
|
onReviewLetterEscalationHour: undefined,
|
|
urgentOnReviewLetterEscalationHour: undefined,
|
|
};
|
|
|
|
const EscalationConfigurationPage = () => {
|
|
const { user } = useAuth();
|
|
const queryClient = useQueryClient();
|
|
const organizationId = user?.employee?.[0]?.organizationId || "";
|
|
const localizedName = useLocalizedName();
|
|
|
|
const { getList: getUnitList } = useUnit();
|
|
const { data: units, isLoading: isLoadingUnits } = getUnitList(
|
|
organizationId,
|
|
{ take: 1000 },
|
|
);
|
|
|
|
const [selectedUnitId, setSelectedUnitId] = useState("");
|
|
const [open, setOpen] = useState(false);
|
|
const [fieldKey, setFieldKey] = useState<string | null>(null);
|
|
const [fieldValue, setFieldValue] = useState<number | undefined>(0);
|
|
const [form, setForm] = useState<FormState>(emptyForm);
|
|
|
|
useEffect(() => {
|
|
if (units?.data?.items?.length > 0 && !selectedUnitId) {
|
|
setSelectedUnitId(units?.data?.items[0].id);
|
|
}
|
|
}, [units]);
|
|
|
|
const { data, isLoading, error } = useUnitConfiguration(selectedUnitId);
|
|
const config = data?.data?.items?.[0];
|
|
|
|
// Sync local form from server data whenever the config or selected unit changes
|
|
useEffect(() => {
|
|
if (config) {
|
|
setForm({
|
|
escalationNotificationType: config.escalationNotificationType ?? "",
|
|
escalationNotificationChannel: config.escalationNotificationChannel ?? "",
|
|
escalationHour: config.escalationHour,
|
|
urgentLetterEscalationHour: config.urgentLetterEscalationHour,
|
|
onReviewLetterEscalationHour: config.onReviewLetterEscalationHour,
|
|
urgentOnReviewLetterEscalationHour:
|
|
config.urgentOnReviewLetterEscalationHour,
|
|
});
|
|
} else {
|
|
setForm(emptyForm);
|
|
}
|
|
}, [config]);
|
|
|
|
const { mutate: saveAll, isPending: isUpdating } = useMutation({
|
|
mutationFn: (id: string) =>
|
|
prefixSuffixService.updateInternalPrefixSuffix(
|
|
{
|
|
escalationNotificationType:
|
|
form.escalationNotificationType || undefined,
|
|
escalationNotificationChannel:
|
|
form.escalationNotificationChannel || undefined,
|
|
escalationHour: form.escalationHour,
|
|
urgentLetterEscalationHour: form.urgentLetterEscalationHour,
|
|
onReviewLetterEscalationHour: form.onReviewLetterEscalationHour,
|
|
urgentOnReviewLetterEscalationHour:
|
|
form.urgentOnReviewLetterEscalationHour,
|
|
unitId: selectedUnitId,
|
|
},
|
|
id,
|
|
),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["unitConfig", selectedUnitId],
|
|
});
|
|
},
|
|
onError: (err) => {
|
|
console.error("Failed to update escalation settings", err);
|
|
},
|
|
});
|
|
|
|
const handleSaveAll = () => {
|
|
if (!config?.id) return;
|
|
saveAll(config.id);
|
|
};
|
|
|
|
const handleEdit = (key: string, value: number | undefined) => {
|
|
setFieldKey(key);
|
|
setFieldValue(value);
|
|
setOpen(true);
|
|
};
|
|
|
|
// Applies the edited hour value to local form state (no API call yet)
|
|
const handleDialogApply = () => {
|
|
if (!fieldKey) return;
|
|
setForm((prev) => ({ ...prev, [fieldKey]: fieldValue }));
|
|
setOpen(false);
|
|
setFieldKey(null);
|
|
};
|
|
|
|
const fields = [
|
|
{
|
|
key: "escalationHour",
|
|
label: t("setting.escalationHour"),
|
|
value: form.escalationHour,
|
|
},
|
|
{
|
|
key: "urgentLetterEscalationHour",
|
|
label: t("setting.urgentLetterEscalationHour"),
|
|
value: form.urgentLetterEscalationHour,
|
|
},
|
|
{
|
|
key: "onReviewLetterEscalationHour",
|
|
label: t("setting.onReviewLetterEscalationHour"),
|
|
value: form.onReviewLetterEscalationHour,
|
|
},
|
|
{
|
|
key: "urgentOnReviewLetterEscalationHour",
|
|
label: t("setting.urgentOnReviewLetterEscalationHour"),
|
|
value: form.urgentOnReviewLetterEscalationHour,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className={configurationCardShell}>
|
|
<div className="mb-5 flex flex-col gap-4 border-b border-slate-200 pb-4 dark:border-slate-700 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="space-y-1">
|
|
<h1 className="flex items-center gap-2 text-2xl font-bold text-slate-900 sm:text-3xl dark:text-slate-100">
|
|
<Settings className="h-8 w-8 text-primary" />
|
|
{t("setting.escalationTitle")}
|
|
</h1>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
|
{t("setting.escalationDescription")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="w-full sm:ml-auto sm:w-auto">
|
|
<Select
|
|
value={selectedUnitId}
|
|
onValueChange={(value) => setSelectedUnitId(value)}
|
|
>
|
|
<SelectTrigger className="h-10 w-full bg-white/90 dark:border-slate-700 dark:bg-slate-900 sm:w-44 [&>span]:truncate">
|
|
<SelectValue placeholder="Select a unit" />
|
|
</SelectTrigger>
|
|
<SelectContent className="dark:border-slate-700 dark:bg-slate-900">
|
|
{isLoadingUnits ? (
|
|
<SelectItem value="loading" disabled>
|
|
{t("setting.loading")}
|
|
</SelectItem>
|
|
) : units?.data?.items?.length > 0 ? (
|
|
units?.data?.items.map((unit: any) => (
|
|
<SelectItem key={unit.id} value={unit.id}>
|
|
<span className="block truncate max-w-70">{localizedName(unit.name)}</span>
|
|
</SelectItem>
|
|
))
|
|
) : (
|
|
<SelectItem value="no-units" disabled>
|
|
{t("setting.noUnit")}
|
|
</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{isLoading && (
|
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
|
{t("setting.loadingConfig")}
|
|
</p>
|
|
)}
|
|
{error && (
|
|
<p className="text-sm text-red-600 dark:text-red-400">
|
|
{t("setting.loadError")}
|
|
</p>
|
|
)}
|
|
{!config && !isLoading && !error && (
|
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
|
{t("setting.noConfigFound")}
|
|
</p>
|
|
)}
|
|
|
|
{config && (
|
|
<div className="space-y-4">
|
|
{/* Escalation Notification Type */}
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
{t("setting.escalationNotificationType")}
|
|
</p>
|
|
<Select
|
|
value={form.escalationNotificationType}
|
|
onValueChange={(val) =>
|
|
setForm((prev) => ({
|
|
...prev,
|
|
escalationNotificationType:
|
|
val as EEscalationNotificationType,
|
|
}))
|
|
}
|
|
>
|
|
<SelectTrigger className="w-full sm:w-64 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
|
|
<SelectValue
|
|
placeholder={t(
|
|
"setting.escalationNotificationTypePlaceholder",
|
|
)}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent className="dark:border-slate-700 dark:bg-slate-900">
|
|
<SelectItem value={EEscalationNotificationType.JUMP}>
|
|
{t("setting.escalationTypeJump")}
|
|
</SelectItem>
|
|
<SelectItem value={EEscalationNotificationType.NOTIFY_STEP}>
|
|
{t("setting.escalationTypeNotifyStep")}
|
|
</SelectItem>
|
|
<SelectItem
|
|
value={EEscalationNotificationType.NOTIFY_PARENT_STEP}
|
|
>
|
|
{t("setting.escalationTypeNotifyParentStep")}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Escalation Hours */}
|
|
<div className="overflow-x-auto rounded-xl border border-slate-200 bg-white/90 dark:border-slate-700 dark:bg-slate-900/70">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="dark:border-slate-700">
|
|
<TableHead className="text-slate-600 dark:text-slate-300">
|
|
{t("setting.Name")}
|
|
</TableHead>
|
|
<TableHead className="text-slate-600 dark:text-slate-300">
|
|
{t("setting.Value")}
|
|
</TableHead>
|
|
<TableHead className="text-right text-slate-600 dark:text-slate-300">
|
|
{t("setting.Action")}
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{fields.map((field) => (
|
|
<TableRow key={field.key} className="dark:border-slate-700">
|
|
<TableCell className="text-slate-900 dark:text-slate-100">
|
|
{field.label}
|
|
</TableCell>
|
|
<TableCell className="text-slate-700 dark:text-slate-300">
|
|
{field.value ?? "-"}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button
|
|
onClick={() => handleEdit(field.key, field.value)}
|
|
variant="outline"
|
|
size="sm"
|
|
className="dark:border-slate-700 dark:bg-slate-900 dark:hover:bg-slate-800"
|
|
>
|
|
<Edit2Icon className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Escalation Notification Channel */}
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
{t("setting.escalationNotificationChannel")}
|
|
</p>
|
|
<Select
|
|
value={form.escalationNotificationChannel}
|
|
onValueChange={(val) =>
|
|
setForm((prev) => ({
|
|
...prev,
|
|
escalationNotificationChannel:
|
|
val as EEscalationNotificationChannel,
|
|
}))
|
|
}
|
|
>
|
|
<SelectTrigger className="w-full sm:w-64 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100">
|
|
<SelectValue
|
|
placeholder={t(
|
|
"setting.escalationNotificationChannelPlaceholder",
|
|
)}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent className="dark:border-slate-700 dark:bg-slate-900">
|
|
<SelectItem value={EEscalationNotificationChannel.ALL}>
|
|
{t("setting.escalationChannelAll")}
|
|
</SelectItem>
|
|
<SelectItem value={EEscalationNotificationChannel.EMAIL}>
|
|
{t("setting.escalationChannelEmail")}
|
|
</SelectItem>
|
|
<SelectItem value={EEscalationNotificationChannel.IN_APP}>
|
|
{t("setting.escalationChannelInApp")}
|
|
</SelectItem>
|
|
<SelectItem value={EEscalationNotificationChannel.SMS}>
|
|
{t("setting.escalationChannelSms")}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Single batch save */}
|
|
<div className="flex justify-end pt-2">
|
|
<Button
|
|
onClick={handleSaveAll}
|
|
disabled={isUpdating}
|
|
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
>
|
|
{isUpdating ? t("setting.saving") : t("setting.saveChanges")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Modal for editing a single hour field — applies to local form only */}
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogContent className="dark:border-slate-700 dark:bg-slate-900">
|
|
<DialogHeader>
|
|
<DialogTitle className="dark:text-slate-100">
|
|
{t("setting.edit")} {fieldKey && t(`setting.${fieldKey}`)}
|
|
</DialogTitle>
|
|
<DialogDescription className="dark:text-slate-400">
|
|
{t("setting.updateEscalationMsg")}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3">
|
|
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
|
{fieldKey && t(`setting.${fieldKey}`)}
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
value={fieldValue ?? ""}
|
|
onChange={(e) => setFieldValue(Number(e.target.value))}
|
|
className="dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100"
|
|
/>
|
|
</div>
|
|
|
|
<DialogFooter className="flex justify-end gap-2 mt-4">
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setOpen(false)}
|
|
className="dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700"
|
|
>
|
|
{t("common.Cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleDialogApply}
|
|
className="bg-primary hover:bg-primary/90 text-primary-foreground"
|
|
>
|
|
{t("common.apply", "Apply")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EscalationConfigurationPage;
|