mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 05:30:55 +00:00
532 lines
20 KiB
TypeScript
532 lines
20 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { useAuth } from "@/shared/context/AuthContext";
|
|
import { usePositionConfigurationByPositionId } from "@/shared/hooks/usePositionConfigurations";
|
|
import { usePositions } from "@/user-management/hooks/usePosition";
|
|
import { useUnit } from "@/user-management/hooks/useUnit";
|
|
import { Card, CardContent } from "@/shared/common/ui/card";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/shared/common/ui/form";
|
|
import { Switch } from "@/shared/common/ui/switch";
|
|
import { Alert, AlertDescription } from "@/shared/common/ui/alert";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import * as z from "zod";
|
|
import {
|
|
Settings,
|
|
Mail,
|
|
MessageSquare,
|
|
Bell,
|
|
Workflow,
|
|
Loader2,
|
|
AlertCircle,
|
|
Save,
|
|
} from "lucide-react";
|
|
import {
|
|
PositionConfiguration,
|
|
PositionConfigurationPayload,
|
|
} from "@/shared/services/positionConfigurationService";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
import { t } from "i18next";
|
|
|
|
export enum PositionScopeToFetch {
|
|
PARALLEL_WITH_IMMEDIATE_CHILD = "parallel_with_immediate_child",
|
|
IMMEDIATE_CHILD = "immediate_child",
|
|
ALL = "all",
|
|
}
|
|
|
|
export const positionScopeValues = [
|
|
PositionScopeToFetch.ALL,
|
|
PositionScopeToFetch.IMMEDIATE_CHILD,
|
|
PositionScopeToFetch.PARALLEL_WITH_IMMEDIATE_CHILD,
|
|
] as const;
|
|
|
|
const configurationSchema = z.object({
|
|
positionId: z.string().min(1, "Position is required"),
|
|
smsNotificationWhenRecordSubmitted: z.boolean(),
|
|
emailNotificationWhenRecordSubmitted: z.boolean(),
|
|
inboxNotificationWhenRecordSubmitted: z.boolean(),
|
|
skipWorkflowIfNotAssigned: z.boolean(),
|
|
positionScopeToFetch: z.enum(positionScopeValues),
|
|
});
|
|
|
|
type ConfigurationFormData = z.infer<typeof configurationSchema>;
|
|
|
|
const defaultFormValues: ConfigurationFormData = {
|
|
positionId: "",
|
|
smsNotificationWhenRecordSubmitted: true,
|
|
emailNotificationWhenRecordSubmitted: true,
|
|
inboxNotificationWhenRecordSubmitted: true,
|
|
skipWorkflowIfNotAssigned: false,
|
|
positionScopeToFetch: PositionScopeToFetch.ALL,
|
|
};
|
|
|
|
const parsePositionScope = (
|
|
config: PositionConfiguration | null,
|
|
): PositionScopeToFetch => {
|
|
if (
|
|
config?.positionScopeToFetch &&
|
|
positionScopeValues.includes(config.positionScopeToFetch as PositionScopeToFetch)
|
|
) {
|
|
return config.positionScopeToFetch as PositionScopeToFetch;
|
|
}
|
|
|
|
if (!config?.items?.data) return PositionScopeToFetch.ALL;
|
|
|
|
try {
|
|
const parsed = JSON.parse(config.items.data) as {
|
|
positionScopeToFetch?: PositionScopeToFetch;
|
|
};
|
|
if (
|
|
parsed.positionScopeToFetch &&
|
|
positionScopeValues.includes(parsed.positionScopeToFetch)
|
|
) {
|
|
return parsed.positionScopeToFetch;
|
|
}
|
|
} catch {
|
|
return PositionScopeToFetch.ALL;
|
|
}
|
|
|
|
return PositionScopeToFetch.ALL;
|
|
};
|
|
|
|
const buildPayload = (
|
|
data: ConfigurationFormData,
|
|
): PositionConfigurationPayload => ({
|
|
positionId: data.positionId,
|
|
smsNotificationWhenRecordSubmitted: data.smsNotificationWhenRecordSubmitted,
|
|
emailNotificationWhenRecordSubmitted:
|
|
data.emailNotificationWhenRecordSubmitted,
|
|
inboxNotificationWhenRecordSubmitted:
|
|
data.inboxNotificationWhenRecordSubmitted,
|
|
skipWorkflowIfNotAssigned: data.skipWorkflowIfNotAssigned,
|
|
positionScopeToFetch: data.positionScopeToFetch,
|
|
});
|
|
|
|
const PositionConfigurationPage = () => {
|
|
const { user } = useAuth();
|
|
const defaultUnitId = user?.employee?.[0]?.unitId || "";
|
|
const organizationId = user?.employee?.[0]?.organizationId || "";
|
|
const localizedName = useLocalizedName();
|
|
const [selectedUnitId, setSelectedUnitId] = useState(defaultUnitId);
|
|
const [selectedPositionId, setSelectedPositionId] = useState("");
|
|
|
|
const {
|
|
configuration,
|
|
isLoading: isLoadingConfig,
|
|
isError: isConfigError,
|
|
error: configError,
|
|
saveConfiguration,
|
|
isSaving,
|
|
refetch,
|
|
} = usePositionConfigurationByPositionId(selectedPositionId);
|
|
|
|
const { getList: getUnitList } = useUnit();
|
|
const { data: units, isLoading: isLoadingUnits } = getUnitList(
|
|
organizationId,
|
|
{ take: 1000 },
|
|
);
|
|
|
|
const { usePositionListByUnitId } = usePositions();
|
|
const { data: positions, isLoading: isLoadingPositions } =
|
|
usePositionListByUnitId(selectedUnitId, { take: 1000 });
|
|
|
|
const form = useForm<ConfigurationFormData>({
|
|
resolver: zodResolver(configurationSchema),
|
|
defaultValues: defaultFormValues,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (defaultUnitId && !selectedUnitId) {
|
|
setSelectedUnitId(defaultUnitId);
|
|
}
|
|
}, [defaultUnitId, selectedUnitId]);
|
|
|
|
useEffect(() => {
|
|
setSelectedPositionId("");
|
|
}, [selectedUnitId]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedPositionId) {
|
|
form.reset(defaultFormValues);
|
|
return;
|
|
}
|
|
|
|
if (isLoadingConfig) return;
|
|
|
|
if (configuration) {
|
|
form.reset({
|
|
positionId: selectedPositionId,
|
|
smsNotificationWhenRecordSubmitted:
|
|
configuration.smsNotificationWhenRecordSubmitted,
|
|
emailNotificationWhenRecordSubmitted:
|
|
configuration.emailNotificationWhenRecordSubmitted,
|
|
inboxNotificationWhenRecordSubmitted:
|
|
configuration.inboxNotificationWhenRecordSubmitted,
|
|
skipWorkflowIfNotAssigned: configuration.skipWorkflowIfNotAssigned,
|
|
positionScopeToFetch: parsePositionScope(configuration),
|
|
});
|
|
return;
|
|
}
|
|
|
|
form.reset({
|
|
...defaultFormValues,
|
|
positionId: selectedPositionId,
|
|
});
|
|
}, [selectedPositionId, configuration, isLoadingConfig, form]);
|
|
|
|
const handleSave = (data: ConfigurationFormData) => {
|
|
saveConfiguration(buildPayload(data), {
|
|
onSuccess: () => {
|
|
refetch();
|
|
},
|
|
});
|
|
};
|
|
|
|
if (!organizationId) {
|
|
return (
|
|
<div className="flex h-64 items-center justify-center p-6">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<AlertCircle className="h-8 w-8 text-red-500" />
|
|
<div className="text-sm text-muted-foreground">
|
|
{t("setting.msg1")}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="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">
|
|
<div className="mb-5 space-y-1 border-b border-slate-200 pb-4 dark:border-slate-700">
|
|
<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.positionConfig")}
|
|
</h1>
|
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
|
{t("setting.positionConfigMsg")}
|
|
</p>
|
|
</div>
|
|
|
|
<Card className="border-slate-200 bg-white/90 dark:border-slate-700 dark:bg-slate-900/70">
|
|
<CardContent className="space-y-6 pt-6">
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div className="space-y-2 w-full max-w-md">
|
|
<label className="text-sm font-medium text-slate-700 dark:text-slate-200">
|
|
{t("contentManagement.unit")}
|
|
</label>
|
|
<Select value={selectedUnitId} onValueChange={setSelectedUnitId}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder={t("organization.selectUnit")} />
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
position="item-aligned"
|
|
className="max-h-72 w-[min(24rem,90vw)]"
|
|
>
|
|
{isLoadingUnits ? (
|
|
<SelectItem value="loading" disabled>
|
|
{t("common.loading", "Loading...")}
|
|
</SelectItem>
|
|
) : units?.data?.items?.length ? (
|
|
units.data.items.map(
|
|
(unit: {
|
|
id: string;
|
|
name: { am?: string; en?: string };
|
|
}) => (
|
|
<SelectItem key={unit.id} value={unit.id}>
|
|
{localizedName(unit.name)}
|
|
</SelectItem>
|
|
),
|
|
)
|
|
) : (
|
|
<SelectItem value="no-units" disabled>
|
|
{t("setting.noUnit", "No units found")}
|
|
</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2 w-full max-w-md">
|
|
<label className="text-sm font-medium text-slate-700 dark:text-slate-200">
|
|
{t("setting.position")}
|
|
</label>
|
|
<Select
|
|
value={selectedPositionId}
|
|
onValueChange={setSelectedPositionId}
|
|
disabled={!selectedUnitId}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue
|
|
placeholder={
|
|
selectedUnitId
|
|
? t("setting.select")
|
|
: t("setting.select2")
|
|
}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
position="item-aligned"
|
|
className="max-h-72 w-[min(24rem,90vw)]"
|
|
>
|
|
{!selectedUnitId ? (
|
|
<SelectItem value="no-unit-selected" disabled>
|
|
{t("setting.select2")}
|
|
</SelectItem>
|
|
) : isLoadingPositions ? (
|
|
<SelectItem value="loading" disabled>
|
|
{t("common.loading", "Loading...")}
|
|
</SelectItem>
|
|
) : positions?.items?.length ? (
|
|
positions.items.map(
|
|
(position: {
|
|
id: string;
|
|
name: { am?: string; en?: string };
|
|
}) => (
|
|
<SelectItem key={position.id} value={position.id}>
|
|
{localizedName(position.name)}
|
|
</SelectItem>
|
|
),
|
|
)
|
|
) : (
|
|
<SelectItem value="no-positions" disabled>
|
|
{t(
|
|
"setting.noPositions",
|
|
"No positions found in this unit",
|
|
)}
|
|
</SelectItem>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{!selectedPositionId ? (
|
|
<p className="text-sm text-slate-600 dark:text-slate-400">
|
|
{t(
|
|
"setting.selectPositionToConfigure",
|
|
"Select a unit and position to load configuration.",
|
|
)}
|
|
</p>
|
|
) : isLoadingConfig ? (
|
|
<div className="flex items-center justify-center py-10">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("setting.msg2")}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{isConfigError && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription className="flex flex-wrap items-center gap-2">
|
|
{t("setting.error")}{" "}
|
|
{configError instanceof Error
|
|
? configError.message
|
|
: "Unknown error"}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => refetch()}>
|
|
{t("setting.retry")}
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(handleSave)}
|
|
className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="positionScopeToFetch"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="dark:text-gray-200">
|
|
{t("setting.positionScope")}
|
|
</FormLabel>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue
|
|
placeholder={t("setting.positionScope")}
|
|
/>
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value={PositionScopeToFetch.ALL}>
|
|
{t("setting.all")}
|
|
</SelectItem>
|
|
<SelectItem
|
|
value={PositionScopeToFetch.IMMEDIATE_CHILD}>
|
|
{t("setting.immediateChild")}
|
|
</SelectItem>
|
|
<SelectItem
|
|
value={
|
|
PositionScopeToFetch.PARALLEL_WITH_IMMEDIATE_CHILD
|
|
}>
|
|
{t("setting.parallel")}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold">
|
|
{t("setting.notification")}
|
|
</h3>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="smsNotificationWhenRecordSubmitted"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="flex items-center gap-2">
|
|
<MessageSquare className="h-4 w-4" />
|
|
{t("setting.sms")}
|
|
</FormLabel>
|
|
<div className="text-sm text-muted-foreground">
|
|
{t("setting.smsMsg")}
|
|
</div>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="emailNotificationWhenRecordSubmitted"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="flex items-center gap-2">
|
|
<Mail className="h-4 w-4" />
|
|
{t("setting.email")}
|
|
</FormLabel>
|
|
<div className="text-sm text-muted-foreground">
|
|
{t("setting.emailMsg")}
|
|
</div>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="inboxNotificationWhenRecordSubmitted"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="flex items-center gap-2">
|
|
<Bell className="h-4 w-4" />
|
|
{t("setting.inbox")}
|
|
</FormLabel>
|
|
<div className="text-sm text-muted-foreground">
|
|
{t("setting.inboxMsg")}
|
|
</div>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<h3 className="text-lg font-semibold">
|
|
{t("setting.workflow")}
|
|
</h3>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="skipWorkflowIfNotAssigned"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="flex items-center gap-2">
|
|
<Workflow className="h-4 w-4" />
|
|
{t("setting.workflowMsg")}
|
|
</FormLabel>
|
|
<div className="text-sm text-muted-foreground">
|
|
{t("setting.workflowMsg1")}
|
|
</div>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="submit"
|
|
disabled={isSaving}
|
|
className="bg-primary hover:bg-primary/90">
|
|
{isSaving ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
{t("setting.updating")}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
{configuration
|
|
? t("setting.UpdateConfig")
|
|
: t("setting.createConfig")}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PositionConfigurationPage;
|