From c2c2fc76f29fd1c7fcf0a44057b02e9d5e876af4 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 7 Oct 2025 14:00:58 +0100 Subject: [PATCH] - updated backoffice setup - need to add async loading state --- .../Channel1-JSON/ChannelFields.tsx | 242 ++++++++++++------ .../OverviewText/OverviewTextFields.tsx | 4 +- .../SettingForms/SettingForms.tsx | 66 ----- .../SightingData/SightingDataFields.tsx | 4 +- src/hooks/useCameraOutput.ts | 77 +++++- src/types/types.ts | 11 +- 6 files changed, 251 insertions(+), 153 deletions(-) diff --git a/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx b/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx index 9c0e64e..c2d601b 100644 --- a/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx +++ b/src/components/SettingForms/Channel1-JSON/ChannelFields.tsx @@ -1,88 +1,182 @@ -import { Field, Form, Formik } from "formik"; +import { Field, Form, Formik, useFormikContext } from "formik"; import FormGroup from "../components/FormGroup"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons"; 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 [showPwd, setShowPwd] = useState(false); - const initialValues = { - backOfficeURL: "", - username: "", - password: "", - connectTimeoutSeconds: 0, - readTimeoutSeconds: 0, + const { backOfficeQuery, backOfficeMutation } = useCameraOutput(); + + const backOfficeURL = backOfficeQuery?.data?.propBackofficeURL?.value; + const username = backOfficeQuery?.data?.propUsername?.value; + const password = backOfficeQuery?.data?.propPassword?.value; + 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) => { - console.log(values); + const handleSubmit = (values: InitialValuesForm) => { + 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 ( - -
-
- - - - - - - - - - - - setShowPwd((s) => !s)} - icon={showPwd ? faEyeSlash : faEye} - /> - - - - - - - - - -
-
+ + {({ errors, touched }) => ( +
+
+ + + + + + + + + + + + + setShowPwd((s) => !s)} + icon={showPwd ? faEyeSlash : faEye} + /> + + + + + + + + + +
+ + + + )}
); }; diff --git a/src/components/SettingForms/OverviewText/OverviewTextFields.tsx b/src/components/SettingForms/OverviewText/OverviewTextFields.tsx index e6944bc..d7a83ef 100644 --- a/src/components/SettingForms/OverviewText/OverviewTextFields.tsx +++ b/src/components/SettingForms/OverviewText/OverviewTextFields.tsx @@ -1,10 +1,8 @@ -import { Field, useFormikContext } from "formik"; +import { Field } from "formik"; import FormGroup from "../components/FormGroup"; import FormToggle from "../components/FormToggle"; const OverviewTextFields = () => { - useFormikContext(); - return (
diff --git a/src/components/SettingForms/SettingForms/SettingForms.tsx b/src/components/SettingForms/SettingForms/SettingForms.tsx index 78e5f8b..632d076 100644 --- a/src/components/SettingForms/SettingForms/SettingForms.tsx +++ b/src/components/SettingForms/SettingForms/SettingForms.tsx @@ -1,78 +1,12 @@ -import { Formik, Form } from "formik"; import BearerTypeCard from "../BearerType/BearerTypeCard"; 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 [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 (
- // - //
- //
- // - //
- // - // {advancedToggle && ( - // <> - //
- // - //
- //
- // - //
- // - // )} - // - // - //
); }; diff --git a/src/components/SettingForms/SightingData/SightingDataFields.tsx b/src/components/SettingForms/SightingData/SightingDataFields.tsx index 262d694..0259714 100644 --- a/src/components/SettingForms/SightingData/SightingDataFields.tsx +++ b/src/components/SettingForms/SightingData/SightingDataFields.tsx @@ -1,10 +1,8 @@ -import { Field, useFormikContext } from "formik"; +import { Field } from "formik"; import FormGroup from "../components/FormGroup"; import FormToggle from "../components/FormToggle"; const SightingDataFields = () => { - useFormikContext(); - return (
diff --git a/src/hooks/useCameraOutput.ts b/src/hooks/useCameraOutput.ts index 995072a..9c7f681 100644 --- a/src/hooks/useCameraOutput.ts +++ b/src/hooks/useCameraOutput.ts @@ -2,7 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { CAM_BASE } from "../utils/config"; import { useEffect } from "react"; import { toast } from "sonner"; -import type { BearerTypeFieldType } from "../types/types"; +import type { BearerTypeFieldType, InitialValuesForm } from "../types/types"; const getDispatcherConfig = async () => { const response = await fetch(`${CAM_BASE}/api/fetch-config?id=Dispatcher`); @@ -18,14 +18,13 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => { property: "propEnabled", value: data.enabled, }, - // Todo: figure out how to add verbose + // Todo: figure out how to add verbose conditionally { property: "propFormat", value: data.format, }, ], }; - const response = await fetch(`${CAM_BASE}/api/update-config?id=Dispatcher`, { method: "POST", body: JSON.stringify(updateConfigPayload), @@ -34,12 +33,62 @@ const updateDispatcherConfig = async (data: BearerTypeFieldType) => { 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 = () => { const dispatcherQuery = useQuery({ queryKey: ["dispatcher"], queryFn: getDispatcherConfig, }); + const backOfficeQuery = useQuery({ + queryKey: ["backoffice"], + queryFn: getBackOfficeConfig, + }); + const dispatcherMutation = useMutation({ mutationFn: updateDispatcherConfig, 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(() => { if (dispatcherQuery.isError) toast.error(dispatcherQuery.error.message); }, [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, + }; }; diff --git a/src/types/types.ts b/src/types/types.ts index bb1361e..6169124 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -49,9 +49,6 @@ export type BearerTypeFieldType = { }; export type InitialValuesForm = { - format: string; - enabled: boolean; - verbose: boolean; backOfficeURL: string; username: string; password: string; @@ -59,6 +56,14 @@ export type InitialValuesForm = { readTimeoutSeconds: number; }; +export type InitialValuesFormErrors = { + backOfficeURL?: string; + username?: string; + password?: string; + connectTimeoutSeconds?: string; + readTimeoutSeconds?: string; +}; + export type NPEDFieldType = { frontId: string; username: string | undefined;