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

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;