- updated backoffice setup

- need to add async loading state
This commit is contained in:
2025-10-07 14:00:58 +01:00
parent 3e564b933d
commit c2c2fc76f2
6 changed files with 251 additions and 153 deletions

View File

@@ -1,37 +1,107 @@
import { Field, Form, Formik } from "formik"; import { Field, Form, Formik, useFormikContext } from "formik";
import FormGroup from "../components/FormGroup"; import FormGroup from "../components/FormGroup";
import { useState } from "react"; import { useEffect, useState } from "react";
import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons"; import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useCameraOutput } from "../../../hooks/useCameraOutput";
import type {
InitialValuesForm,
InitialValuesFormErrors,
} from "../../../types/types";
import { toast } from "sonner";
const ChannelFields = () => { const ChannelFields = () => {
const [showPwd, setShowPwd] = useState(false); const [showPwd, setShowPwd] = useState(false);
const initialValues = { const { backOfficeQuery, backOfficeMutation } = useCameraOutput();
backOfficeURL: "",
username: "", const backOfficeURL = backOfficeQuery?.data?.propBackofficeURL?.value;
password: "", const username = backOfficeQuery?.data?.propUsername?.value;
connectTimeoutSeconds: 0, const password = backOfficeQuery?.data?.propPassword?.value;
readTimeoutSeconds: 0, const connectTimeoutSeconds =
backOfficeQuery?.data?.propConnectTimeoutSeconds?.value;
const readTimeoutSeconds =
backOfficeQuery?.data?.propReadTimeoutSeconds?.value;
const initialValues: InitialValuesForm = {
backOfficeURL: backOfficeURL ?? "",
username: username ?? "",
password: password ?? "",
connectTimeoutSeconds: Number(connectTimeoutSeconds),
readTimeoutSeconds: Number(readTimeoutSeconds),
}; };
const handleSubmit = (values) => { const handleSubmit = (values: InitialValuesForm) => {
console.log(values); backOfficeMutation.mutate(values);
};
const ValidationToastOnce = () => {
const { submitCount, isValid } = useFormikContext();
useEffect(() => {
if (submitCount > 0 && !isValid) {
toast.error("Check fields are filled in");
}
}, [submitCount, isValid]);
return null;
};
const validateValues = (
values: InitialValuesForm
): InitialValuesFormErrors => {
const errors: InitialValuesFormErrors = {};
const url = values.backOfficeURL?.trim();
const username = values.username?.trim();
const password = values.password?.trim();
if (!url) {
errors.backOfficeURL = "Required";
}
if (!username) errors.username = "Required";
if (!password) errors.password = "Required";
const read = Number(values.readTimeoutSeconds);
if (!Number.isFinite(read)) {
errors.readTimeoutSeconds = "Must be a number";
} else if (read < 0) {
errors.readTimeoutSeconds = "Must be ≥ 0";
}
const connect = Number(values.connectTimeoutSeconds);
if (!Number.isFinite(connect)) {
errors.connectTimeoutSeconds = "Must be a number";
} else if (connect < 0) {
errors.connectTimeoutSeconds = "Must be ≥ 0";
}
return errors;
}; };
return ( return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}> <Formik
initialValues={initialValues}
onSubmit={handleSubmit}
enableReinitialize
validate={validateValues}
>
{({ errors, touched }) => (
<Form> <Form>
<div className="flex flex-col space-y-2 px-2"> <div className="flex flex-col space-y-2 px-2">
<FormGroup> <FormGroup>
<label htmlFor="backoffice" className="m-0"> <label htmlFor="backoffice" className="m-0">
Back Office URL Back Office URL
</label> </label>
<Field <Field
name={"backOfficeURL"} name={"backOfficeURL"}
type="text" type="text"
id="backoffice" id="backoffice"
placeholder="https://www.backoffice.com" placeholder="https://www.backoffice.com"
className="p-1.5 border border-gray-400 rounded-lg w-full md:w-60" className={`p-1.5 border ${
errors.backOfficeURL && touched.backOfficeURL
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -41,7 +111,11 @@ const ChannelFields = () => {
type="text" type="text"
id="username" id="username"
placeholder="Back office username" placeholder="Back office username"
className="p-1.5 border border-gray-400 rounded-lg w-full md:w-60" className={`p-1.5 border ${
errors.username && touched.username
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -51,7 +125,11 @@ const ChannelFields = () => {
type={showPwd ? "text" : "password"} type={showPwd ? "text" : "password"}
id="password" id="password"
placeholder="Back office password" placeholder="Back office password"
className="p-1.5 border border-gray-400 rounded-lg w-full md:w-60" className={`p-1.5 border ${
errors.password && touched.password
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/> />
<FontAwesomeIcon <FontAwesomeIcon
type="button" type="button"
@@ -68,7 +146,11 @@ const ChannelFields = () => {
name={"connectTimeoutSeconds"} name={"connectTimeoutSeconds"}
type="number" type="number"
id="connectTimeoutSeconds" id="connectTimeoutSeconds"
className="p-1.5 border border-gray-400 rounded-lg w-full md:w-60" className={`p-1.5 border ${
errors.connectTimeoutSeconds && touched.connectTimeoutSeconds
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/> />
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
@@ -78,11 +160,23 @@ const ChannelFields = () => {
type="number" type="number"
id="readTimeoutSeconds" id="readTimeoutSeconds"
placeholder="https://example.com" placeholder="https://example.com"
className="p-1.5 border border-gray-400 rounded-lg w-full md:w-60" className={`p-1.5 border ${
errors.readTimeoutSeconds && touched.readTimeoutSeconds
? "border-red-500"
: "border-gray-400 "
} rounded-lg w-full md:w-60`}
/> />
</FormGroup> </FormGroup>
</div> </div>
<button
type="submit"
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full md:w-[50%]"
>
Save Changes
</button>
<ValidationToastOnce />
</Form> </Form>
)}
</Formik> </Formik>
); );
}; };

View File

@@ -1,10 +1,8 @@
import { Field, useFormikContext } from "formik"; import { Field } from "formik";
import FormGroup from "../components/FormGroup"; import FormGroup from "../components/FormGroup";
import FormToggle from "../components/FormToggle"; import FormToggle from "../components/FormToggle";
const OverviewTextFields = () => { const OverviewTextFields = () => {
useFormikContext();
return ( return (
<div className="flex flex-col space-y-2 px-2"> <div className="flex flex-col space-y-2 px-2">
<FormGroup> <FormGroup>

View File

@@ -1,78 +1,12 @@
import { Formik, Form } from "formik";
import BearerTypeCard from "../BearerType/BearerTypeCard"; import BearerTypeCard from "../BearerType/BearerTypeCard";
import ChannelCard from "../Channel1-JSON/ChannelCard"; import ChannelCard from "../Channel1-JSON/ChannelCard";
import type { InitialValuesForm } from "../../../types/types";
import { useState } from "react";
import AdvancedToggle from "../../UI/AdvancedToggle";
import OverviewTextCard from "../OverviewText/OverviewTextCard";
import SightingDataCard from "../SightingData/SightingDataCard";
const SettingForms = () => { const SettingForms = () => {
const [advancedToggle, setAdvancedToggle] = useState(false);
const initialValues = {
overviewQuality: "high",
overviewImageScaleFactor: "full",
overviewType: "Plate Overview",
invertMotion: false,
maxPlateValueLength: 0,
vrmToTransit: "plain VRM ASCII (default)",
staticReadAction: "Use Lane Direction",
noRegionAction: "send",
countryCodeType: "IBAN 2 Character code (default)",
filterMinConfidence: 0,
filterMaxConfidence: 100,
overviewQualityOverride: 0,
sightingDataEnabled: false,
sighthingDataVerbose: false,
includeVRM: false,
includeMotion: false,
includeTimestamp: false,
timestampFormat: "UTC",
includeCameraName: false,
customFieldA: "",
customFieldB: "",
customFieldC: "",
customFieldD: "",
overlayPosition: "Top",
};
const handleSubmit = (values: InitialValuesForm) => {
alert(JSON.stringify(values));
};
return ( return (
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-2 sm:px-4 lg:px-0 w-full"> <div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-2 sm:px-4 lg:px-0 w-full">
<BearerTypeCard /> <BearerTypeCard />
<ChannelCard /> <ChannelCard />
</div> </div>
// <Formik initialValues={initialValues} onSubmit={handleSubmit}>
// <Form className="flex flex-col space-y-3">
// <div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-2 sm:px-4 lg:px-0 w-full">
//
// </div>
// <AdvancedToggle
// advancedToggle={advancedToggle}
// onAdvancedChange={setAdvancedToggle}
// />
// {advancedToggle && (
// <>
// <div className="md:col-span-2">
// <SightingDataCard />
// </div>
// <div className="md:col-span-2">
// <OverviewTextCard />
// </div>
// </>
// )}
// <button
// type="submit"
// className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full md:w-[50%]"
// >
// Save Changes
// </button>
// </Form>
// </Formik>
); );
}; };

View File

@@ -1,10 +1,8 @@
import { Field, useFormikContext } from "formik"; import { Field } from "formik";
import FormGroup from "../components/FormGroup"; import FormGroup from "../components/FormGroup";
import FormToggle from "../components/FormToggle"; import FormToggle from "../components/FormToggle";
const SightingDataFields = () => { const SightingDataFields = () => {
useFormikContext();
return ( return (
<div className="flex flex-col space-y-2 px-2"> <div className="flex flex-col space-y-2 px-2">
<FormGroup> <FormGroup>

View File

@@ -2,7 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { CAM_BASE } from "../utils/config"; import { CAM_BASE } from "../utils/config";
import { useEffect } from "react"; import { useEffect } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { BearerTypeFieldType } from "../types/types"; import type { BearerTypeFieldType, InitialValuesForm } from "../types/types";
const getDispatcherConfig = async () => { const getDispatcherConfig = async () => {
const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher`); const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher`);
@@ -18,14 +18,13 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => {
property: "propEnabled", property: "propEnabled",
value: data.enabled, value: data.enabled,
}, },
// Todo: figure out how to add verbose // Todo: figure out how to add verbose conditionally
{ {
property: "propFormat", property: "propFormat",
value: data.format, value: data.format,
}, },
], ],
}; };
const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher`, { const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher`, {
method: "POST", method: "POST",
body: JSON.stringify(updateConfigPayload), body: JSON.stringify(updateConfigPayload),
@@ -34,12 +33,62 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => {
return response.json(); return response.json();
}; };
const getBackOfficeConfig = async () => {
const response = await fetch(
`${CAM_BASE}/api/fetch-config?id=Dispatcher-json`
);
if (!response.ok) throw new Error("Cannot get Back Office configuration");
return response.json();
};
const updateBackOfficeConfig = async (data: InitialValuesForm) => {
const updateConfigPayload = {
id: "Dispatcher-json",
fields: [
{
property: "propBackofficeURL",
value: data.backOfficeURL,
},
{
property: "propConnectTimeoutSeconds",
value: data.connectTimeoutSeconds,
},
{
property: "propPassword",
value: data.password,
},
{
property: "propReadTimeoutSeconds",
value: data.readTimeoutSeconds,
},
{
property: "propUsername",
value: data.username,
},
],
};
const response = await fetch(
`${CAM_BASE}/api/update-config?id=Dispatcher-json`,
{
method: "POST",
body: JSON.stringify(updateConfigPayload),
}
);
if (!response.ok) throw new Error("Cannot update Back Office configuration");
return response.json();
};
export const useCameraOutput = () => { export const useCameraOutput = () => {
const dispatcherQuery = useQuery({ const dispatcherQuery = useQuery({
queryKey: ["dispatcher"], queryKey: ["dispatcher"],
queryFn: getDispatcherConfig, queryFn: getDispatcherConfig,
}); });
const backOfficeQuery = useQuery({
queryKey: ["backoffice"],
queryFn: getBackOfficeConfig,
});
const dispatcherMutation = useMutation({ const dispatcherMutation = useMutation({
mutationFn: updateDispatcherConfig, mutationFn: updateDispatcherConfig,
mutationKey: ["dispatcherUpdate"], mutationKey: ["dispatcherUpdate"],
@@ -51,9 +100,29 @@ export const useCameraOutput = () => {
}, },
}); });
const backOfficeMutation = useMutation({
mutationKey: ["backOfficeUpdate"],
mutationFn: updateBackOfficeConfig,
onError: (error) => toast.error(error.message),
onSuccess: (data) => {
if (data) {
toast.success("Settings successfully updated");
}
},
});
useEffect(() => { useEffect(() => {
if (dispatcherQuery.isError) toast.error(dispatcherQuery.error.message); if (dispatcherQuery.isError) toast.error(dispatcherQuery.error.message);
}, [dispatcherQuery?.error?.message, dispatcherQuery.isError]); }, [dispatcherQuery?.error?.message, dispatcherQuery.isError]);
return { dispatcherQuery, dispatcherMutation }; useEffect(() => {
if (backOfficeQuery.isError) toast.error(backOfficeQuery.error.message);
}, [backOfficeQuery?.error?.message, backOfficeQuery.isError]);
return {
dispatcherQuery,
dispatcherMutation,
backOfficeQuery,
backOfficeMutation,
};
}; };

View File

@@ -49,9 +49,6 @@ export type BearerTypeFieldType = {
}; };
export type InitialValuesForm = { export type InitialValuesForm = {
format: string;
enabled: boolean;
verbose: boolean;
backOfficeURL: string; backOfficeURL: string;
username: string; username: string;
password: string; password: string;
@@ -59,6 +56,14 @@ export type InitialValuesForm = {
readTimeoutSeconds: number; readTimeoutSeconds: number;
}; };
export type InitialValuesFormErrors = {
backOfficeURL?: string;
username?: string;
password?: string;
connectTimeoutSeconds?: string;
readTimeoutSeconds?: string;
};
export type NPEDFieldType = { export type NPEDFieldType = {
frontId: string; frontId: string;
username: string | undefined; username: string | undefined;