Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/EscalationConfigurationPage.tsx
yaschalew 697799b00d fix
2026-07-10 14:24:58 +03:00

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;