From 1ffad51503fc71ef97f318a7258120c4a0050189 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 20 Oct 2025 16:17:37 +0100 Subject: [PATCH 01/11] fixed feature to upload sound files --- src/components/SettingForms/Sound/SoundSettingsFields.tsx | 2 +- src/components/SettingForms/Sound/SoundUpload.tsx | 6 ++++++ src/hooks/useSightingFeed.ts | 6 +++++- src/types/types.ts | 1 + src/utils/utils.ts | 4 ++++ 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/SettingForms/Sound/SoundSettingsFields.tsx b/src/components/SettingForms/Sound/SoundSettingsFields.tsx index 0c08b0b..99a8efa 100644 --- a/src/components/SettingForms/Sound/SoundSettingsFields.tsx +++ b/src/components/SettingForms/Sound/SoundSettingsFields.tsx @@ -31,7 +31,7 @@ const SoundSettingsFields = () => { NPEDsoundVolume: state.NPEDsoundVolume, hotlistSoundVolume: state.hotlistSoundVolume, }; - + console.log(updatedValues); dispatch({ type: "UPDATE", payload: updatedValues }); const result = await mutation.mutateAsync({ operation: "INSERT", diff --git a/src/components/SettingForms/Sound/SoundUpload.tsx b/src/components/SettingForms/Sound/SoundUpload.tsx index 17f28eb..7b095ba 100644 --- a/src/components/SettingForms/Sound/SoundUpload.tsx +++ b/src/components/SettingForms/Sound/SoundUpload.tsx @@ -9,6 +9,8 @@ const SoundUpload = () => { const initialValues: SoundUploadValue = { name: "", soundFile: null, + soundFileName: "", + soundUrl: "", }; const handleSubmit = (values: SoundUploadValue) => { @@ -16,6 +18,7 @@ const SoundUpload = () => { toast.warning("Please select an audio file"); } else { dispatch({ type: "ADD", payload: values }); + toast.success("Sound file upload successfully"); } }; @@ -36,7 +39,10 @@ const SoundUpload = () => { className="mt-4 w-full flex flex-col items-center justify-center rounded-2xl border border-slate-800 bg-slate-900/40 p-10 text-center file:px-3 file:border file:border-gray-500 file:rounded-lg file:bg-blue-800 file:mr-5" onChange={(e) => { if (e.target?.files && e.target?.files[0]?.type === "audio/mpeg") { + const url = URL.createObjectURL(e.target.files[0]); + setFieldValue("soundUrl", url); setFieldValue("name", e.target.files[0].name); + setFieldValue("soundFileName", e.target.files[0].name); setFieldValue("soundFile", e.target.files[0]); } else { setFieldError("soundFile", "Not an mp3 file"); diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index d9a386d..8a771f0 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -43,8 +43,12 @@ export function useSightingFeed(url: string | undefined) { }, [audioArmed, latestRef]); const soundSrc = useMemo(() => { + if (state?.sightingSound?.includes(".mp3")) { + const file = state.soundOptions?.find((item) => item.name === state.sightingSound); + return file?.soundUrl; + } return getSoundFileURL(state?.sightingSound) ?? switchSound; - }, [state.sightingSound]); + }, [state.sightingSound, state.soundOptions]); function refetchInterval(query: Query) { if (!query) return; diff --git a/src/types/types.ts b/src/types/types.ts index 37afae9..866b1e3 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -292,6 +292,7 @@ export type SoundUploadValue = { name: string; soundFileName?: string; soundFile?: File | null; + soundUrl?: string; }; export type SoundState = { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 78c7b30..dc164e9 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -21,6 +21,10 @@ export function getSoundFileURL(name: string) { return sounds[name] ?? null; } +export const showSoundURL = (url: URL | string | undefined) => { + console.log(url); +}; + const randomChars = () => { const uppercaseAsciiStart = 65; const letterIndex = Math.floor(Math.random() * 26); From 78905b09e05ffb0d7c9d6b7ff359792211833ef0 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Tue, 21 Oct 2025 12:52:14 +0100 Subject: [PATCH 02/11] - added framework for playing uploaded music files. need to permanently store and retreive files --- .../Sound/SoundSettingsFields.tsx | 4 ++- .../SettingForms/Sound/SoundUpload.tsx | 35 +++++++++++++++---- .../SightingsWidget/SightingWidget.tsx | 12 +++++-- .../providers/SoundContextProvider.tsx | 6 +++- src/context/reducers/SoundContextReducer.ts | 1 + src/hooks/useSightingFeed.ts | 4 +-- src/types/types.ts | 2 ++ 7 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/components/SettingForms/Sound/SoundSettingsFields.tsx b/src/components/SettingForms/Sound/SoundSettingsFields.tsx index 99a8efa..41d039e 100644 --- a/src/components/SettingForms/Sound/SoundSettingsFields.tsx +++ b/src/components/SettingForms/Sound/SoundSettingsFields.tsx @@ -30,9 +30,11 @@ const SoundSettingsFields = () => { sightingVolume: state.sightingVolume, NPEDsoundVolume: state.NPEDsoundVolume, hotlistSoundVolume: state.hotlistSoundVolume, + soundOptions: [...(state.soundOptions ?? [])], }; - console.log(updatedValues); + dispatch({ type: "UPDATE", payload: updatedValues }); + const result = await mutation.mutateAsync({ operation: "INSERT", path: "soundSettings", diff --git a/src/components/SettingForms/Sound/SoundUpload.tsx b/src/components/SettingForms/Sound/SoundUpload.tsx index 7b095ba..c30d389 100644 --- a/src/components/SettingForms/Sound/SoundUpload.tsx +++ b/src/components/SettingForms/Sound/SoundUpload.tsx @@ -3,9 +3,12 @@ import FormGroup from "../components/FormGroup"; import type { SoundUploadValue } from "../../../types/types"; import { useSoundContext } from "../../../context/SoundContext"; import { toast } from "sonner"; +import { useCameraBlackboard } from "../../../hooks/useCameraBlackboard"; const SoundUpload = () => { - const { dispatch } = useSoundContext(); + const { state, dispatch } = useSoundContext(); + const { mutation } = useCameraBlackboard(); + const initialValues: SoundUploadValue = { name: "", soundFile: null, @@ -13,14 +16,34 @@ const SoundUpload = () => { soundUrl: "", }; - const handleSubmit = (values: SoundUploadValue) => { + const handleSubmit = async (values: SoundUploadValue) => { if (!values.soundFile) { toast.warning("Please select an audio file"); - } else { - dispatch({ type: "ADD", payload: values }); - - toast.success("Sound file upload successfully"); + return; } + const alreadyExists = state?.soundOptions?.some((soundOption) => soundOption.name === values.name); + if (state.soundOptions?.includes(values) || alreadyExists) { + toast.warning("Sound already in list"); + return; + } + + const updatedValues = { + ...state, + soundOptions: [...(state.soundOptions ?? []), values], + }; + + const result = await mutation.mutateAsync({ + operation: "INSERT", + path: "soundSettings", + value: updatedValues, + }); + if (result.reason !== "OK") { + toast.error("Cannot update sound settings"); + } else { + toast.success(`${values.name} file added`); + } + + dispatch({ type: "ADD", payload: values }); }; return ( diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 52317b9..4cf0de7 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -43,12 +43,20 @@ export default function SightingHistoryWidget({ className, title }: SightingHist const { state } = useSoundContext(); const soundSrcNped = useMemo(() => { + if (state?.NPEDsound?.includes(".mp3") || state.NPEDsound?.includes(".wav")) { + const file = state.soundOptions?.find((item) => item.name === state.NPEDsound); + return file?.soundUrl ?? popup; + } return getSoundFileURL(state.NPEDsound) ?? popup; - }, [state.NPEDsound]); + }, [state.NPEDsound, state.soundOptions]); const soundSrcHotlist = useMemo(() => { + if (state?.hotlistSound?.includes(".mp3") || state.hotlistSound?.includes(".wav")) { + const file = state.soundOptions?.find((item) => item.name === state.hotlistSound); + return file?.soundUrl ?? notification; + } return getSoundFileURL(state?.hotlistSound) ?? notification; - }, [state?.hotlistSound]); + }, [state.hotlistSound, state.soundOptions]); const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume }); const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume }); diff --git a/src/context/providers/SoundContextProvider.tsx b/src/context/providers/SoundContextProvider.tsx index 37e75ec..f534800 100644 --- a/src/context/providers/SoundContextProvider.tsx +++ b/src/context/providers/SoundContextProvider.tsx @@ -21,7 +21,11 @@ const SoundContextProvider = ({ children }: SoundContextProviderProps) => { path: "soundSettings", }); - dispatch({ type: "UPDATE", payload: result.result }); + if (!result.result || typeof result.result !== "object") { + dispatch({ type: "UPDATE", payload: state }); + } else { + dispatch({ type: "UPDATE", payload: result.result }); + } }; fetchSound(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/context/reducers/SoundContextReducer.ts b/src/context/reducers/SoundContextReducer.ts index 86cac6e..0145094 100644 --- a/src/context/reducers/SoundContextReducer.ts +++ b/src/context/reducers/SoundContextReducer.ts @@ -34,6 +34,7 @@ export function reducer(state: SoundState, action: SoundAction): SoundState { NPEDsoundVolume: action.payload.NPEDsoundVolume, sightingVolume: action.payload.sightingVolume, hotlistSoundVolume: action.payload.hotlistSoundVolume, + soundOptions: action.payload.soundOptions, }; } diff --git a/src/hooks/useSightingFeed.ts b/src/hooks/useSightingFeed.ts index 8a771f0..4c7d88e 100644 --- a/src/hooks/useSightingFeed.ts +++ b/src/hooks/useSightingFeed.ts @@ -43,9 +43,9 @@ export function useSightingFeed(url: string | undefined) { }, [audioArmed, latestRef]); const soundSrc = useMemo(() => { - if (state?.sightingSound?.includes(".mp3")) { + if (state?.sightingSound?.includes(".mp3") || state.sightingSound?.includes(".wav")) { const file = state.soundOptions?.find((item) => item.name === state.sightingSound); - return file?.soundUrl; + return file?.soundUrl ?? switchSound; } return getSoundFileURL(state?.sightingSound) ?? switchSound; }, [state.sightingSound, state.soundOptions]); diff --git a/src/types/types.ts b/src/types/types.ts index 866b1e3..72a2369 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -286,6 +286,7 @@ export type FormValues = { NPEDsound: SoundValue; hotlists: Hotlist[]; hotlistSound: SoundValue; + soundOptions?: SoundUploadValue[]; }; export type SoundUploadValue = { @@ -316,6 +317,7 @@ type UpdateAction = { NPEDsoundVolume: number; hotlistSoundVolume: number; hotlistSound: SoundValue; + soundOptions?: SoundUploadValue[]; }; }; From b58181e551de581325ca54be9bcc2e336d566982 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 22 Oct 2025 08:54:42 +0100 Subject: [PATCH 03/11] - started improvements on session page to include pause and save buttons --- src/components/SessionForm/SessionCard.tsx | 47 ++++++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/components/SessionForm/SessionCard.tsx b/src/components/SessionForm/SessionCard.tsx index a5dc93e..ffdc3aa 100644 --- a/src/components/SessionForm/SessionCard.tsx +++ b/src/components/SessionForm/SessionCard.tsx @@ -3,6 +3,8 @@ import CardHeader from "../UI/CardHeader"; import { useNPEDContext } from "../../context/NPEDUserContext"; import type { ReducedSightingType } from "../../types/types"; import { toast } from "sonner"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFloppyDisk, faPause, faPlay, faStop } from "@fortawesome/free-solid-svg-icons"; const SessionCard = () => { const { sessionStarted, setSessionStarted, sessionList } = useNPEDContext(); @@ -12,19 +14,24 @@ const SessionCard = () => { toast(`${sessionStarted ? "Vehicle tracking session Ended" : "Vehicle tracking session Started"}`); }; + const handleSaveCick = () => { + console.log("clicked"); + }; + const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))]; const dedupedSightings = sightings.map((sighting) => sighting[1]); const vehicles = dedupedSightings.reduce>( (acc, item) => { + const hotlisthit = Object.values(item.metadata?.hotlistMatches ?? {}).includes(true); if (item.metadata?.npedJSON["NPED CATEGORY"] === "A") acc.npedCatA.push(item); if (item.metadata?.npedJSON["NPED CATEGORY"] === "B") acc.npedCatB.push(item); if (item.metadata?.npedJSON["NPED CATEGORY"] === "C") acc.npedCatC.push(item); if (item.metadata?.npedJSON["NPED CATEGORY"] === "D") acc.npedCatD.push(item); if (item.metadata?.npedJSON["TAX STATUS"] === false) acc.notTaxed.push(item); if (item.metadata?.npedJSON["MOT STATUS"] === false) acc.notMOT.push(item); - + if (hotlisthit) acc.hotlistHit.push(item); return acc; }, { @@ -34,6 +41,7 @@ const SessionCard = () => { npedCatD: [], notTaxed: [], notMOT: [], + hotlistHit: [], } ); @@ -47,12 +55,39 @@ const SessionCard = () => { } transition w-full`} onClick={handleStartClick} > - {sessionStarted ? "End Session" : "Start Session"} +
+ +

{sessionStarted ? "End Session" : "Start Session"}

+
+
+ {sessionStarted && ( + + )} + {sessionStarted && ( + + )} +
  • -

    Number of Vehicles:

    +

    Number of Vehicles sightings:

    {dedupedSightings.length}
  • @@ -63,13 +98,17 @@ const SessionCard = () => {

    Vehicles without MOT:

    {" "} {vehicles.notMOT.length}
  • +
  • +

    Vehicles on Hotlists:

    {" "} + {vehicles.hotlistHit.length} +
  • Vehicles with NPED Cat A:

    {vehicles.npedCatA.length}
  • Vehicles with NPED Cat B:

    {" "} - {vehicles.npedCatB.length} + {vehicles.npedCatB.length}
  • Vehicles with NPED Cat C:{" "} From a958901bedea2241fb4d95c3d56257b0aac01e24 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Wed, 22 Oct 2025 12:20:15 +0100 Subject: [PATCH 04/11] - will pick up later --- src/components/SightingsWidget/SightingWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 4cf0de7..4791c66 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -111,7 +111,7 @@ export default function SightingHistoryWidget({ className, title }: SightingHist for (const sighting of rows) { const id = sighting.vrm; - if (processedRefs.current.has(id)) continue; + // if (processedRefs.current.has(id)) continue; const isHot = checkIsHotListHit(sighting); const cat = sighting?.metadata?.npedJSON?.["NPED CATEGORY"]; From 359f3781f21b45c5b79dd7d750ba4ec43a0079eb Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 24 Oct 2025 10:49:04 +0100 Subject: [PATCH 05/11] - refactored to allow for stacking of special hits (NPED + Hotlist) --- .../SightingModal/SightingModal.tsx | 28 +++++---- .../SightingsWidget/SightingWidget.tsx | 61 +++++++++++-------- src/types/types.ts | 8 +++ src/utils/utils.ts | 8 ++- 4 files changed, 64 insertions(+), 41 deletions(-) diff --git a/src/components/SightingModal/SightingModal.tsx b/src/components/SightingModal/SightingModal.tsx index 9e31bfc..9406704 100644 --- a/src/components/SightingModal/SightingModal.tsx +++ b/src/components/SightingModal/SightingModal.tsx @@ -23,8 +23,8 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }: const { dispatch } = useAlertHitContext(); const { query, mutation } = useCameraBlackboard(); - const hotlistName = getHotlistName(sighting?.metadata?.hotlistMatches); - + const hotlistNames = getHotlistName(sighting?.metadata?.hotlistMatches); + console.log(hotlistNames); const handleAcknowledgeButton = () => { try { if (!sighting) { @@ -117,16 +117,6 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
    plate patch - {hotlistName && ( -
    -

    Hotlist

    -
    -

    - {hotlistName ? hotlistName[0].replace(/\.csv$/i, "") : "-"} -

    -
    -
    - )}
    {isHotListHit && hotlistHit} @@ -134,6 +124,20 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }: {isNPEDHitB && hotlistHit} {isNPEDHitC && hotlistHit} + {hotlistNames && ( +
    +

    Hotlists

    +
    + {hotlistNames.map((hotlistName) => ( +
    +

    + {hotlistName ? hotlistName?.replace(/\.csv$/i, "") : "-"} +

    +
    + ))} +
    +
    + )}
    ([]); useNow(1000); const { state } = useSoundContext(); @@ -78,6 +79,14 @@ export default function SightingHistoryWidget({ className, title }: SightingHist const hasAutoOpenedRef = useRef(false); const npedRef = useRef(false); + const enqueue = useCallback((sighting: SightingType, kind: HitKind) => { + const id = sighting.vrm ?? sighting.ref; + if (processedRefs.current.has(id)) return; + processedRefs.current.add(id); + + setModalQueue((q) => [...q, { id, sighting, kind }]); + }, []); + const reduceObject = (obj: SightingType): ReducedSightingType => { return { vrm: obj.vrm, @@ -111,27 +120,16 @@ export default function SightingHistoryWidget({ className, title }: SightingHist for (const sighting of rows) { const id = sighting.vrm; - // if (processedRefs.current.has(id)) continue; - const isHot = checkIsHotListHit(sighting); - const cat = sighting?.metadata?.npedJSON?.["NPED CATEGORY"]; + if (processedRefs.current.has(id)) continue; + const isHotlistHit = checkIsHotListHit(sighting); + const npedcategory = sighting?.metadata?.npedJSON?.["NPED CATEGORY"]; + const isNPED = npedcategory === "A" || npedcategory === "B" || npedcategory === "C"; - if (cat === "A" || cat === "B" || cat === "C") { - npedSound(); - setSelectedSighting(sighting); - setSightingModalOpen(true); - processedRefs.current.add(id); - break; // stop after one new open per render cycle - } - - if (isHot) { - hotlistsound(); - setSelectedSighting(sighting); - setSightingModalOpen(true); - processedRefs.current.add(id); - break; + if (isNPED || isHotlistHit) { + enqueue(sighting, isNPED ? "NPED" : "HOTLIST"); // enqueue ONLY } } - }, [rows, hotlistsound, npedSound, setSightingModalOpen, setSelectedSighting]); + }, [rows, enqueue]); useEffect(() => { rows?.forEach((obj) => { @@ -164,22 +162,33 @@ export default function SightingHistoryWidget({ className, title }: SightingHist }); if (firstNPED) { - setSelectedSighting(firstNPED); - npedSound(); - setSightingModalOpen(true); + enqueue(firstNPED, "NPED"); + npedRef.current = true; } if (firstHot) { - setSelectedSighting(firstHot); - hotlistsound(); - setSightingModalOpen(true); + enqueue(firstHot, "HOTLIST"); + hasAutoOpenedRef.current = true; } - }, [hotlistsound, npedSound, setSelectedSighting]); + }, [enqueue, hotlistsound, npedSound, rows, setSelectedSighting, setSightingModalOpen]); + + useEffect(() => { + if (!isSightingModalOpen && modalQueue.length > 0) { + const next = modalQueue[0]; + + if (next.kind === "NPED") npedSound(); + else hotlistsound(); + + setSelectedSighting(next.sighting); + setSightingModalOpen(true); + } + }, [isSightingModalOpen, npedSound, hotlistsound, setSelectedSighting, setSightingModalOpen, modalQueue]); const handleClose = () => { setSightingModalOpen(false); + setModalQueue((q) => q.slice(1)); }; return ( <> diff --git a/src/types/types.ts b/src/types/types.ts index 72a2369..504ae90 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -371,3 +371,11 @@ export type ModemSettingsType = { password: string; authenticationType: string; }; + +export type HitKind = "NPED" | "HOTLIST"; + +export type QueuedHit = { + id: number | string; + sighting: SightingType; + kind: HitKind; +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index dc164e9..537853e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -147,10 +147,12 @@ export const checkIsHotListHit = (sigthing: SightingType | null) => { }; export function getHotlistName(obj: HotlistMatches | undefined) { - if (!obj || Object.values(obj).includes(false)) return; + if (!obj) return; - const keys = Object.keys(obj); - return keys; + const hotlistNames = Object.entries(obj) + .filter(([, value]) => value === true) + .map(([key]) => key); + return hotlistNames; } export const getNPEDCategory = (r?: SightingType | null) => From 79036338090119179be249b3e4c9d3a815b7fd00 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 24 Oct 2025 10:55:05 +0100 Subject: [PATCH 06/11] - removed console.log --- src/components/SightingModal/SightingModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SightingModal/SightingModal.tsx b/src/components/SightingModal/SightingModal.tsx index 9406704..668f202 100644 --- a/src/components/SightingModal/SightingModal.tsx +++ b/src/components/SightingModal/SightingModal.tsx @@ -24,7 +24,6 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }: const { query, mutation } = useCameraBlackboard(); const hotlistNames = getHotlistName(sighting?.metadata?.hotlistMatches); - console.log(hotlistNames); const handleAcknowledgeButton = () => { try { if (!sighting) { From c83122cd52ecc1a5bbb96639b1c57a6e4911034d Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Fri, 24 Oct 2025 12:10:10 +0100 Subject: [PATCH 07/11] - added session sighting component - add new session paused state and stop adding to session when true --- src/components/SessionForm/SessionCard.tsx | 80 +++++++++++-------- .../SightingsWidget/SightingWidget.tsx | 3 +- src/components/UI/Header.tsx | 27 +++---- src/components/UI/VehicleSessionItem.tsx | 18 +++++ src/context/NPEDUserContext.ts | 9 +-- .../providers/NPEDUserContextProvider.tsx | 3 + 6 files changed, 84 insertions(+), 56 deletions(-) create mode 100644 src/components/UI/VehicleSessionItem.tsx diff --git a/src/components/SessionForm/SessionCard.tsx b/src/components/SessionForm/SessionCard.tsx index ffdc3aa..588bc07 100644 --- a/src/components/SessionForm/SessionCard.tsx +++ b/src/components/SessionForm/SessionCard.tsx @@ -5,19 +5,26 @@ import type { ReducedSightingType } from "../../types/types"; import { toast } from "sonner"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFloppyDisk, faPause, faPlay, faStop } from "@fortawesome/free-solid-svg-icons"; +import VehicleSessionItem from "../UI/VehicleSessionItem"; const SessionCard = () => { - const { sessionStarted, setSessionStarted, sessionList } = useNPEDContext(); + const { sessionStarted, setSessionStarted, sessionList, setSessionPaused, sessionPaused } = useNPEDContext(); const handleStartClick = () => { setSessionStarted(!sessionStarted); - toast(`${sessionStarted ? "Vehicle tracking session Ended" : "Vehicle tracking session Started"}`); + setSessionPaused(false); + toast(`${sessionStarted ? "Vehicle tracking session ended" : "Vehicle tracking session started"}`); }; const handleSaveCick = () => { console.log("clicked"); }; + const handlepauseClick = () => { + setSessionPaused(!sessionPaused); + toast(`${sessionStarted ? "Vehicle tracking session paused" : "Vehicle tracking session resumed"}`); + }; + const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))]; const dedupedSightings = sightings.map((sighting) => sighting[1]); @@ -75,45 +82,52 @@ const SessionCard = () => { {sessionStarted && ( )}
      -
    • -

      Number of Vehicles sightings:

      - {dedupedSightings.length} -
    • -
    • -

      Vehicles without Tax:

      - {vehicles.notTaxed.length} -
    • -
    • -

      Vehicles without MOT:

      {" "} - {vehicles.notMOT.length} -
    • -
    • -

      Vehicles on Hotlists:

      {" "} - {vehicles.hotlistHit.length} -
    • -
    • -

      Vehicles with NPED Cat A:

      - {vehicles.npedCatA.length} -
    • -
    • -

      Vehicles with NPED Cat B:

      {" "} - {vehicles.npedCatB.length} -
    • -
    • - Vehicles with NPED Cat C:{" "} - {vehicles.npedCatC.length} -
    • + + + + + + +
    diff --git a/src/components/SightingsWidget/SightingWidget.tsx b/src/components/SightingsWidget/SightingWidget.tsx index 4cf0de7..28c2182 100644 --- a/src/components/SightingsWidget/SightingWidget.tsx +++ b/src/components/SightingsWidget/SightingWidget.tsx @@ -71,7 +71,7 @@ export default function SightingHistoryWidget({ className, title }: SightingHist } = useSightingFeedContext(); const { dispatch } = useAlertHitContext(); - const { sessionStarted, setSessionList, sessionList } = useNPEDContext(); + const { sessionStarted, setSessionList, sessionList, sessionPaused } = useNPEDContext(); const processedRefs = useRef>(new Set()); @@ -88,6 +88,7 @@ export default function SightingHistoryWidget({ className, title }: SightingHist useEffect(() => { if (sessionStarted) { if (!mostRecent) return; + if (sessionPaused) return; const reducedMostRecent = reduceObject(mostRecent); setSessionList([...sessionList, reducedMostRecent]); } diff --git a/src/components/UI/Header.tsx b/src/components/UI/Header.tsx index b48c938..6fdf67a 100644 --- a/src/components/UI/Header.tsx +++ b/src/components/UI/Header.tsx @@ -1,21 +1,14 @@ import { Link } from "react-router"; import Logo from "/MAV.svg"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faGear, - faHome, - faListCheck, - faMaximize, - faMinimize, - faRotate, -} from "@fortawesome/free-solid-svg-icons"; +import { faGear, faHome, faListCheck, faMaximize, faMinimize, faRotate } from "@fortawesome/free-solid-svg-icons"; import { useState } from "react"; import SoundBtn from "./SoundBtn"; import { useNPEDContext } from "../../context/NPEDUserContext"; export default function Header() { const [isFullscreen, setIsFullscreen] = useState(false); - const { sessionStarted } = useNPEDContext(); + const { sessionStarted, sessionPaused } = useNPEDContext(); const toggleFullscreen = () => { if (!document.fullscreenElement) { @@ -39,9 +32,13 @@ export default function Header() {
    - {sessionStarted && ( -
    Session Active
    - )} +
    + {sessionStarted && sessionPaused ? ( +

    Session Paused

    + ) : ( + sessionStarted &&

    Session Active

    + )} +
    @@ -59,11 +56,7 @@ export default function Header() {
    - + diff --git a/src/components/UI/VehicleSessionItem.tsx b/src/components/UI/VehicleSessionItem.tsx new file mode 100644 index 0000000..a0e8ea2 --- /dev/null +++ b/src/components/UI/VehicleSessionItem.tsx @@ -0,0 +1,18 @@ +import clsx from "clsx"; + +type VehicleSessionItemProps = { + sessionNumber: number; + textColour: string; + vehicleTag: string; +}; + +const VehicleSessionItem = ({ sessionNumber, textColour, vehicleTag }: VehicleSessionItemProps) => { + return ( +
  • +

    {vehicleTag}

    + {sessionNumber} +
  • + ); +}; + +export default VehicleSessionItem; diff --git a/src/context/NPEDUserContext.ts b/src/context/NPEDUserContext.ts index 7b36b9e..f776379 100644 --- a/src/context/NPEDUserContext.ts +++ b/src/context/NPEDUserContext.ts @@ -5,17 +5,16 @@ type UserContextValue = { user: NPEDUser | null; setUser: React.Dispatch>; sessionStarted: boolean; + sessionPaused: boolean; + setSessionPaused: React.Dispatch>; setSessionStarted: React.Dispatch>; sessionList: ReducedSightingType[]; setSessionList: React.Dispatch>; }; -export const NPEDUserContext = createContext( - undefined -); +export const NPEDUserContext = createContext(undefined); export const useNPEDContext = () => { const ctx = useContext(NPEDUserContext); - if (!ctx) - throw new Error("useNPEDContext must be used within "); + if (!ctx) throw new Error("useNPEDContext must be used within "); return ctx; }; diff --git a/src/context/providers/NPEDUserContextProvider.tsx b/src/context/providers/NPEDUserContextProvider.tsx index e76247f..6241b8e 100644 --- a/src/context/providers/NPEDUserContextProvider.tsx +++ b/src/context/providers/NPEDUserContextProvider.tsx @@ -10,6 +10,7 @@ export const NPEDUserProvider = ({ children }: NPEDUserProviderType) => { const [user, setUser] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); const [sessionList, setSessionList] = useState([]); + const [sessionPaused, setSessionPaused] = useState(false); return ( { sessionStarted, sessionList, setSessionList, + sessionPaused, + setSessionPaused, }} > {children} From 18534ceb2c9ba9c65cb1ab8bbe38f0bac69d5356 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 27 Oct 2025 08:28:44 +0000 Subject: [PATCH 08/11] - added functionality to save sighting sessions to black board --- src/components/SessionForm/SessionCard.tsx | 47 ++++++++++++------- src/context/NPEDUserContext.ts | 4 +- .../providers/NPEDUserContextProvider.tsx | 23 ++++++++- src/context/reducers/NPEDContextReducer.ts | 20 ++++++++ src/types/types.ts | 14 ++++++ 5 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 src/context/reducers/NPEDContextReducer.ts diff --git a/src/components/SessionForm/SessionCard.tsx b/src/components/SessionForm/SessionCard.tsx index 588bc07..42cdd7f 100644 --- a/src/components/SessionForm/SessionCard.tsx +++ b/src/components/SessionForm/SessionCard.tsx @@ -6,30 +6,18 @@ import { toast } from "sonner"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFloppyDisk, faPause, faPlay, faStop } from "@fortawesome/free-solid-svg-icons"; import VehicleSessionItem from "../UI/VehicleSessionItem"; +import { useCameraBlackboard } from "../../hooks/useCameraBlackboard"; const SessionCard = () => { - const { sessionStarted, setSessionStarted, sessionList, setSessionPaused, sessionPaused } = useNPEDContext(); - - const handleStartClick = () => { - setSessionStarted(!sessionStarted); - setSessionPaused(false); - toast(`${sessionStarted ? "Vehicle tracking session ended" : "Vehicle tracking session started"}`); - }; - - const handleSaveCick = () => { - console.log("clicked"); - }; - - const handlepauseClick = () => { - setSessionPaused(!sessionPaused); - toast(`${sessionStarted ? "Vehicle tracking session paused" : "Vehicle tracking session resumed"}`); - }; + const { sessionStarted, setSessionStarted, sessionList, setSessionPaused, sessionPaused, savedSightings } = + useNPEDContext(); + const { mutation } = useCameraBlackboard(); const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))]; const dedupedSightings = sightings.map((sighting) => sighting[1]); - const vehicles = dedupedSightings.reduce>( + const vehicles = savedSightings.reduce>( (acc, item) => { const hotlisthit = Object.values(item.metadata?.hotlistMatches ?? {}).includes(true); if (item.metadata?.npedJSON["NPED CATEGORY"] === "A") acc.npedCatA.push(item); @@ -39,6 +27,7 @@ const SessionCard = () => { if (item.metadata?.npedJSON["TAX STATUS"] === false) acc.notTaxed.push(item); if (item.metadata?.npedJSON["MOT STATUS"] === false) acc.notMOT.push(item); if (hotlisthit) acc.hotlistHit.push(item); + acc.vehicles.push(item); return acc; }, { @@ -49,9 +38,31 @@ const SessionCard = () => { notTaxed: [], notMOT: [], hotlistHit: [], + vehicles: [], } ); + const handleStartClick = () => { + setSessionStarted(!sessionStarted); + setSessionPaused(false); + toast(`${sessionStarted ? "Vehicle tracking session ended" : "Vehicle tracking session started"}`); + }; + + const handlepauseClick = () => { + setSessionPaused(!sessionPaused); + toast(`${sessionStarted ? "Vehicle tracking session paused" : "Vehicle tracking session resumed"}`); + }; + + const handleSaveCick = async () => { + const result = await mutation.mutateAsync({ + operation: "INSERT", + path: "sessionStats", + value: dedupedSightings, + }); + + if (result.reason === "OK") toast.success("Session saved"); + }; + return ( @@ -94,7 +105,7 @@ const SessionCard = () => {
      diff --git a/src/context/NPEDUserContext.ts b/src/context/NPEDUserContext.ts index f776379..d4fba95 100644 --- a/src/context/NPEDUserContext.ts +++ b/src/context/NPEDUserContext.ts @@ -1,5 +1,5 @@ import { createContext, useContext, type SetStateAction } from "react"; -import type { NPEDUser, ReducedSightingType } from "../types/types"; +import type { DedupedSightings, NPEDUser, ReducedSightingType } from "../types/types"; type UserContextValue = { user: NPEDUser | null; @@ -10,6 +10,8 @@ type UserContextValue = { setSessionStarted: React.Dispatch>; sessionList: ReducedSightingType[]; setSessionList: React.Dispatch>; + savedSightings: DedupedSightings; + setSavedSightings: React.Dispatch>; }; export const NPEDUserContext = createContext(undefined); diff --git a/src/context/providers/NPEDUserContextProvider.tsx b/src/context/providers/NPEDUserContextProvider.tsx index 6241b8e..17b0d69 100644 --- a/src/context/providers/NPEDUserContextProvider.tsx +++ b/src/context/providers/NPEDUserContextProvider.tsx @@ -1,17 +1,34 @@ -import { useState, type ReactNode } from "react"; -import type { NPEDUser, ReducedSightingType } from "../../types/types"; +import { useEffect, useReducer, useState, type ReactNode } from "react"; +import type { DedupedSightings, NPEDUser, ReducedSightingType } from "../../types/types"; import { NPEDUserContext } from "../NPEDUserContext"; +import { useCameraBlackboard } from "../../hooks/useCameraBlackboard"; +import { initialState, reducer } from "../reducers/NPEDContextReducer"; type NPEDUserProviderType = { children: ReactNode; }; export const NPEDUserProvider = ({ children }: NPEDUserProviderType) => { + const [state, dispatch] = useReducer(reducer, initialState); + const { mutation } = useCameraBlackboard(); const [user, setUser] = useState(null); const [sessionStarted, setSessionStarted] = useState(false); const [sessionList, setSessionList] = useState([]); const [sessionPaused, setSessionPaused] = useState(false); + const [savedSightings, setSavedSightings] = useState([]); + useEffect(() => { + const fetchData = async () => { + const result = await mutation.mutateAsync({ + operation: "VIEW", + path: "sessionStats", + }); + if (!result.result) return; + setSavedSightings(result?.result); + }; + fetchData(); + }, []); + console.log(savedSightings); return ( { setSessionList, sessionPaused, setSessionPaused, + savedSightings, + setSavedSightings, }} > {children} diff --git a/src/context/reducers/NPEDContextReducer.ts b/src/context/reducers/NPEDContextReducer.ts new file mode 100644 index 0000000..4349bbc --- /dev/null +++ b/src/context/reducers/NPEDContextReducer.ts @@ -0,0 +1,20 @@ +import type { NPEDACTION, NPEDSTATE } from "../../types/types"; + +export const initialState = { + sessionStarted: false, + sessionList: [], + sessionPaused: false, + savedSightings: [], +}; + +export function reducer(state: NPEDSTATE, action: NPEDACTION) { + switch (action.type) { + case "SESSION": + return { + ...state, + sessionStarted: action.payload, + }; + default: + return { ...state }; + } +} diff --git a/src/types/types.ts b/src/types/types.ts index 504ae90..ef5e406 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -379,3 +379,17 @@ export type QueuedHit = { sighting: SightingType; kind: HitKind; }; + +export type DedupedSightings = ReducedSightingType[]; + +export type NPEDSTATE = { + sessionStarted: boolean; + sessionList: ReducedSightingType[]; + sessionPaused: boolean; + savedSightings: DedupedSightings; +}; + +export type NPEDACTION = { + type: string; + payload: any; +}; From 251a2f5e7b3c45210bc235f86e16def21b4b6c26 Mon Sep 17 00:00:00 2001 From: Toba Ojo Date: Mon, 27 Oct 2025 09:35:59 +0000 Subject: [PATCH 09/11] - refactored NPED Login & logout --- src/App.tsx | 6 ++-- src/components/SessionForm/SessionCard.tsx | 4 +-- .../SettingForms/NPED/NPEDFields.tsx | 33 +++++++------------ .../SightingsWidget/SightingWidget.tsx | 6 ++-- src/components/UI/Header.tsx | 4 +-- src/context/IntegrationsContext.ts | 22 +++++++++++++ src/context/NPEDUserContext.ts | 22 ------------- ...er.tsx => IntegrationsContextProvider.tsx} | 21 ++++++------ ...ducer.ts => IntegrationsContextReducer.ts} | 13 +++++++- src/hooks/useNPEDAuth.ts | 20 +++++------ src/types/types.ts | 1 + 11 files changed, 76 insertions(+), 76 deletions(-) create mode 100644 src/context/IntegrationsContext.ts delete mode 100644 src/context/NPEDUserContext.ts rename src/context/providers/{NPEDUserContextProvider.tsx => IntegrationsContextProvider.tsx} (68%) rename src/context/reducers/{NPEDContextReducer.ts => IntegrationsContextReducer.ts} (63%) diff --git a/src/App.tsx b/src/App.tsx index 3d9f929..980e322 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import FrontCamera from "./pages/FrontCamera"; import RearCamera from "./pages/RearCamera"; import SystemSettings from "./pages/SystemSettings"; import Session from "./pages/Session"; -import { NPEDUserProvider } from "./context/providers/NPEDUserContextProvider"; +import { IntegrationsProvider } from "./context/providers/IntegrationsContextProvider"; import { AlertHitProvider } from "./context/providers/AlertHitProvider"; import { SoundProvider } from "react-sounds"; import SoundContextProvider from "./context/providers/SoundContextProvider"; @@ -14,7 +14,7 @@ function App() { return ( - + }> @@ -27,7 +27,7 @@ function App() { - + ); diff --git a/src/components/SessionForm/SessionCard.tsx b/src/components/SessionForm/SessionCard.tsx index 42cdd7f..1341ca4 100644 --- a/src/components/SessionForm/SessionCard.tsx +++ b/src/components/SessionForm/SessionCard.tsx @@ -1,6 +1,6 @@ import Card from "../UI/Card"; import CardHeader from "../UI/CardHeader"; -import { useNPEDContext } from "../../context/NPEDUserContext"; +import { useIntegrationsContext } from "../../context/IntegrationsContext"; import type { ReducedSightingType } from "../../types/types"; import { toast } from "sonner"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -10,7 +10,7 @@ import { useCameraBlackboard } from "../../hooks/useCameraBlackboard"; const SessionCard = () => { const { sessionStarted, setSessionStarted, sessionList, setSessionPaused, sessionPaused, savedSightings } = - useNPEDContext(); + useIntegrationsContext(); const { mutation } = useCameraBlackboard(); const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))]; diff --git a/src/components/SettingForms/NPED/NPEDFields.tsx b/src/components/SettingForms/NPED/NPEDFields.tsx index 3e42d24..795c074 100644 --- a/src/components/SettingForms/NPED/NPEDFields.tsx +++ b/src/components/SettingForms/NPED/NPEDFields.tsx @@ -6,16 +6,18 @@ import { toast } from "sonner"; import { faEyeSlash, faEye } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useState } from "react"; +import { useIntegrationsContext } from "../../../context/IntegrationsContext"; const NPEDFields = () => { + const { state } = useIntegrationsContext(); const [showPwd, setShowPwd] = useState(false); - const { signIn, user, signOut } = useNPEDAuth(); + const { signIn, signOut } = useNPEDAuth(); - const initialValues = user + const initialValues = state.npedUser ? { - username: user?.propUsername?.value, - password: user?.propPassword?.value, - clientId: user?.propClientID?.value, + username: state.npedUser?.propUsername?.value, + password: state.npedUser?.propPassword?.value, + clientId: state.npedUser?.propClientID?.value, frontId: "NPED", rearId: "NPED", } @@ -48,20 +50,13 @@ const NPEDFields = () => { }; return ( - + {({ errors, touched, isSubmitting }) => (
      {touched.username && errors.username && ( - - {errors.username} - + {errors.username} )} { className="p-2 border border-gray-400 rounded-lg w-full" /> {touched.password && errors.password && ( - - {errors.password} - + {errors.password} )} { {touched.clientId && errors.clientId && ( - - {errors.clientId} - + {errors.clientId} )} { className="p-1.5 border border-gray-400 rounded-lg" /> - {!user?.propClientID?.value ? ( + {!state.npedUser?.propClientID?.value ? (