rule engine and configration and fix booking for custoemr

This commit is contained in:
marshal
2026-06-01 23:15:11 +03:00
parent b9ca0b8a82
commit 452ebdbc0a
52 changed files with 3355 additions and 3358 deletions

View File

@@ -0,0 +1,275 @@
import { useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Field,
FieldContent,
FieldLabel,
Input,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Switch,
Textarea,
} from "@edr/ui-common";
import {
RULE_ENGINE_SELECT_NONE,
type FormFieldDef,
} from "@/pages/ruleEngine/config/resources";
import type { RuleEngineRecord } from "@/types/rule-engine";
import { ruleEngineField, ruleEngineSurface } from "./ruleEngineStyles";
export interface RuleEngineFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
fields: FormFieldDef[];
initialRecord?: RuleEngineRecord | null;
isSubmitting: boolean;
selectOptionsLoading?: boolean;
onSubmit: (values: Record<string, unknown>) => void;
}
const buildInitialValues = (
fields: FormFieldDef[],
record?: RuleEngineRecord | null,
): Record<string, unknown> => {
const values: Record<string, unknown> = {};
for (const field of fields) {
const raw = record?.[field.name];
if (raw !== undefined && raw !== null) {
if (field.type === "date" && typeof raw === "string") {
values[field.name] = raw.slice(0, 10);
} else {
values[field.name] = raw;
}
} else if (field.type === "boolean") {
values[field.name] = false;
} else if (field.type === "number") {
values[field.name] = "";
} else {
values[field.name] = "";
}
}
return values;
};
const resolveSelectValue = (
field: FormFieldDef,
values: Record<string, unknown>,
): string | undefined => {
const raw = values[field.name];
const isEmpty = raw === "" || raw === null || raw === undefined;
if (field.optional && isEmpty) {
return RULE_ENGINE_SELECT_NONE;
}
if (isEmpty) {
return undefined;
}
return String(raw);
};
const RuleEngineFormDialog = ({
open,
onOpenChange,
title,
description,
fields,
initialRecord,
isSubmitting,
selectOptionsLoading = false,
onSubmit,
}: RuleEngineFormDialogProps) => {
const [values, setValues] = useState<Record<string, unknown>>(() =>
buildInitialValues(fields, initialRecord),
);
useEffect(() => {
if (open) {
setValues(buildInitialValues(fields, initialRecord));
}
}, [open, fields, initialRecord]);
const setField = (name: string, value: unknown) => {
setValues((current) => ({ ...current, [name]: value }));
};
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const payload: Record<string, unknown> = {};
for (const field of fields) {
const raw = values[field.name];
if (field.type === "number") {
if (raw === "" || raw === undefined) continue;
payload[field.name] = Number(raw);
} else if (field.type === "boolean") {
payload[field.name] = Boolean(raw);
} else if (
field.type === "select" &&
(raw === "" || raw === RULE_ENGINE_SELECT_NONE)
) {
if (!field.required) continue;
} else if (raw === "" || raw === undefined) {
if (!field.required) continue;
payload[field.name] = raw;
} else {
payload[field.name] = raw;
}
}
if (fields.some((f) => f.name === "code" && typeof payload.code === "string")) {
payload.code = String(payload.code).toUpperCase();
}
onSubmit(payload);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={ruleEngineSurface.dialog}>
<DialogHeader className="space-y-1">
<DialogTitle className="text-lg font-semibold">{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-1">
<div className="max-h-[min(60vh,28rem)] space-y-4 overflow-y-auto pr-1">
{fields.map((field) => (
<Field key={field.name} orientation="vertical" className="gap-1.5">
{field.type === "boolean" ? (
<div className={ruleEngineField.switchRow}>
<div className="min-w-0">
<FieldLabel htmlFor={field.name} className={ruleEngineField.label}>
{field.label}
</FieldLabel>
<p className={ruleEngineField.switchHint}>
{Boolean(values[field.name]) ? "Enabled" : "Disabled"}
</p>
</div>
<Switch
id={field.name}
checked={Boolean(values[field.name])}
onCheckedChange={(checked) => setField(field.name, checked)}
/>
</div>
) : (
<>
<FieldLabel htmlFor={field.name} className={ruleEngineField.label}>
{field.label}
{field.required ? (
<span className={ruleEngineField.requiredMark}> *</span>
) : null}
</FieldLabel>
<FieldContent>
{field.type === "select" ? (
<Select
value={resolveSelectValue(field, values)}
onValueChange={(v) =>
setField(
field.name,
v === RULE_ENGINE_SELECT_NONE ? "" : v,
)
}
disabled={selectOptionsLoading}
>
<SelectTrigger
id={field.name}
className={ruleEngineField.selectTrigger}
>
<SelectValue
placeholder={
selectOptionsLoading
? "Loading options..."
: (field.placeholder ?? "Select an option")
}
/>
</SelectTrigger>
<SelectContent className={ruleEngineField.selectContent}>
{(field.options ?? [])
.filter((opt) => opt.value !== "")
.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : field.type === "textarea" ? (
<Textarea
id={field.name}
value={String(values[field.name] ?? "")}
onChange={(e) => setField(field.name, e.target.value)}
placeholder={field.placeholder}
className={ruleEngineField.textarea}
/>
) : (
<Input
id={field.name}
type={
field.type === "number"
? "number"
: field.type === "date"
? "date"
: "text"
}
value={String(values[field.name] ?? "")}
onChange={(e) => setField(field.name, e.target.value)}
placeholder={field.placeholder}
className={ruleEngineField.input}
required={field.required}
/>
)}
</FieldContent>
</>
)}
</Field>
))}
</div>
<Separator className="my-4" />
<DialogFooter className="gap-2 sm:gap-2">
<Button
type="button"
variant="outline"
className="rounded-md"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" className="rounded-md" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Saving...
</>
) : (
"Save"
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default RuleEngineFormDialog;