feat: Add support for NPED Category D across components

This commit is contained in:
2025-12-15 13:17:43 +00:00
parent 5dc1d13e25
commit 696a36ba92
10 changed files with 193 additions and 92 deletions

11
public/NPED_Cat_D.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -8,6 +8,7 @@ import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_A from "/NPED_Cat_A.svg";
import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg";
import NPED_CAT_C from "/NPED_Cat_C.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg";
import NPED_CAT_D from "/NPED_Cat_D.svg";
import { checkIsHotListHit, formatAge, getNPEDCategory } from "../../utils/utils"; import { checkIsHotListHit, formatAge, getNPEDCategory } from "../../utils/utils";
import { faX } from "@fortawesome/free-solid-svg-icons"; import { faX } from "@fortawesome/free-solid-svg-icons";
import { faClock } from "@fortawesome/free-regular-svg-icons"; import { faClock } from "@fortawesome/free-regular-svg-icons";
@@ -30,6 +31,7 @@ const AlertItem = ({ item }: AlertItemProps) => {
const isNPEDHitA = cat === "A"; const isNPEDHitA = cat === "A";
const isNPEDHitB = cat === "B"; const isNPEDHitB = cat === "B";
const isNPEDHitC = cat === "C"; const isNPEDHitC = cat === "C";
const isNPEDHitD = cat === "D";
const handleClick = () => { const handleClick = () => {
setIsModalOpen(true); setIsModalOpen(true);
@@ -69,6 +71,7 @@ const AlertItem = ({ item }: AlertItemProps) => {
{isNPEDHitA && <img src={NPED_CAT_A} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />} {isNPEDHitA && <img src={NPED_CAT_A} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
{isNPEDHitB && <img src={NPED_CAT_B} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />} {isNPEDHitB && <img src={NPED_CAT_B} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
{isNPEDHitC && <img src={NPED_CAT_C} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />} {isNPEDHitC && <img src={NPED_CAT_C} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
{isNPEDHitD && <img src={NPED_CAT_D} alt="NPEDHITicon" className="h-20 object-contain rounded-md" />}
<div className={`border p-1 hidden md:block`}> <div className={`border p-1 hidden md:block`}>
<img src={item?.plateUrlColour} height={48} width={200} alt="colour patch" /> <img src={item?.plateUrlColour} height={48} width={200} alt="colour patch" />
</div> </div>

View File

@@ -31,9 +31,7 @@ const HistoryList = () => {
<div className="flex flex-col gap-1 px-2"> <div className="flex flex-col gap-1 px-2">
{state?.alertList?.length > 0 ? ( {state?.alertList?.length > 0 ? (
<div className="mt-3 grid grid-cols-1 gap-3"> <div className="mt-3 grid grid-cols-1 gap-3">
{state?.alertList?.map((alertItem) => ( {state?.alertList?.map((alertItem) => <AlertItem item={alertItem} key={alertItem.vrm} />).reverse()}
<AlertItem item={alertItem} key={alertItem.vrm} />
))}
</div> </div>
) : ( ) : (
<div className="mt-4 flex flex-col items-center justify-center rounded-2xl border border-slate-800 bg-slate-900/40 p-10 text-center"> <div className="mt-4 flex flex-col items-center justify-center rounded-2xl border border-slate-800 bg-slate-900/40 p-10 text-center">

View File

@@ -10,6 +10,7 @@ import { toast } from "sonner";
const NPEDCategoryPopup = () => { const NPEDCategoryPopup = () => {
const { state, dispatch } = useIntegrationsContext(); const { state, dispatch } = useIntegrationsContext();
const { mutation } = useCameraBlackboard(); const { mutation } = useCameraBlackboard();
const isCatAEnabled = state?.iscatEnabled?.catA; const isCatAEnabled = state?.iscatEnabled?.catA;
@@ -34,8 +35,8 @@ const NPEDCategoryPopup = () => {
await mutation.mutateAsync({ await mutation.mutateAsync({
operation: "SAVE", operation: "SAVE",
path: "", path: "",
value: null value: null,
}) });
if (result?.reason === "OK") toast.success("Pop up settings saved"); if (result?.reason === "OK") toast.success("Pop up settings saved");
dispatch({ type: "NPEDCATENABLED", payload: values }); dispatch({ type: "NPEDCATENABLED", payload: values });
}; };
@@ -44,7 +45,7 @@ const NPEDCategoryPopup = () => {
<Card className="p-4"> <Card className="p-4">
<CardHeader title={"Alert Pop ups"} /> <CardHeader title={"Alert Pop ups"} />
<p className="italic my-2">Allows alerts to pop up to user.</p> <p className="italic my-2">Allows alerts to pop up to user.</p>
<Formik initialValues={initialValues} onSubmit={handleSubmit}> <Formik initialValues={initialValues} onSubmit={handleSubmit} enableReinitialize>
<Form className="flex flex-col space-y-5 px-2"> <Form className="flex flex-col space-y-5 px-2">
<FormGroup> <FormGroup>
<NPEDCatToggle name={"catA"} label="NPED Category A" /> <NPEDCatToggle name={"catA"} label="NPED Category A" />

View File

@@ -172,6 +172,11 @@ const SessionCard = () => {
textColour="text-gray-300" textColour="text-gray-300"
vehicleTag={"Vehicles with NPED Cat C:"} vehicleTag={"Vehicles with NPED Cat C:"}
/> />
<VehicleSessionItem
sessionNumber={vehicles.npedCatD.length}
textColour="text-gray-300"
vehicleTag={"Vehicles with NPED Cat D:"}
/>
</ul> </ul>
</div> </div>
</Card> </Card>

View File

@@ -10,7 +10,8 @@ import HotListImg from "/Hotlist_Hit.svg";
import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_A from "/NPED_Cat_A.svg";
import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg";
import NPED_CAT_C from "/NPED_Cat_C.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg";
import { checkIsHotListHit, getHotlistName, getNPEDCategory } from "../../utils/utils"; import NPED_CAT_D from "/NPED_Cat_D.svg";
import { checkIsHotListHit, getHotlistName, getNPEDCategory, getNPEDReason } from "../../utils/utils";
type SightingModalProps = { type SightingModalProps = {
isSightingModalOpen: boolean; isSightingModalOpen: boolean;
@@ -74,11 +75,19 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
}; };
const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY"; const motionAway = (sighting?.motion ?? "").toUpperCase() === "AWAY";
const isHotListHit = checkIsHotListHit(sighting); const isHotListHit = checkIsHotListHit(sighting);
const cat = getNPEDCategory(sighting); const cat = getNPEDCategory(sighting);
const isNPEDHitA = cat === "A"; const isNPEDHitA = cat === "A";
const isNPEDHitB = cat === "B"; const isNPEDHitB = cat === "B";
const isNPEDHitC = cat === "C"; const isNPEDHitC = cat === "C";
const isNPEDHitD = cat === "D";
const reason = getNPEDReason(sighting);
const insuranceStatus = reason ? reason["INSURANCE STATUS"] : null;
const motStatus = reason ? reason["MOT STATUS"] : null;
const taxStatus = reason ? reason["TAX STATUS"] : null;
return ( return (
<> <>
@@ -133,6 +142,7 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
{isNPEDHitA && <img src={NPED_CAT_A} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isNPEDHitA && <img src={NPED_CAT_A} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitB && <img src={NPED_CAT_B} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isNPEDHitB && <img src={NPED_CAT_B} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitC && <img src={NPED_CAT_C} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isNPEDHitC && <img src={NPED_CAT_C} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitD && <img src={NPED_CAT_D} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
</div> </div>
{hotlistNames && hotlistNames.length > 0 && ( {hotlistNames && hotlistNames.length > 0 && (
<div className="flex flex-col border-b border-gray-600 mb-4"> <div className="flex flex-col border-b border-gray-600 mb-4">
@@ -158,10 +168,61 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
<h3 className="text-base md:text-lg font-semibold pb-2 border-b border-gray-700">Vehicle Info</h3> <h3 className="text-base md:text-lg font-semibold pb-2 border-b border-gray-700">Vehicle Info</h3>
<dl className="mt-3 gap-x-4 gap-y-2 text-sm"> <dl className="mt-3 gap-x-4 gap-y-2 text-sm">
<div> <div>
<dt className="text-gray-300">VRM</dt> <dd className="font-medium text-2xl break-all font-mono tracking-wide">{sighting?.vrm ?? "-"}</dd>
<dd className="font-medium text-2xl break-all">{sighting?.vrm ?? "-"}</dd>
</div> </div>
{isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD ? (
<>
<div className="">
<dt className="text-gray-300 text-xl">Insurance</dt>
<dd className="font-medium text-2xl flex">
{insuranceStatus ? (
<span
className={`text-green-600 bg-green-300 w-[30%] px-2 rounded-lg border border-green-900`}
>
YES
</span>
) : (
<span className={`text-red-300 bg-red-600 w-[30%] px-2 rounded-lg border border-red-900`}>
NO
</span>
)}
</dd>
</div>
<div>
<dt className="text-gray-300 text-3xl">MOT</dt>
<dd className="font-medium text-2xl">
{motStatus ? (
<span
className={`text-green-700 bg-green-400 w-[30%] px-2 rounded-lg border border-green-900`}
>
VALID
</span>
) : (
<span className={`text-red-600 bg-red-300 w-[30%] px-2 rounded-lg border border-red-900`}>
EXPIRED
</span>
)}
</dd>
</div>
<div>
<dt className="text-gray-300 text-3xl">Tax</dt>
<dd className="font-medium text-2xl">
{taxStatus ? (
<span
className={`text-green-700 bg-green-400 w-[30%] px-2 rounded-lg border border-green-900`}
>
VALID
</span>
) : (
<span className={`text-red-600 bg-red-300 w-[30%] px-2 rounded-lg border border-red-900`}>
EXPIRED
</span>
)}
</dd>
</div>
</>
) : (
<>
<div> <div>
<dt className="text-gray-300">Motion</dt> <dt className="text-gray-300">Motion</dt>
<dd className="font-medium text-2xl ">{sighting?.motion ?? "-"}</dd> <dd className="font-medium text-2xl ">{sighting?.motion ?? "-"}</dd>
@@ -196,6 +257,8 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
<dt className="text-gray-300">Time</dt> <dt className="text-gray-300">Time</dt>
<dd className="font-medium text-xl">{sighting?.timeStamp ?? "-"}</dd> <dd className="font-medium text-xl">{sighting?.timeStamp ?? "-"}</dd>
</div> </div>
</>
)}
</dl> </dl>
</aside> </aside>
</div> </div>

View File

@@ -12,14 +12,10 @@ import HotListImg from "/Hotlist_Hit.svg";
import NPED_CAT_A from "/NPED_Cat_A.svg"; import NPED_CAT_A from "/NPED_Cat_A.svg";
import NPED_CAT_B from "/NPED_Cat_B.svg"; import NPED_CAT_B from "/NPED_Cat_B.svg";
import NPED_CAT_C from "/NPED_Cat_C.svg"; import NPED_CAT_C from "/NPED_Cat_C.svg";
import popup from "../../assets/sounds/ui/popup_open.mp3"; import NPED_CAT_D from "/NPED_Cat_D.svg";
import notification from "../../assets/sounds/ui/notification.mp3";
import { useSound } from "react-sounds";
import { useIntegrationsContext } from "../../context/IntegrationsContext"; import { useIntegrationsContext } from "../../context/IntegrationsContext";
import { useSoundContext } from "../../context/SoundContext";
import Loading from "../UI/Loading"; import Loading from "../UI/Loading";
import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils"; import { checkIsHotListHit, getNPEDCategory } from "../../utils/utils";
import { useCachedSoundSrc } from "../../hooks/usecachedSoundSrc";
function useNow(tickMs = 1000) { function useNow(tickMs = 1000) {
const [, setNow] = useState(() => Date.now()); const [, setNow] = useState(() => Date.now());
@@ -41,14 +37,9 @@ type SightingHistoryProps = {
export default function SightingHistoryWidget({ className, title }: SightingHistoryProps) { export default function SightingHistoryWidget({ className, title }: SightingHistoryProps) {
const [modalQueue, setModalQueue] = useState<QueuedHit[]>([]); const [modalQueue, setModalQueue] = useState<QueuedHit[]>([]);
useNow(1000); useNow(1000);
const { state } = useSoundContext();
const { src: soundSrcHotlist } = useCachedSoundSrc(state?.hotlistSound, state?.soundOptions, notification);
const { src: soundSrcNped } = useCachedSoundSrc(state?.NPEDsound, state?.soundOptions, popup);
const { play: npedSound } = useSound(soundSrcNped, { volume: state.NPEDsoundVolume });
const { play: hotlistsound } = useSound(soundSrcHotlist, { volume: state.hotlistSoundVolume });
const { const {
sightings, sightings,
setSelectedSighting, setSelectedSighting,
@@ -73,7 +64,10 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
const isCatCEnabled = integrationState?.iscatEnabled?.catC; const isCatCEnabled = integrationState?.iscatEnabled?.catC;
const isCatDEnabled = integrationState?.iscatEnabled?.catD; const isCatDEnabled = integrationState?.iscatEnabled?.catD;
const enqueue = useCallback((sighting: SightingType, kind: HitKind) => { const enqueue = useCallback(
(sighting: SightingType, kind: HitKind) => {
if (!sighting) return;
const id = sighting.vrm ?? sighting.ref; const id = sighting.vrm ?? sighting.ref;
if (processedRefs.current.has(id)) return; if (processedRefs.current.has(id)) return;
@@ -81,10 +75,13 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
if (inList) { if (inList) {
return; return;
} }
processedRefs.current.add(id); processedRefs.current.add(id);
setModalQueue((q) => [...q, { id, sighting, kind }]); setModalQueue((q) => [...q, { id, sighting, kind }]);
}, []); },
[alertState?.alertList]
);
const reduceObject = (obj: SightingType): ReducedSightingType => { const reduceObject = (obj: SightingType): ReducedSightingType => {
return { return {
@@ -133,31 +130,16 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
enqueue(sighting, isNPED ? "NPED" : "HOTLIST"); // enqueue ONLY enqueue(sighting, isNPED ? "NPED" : "HOTLIST"); // enqueue ONLY
} }
} }
}, [rows, enqueue]); }, [rows, enqueue, isCatAEnabled, isCatBEnabled, isCatCEnabled, isCatDEnabled]);
useEffect(() => {
rows?.forEach((obj) => {
const cat = getNPEDCategory(obj);
const isNPEDHitA = cat === "A";
const isNPEDHitB = cat === "B";
const isNPEDHitC = cat === "C";
if (isNPEDHitA || isNPEDHitB || isNPEDHitC) {
dispatch({
type: "ADD",
payload: obj,
});
}
});
}, [dispatch]);
useEffect(() => { useEffect(() => {
if (hasAutoOpenedRef.current || npedRef.current) return; if (hasAutoOpenedRef.current || npedRef.current) return;
const firstNPED = rows.find((r) => { const firstNPED = rows.find((r) => {
const cat = getNPEDCategory(r); const cat = getNPEDCategory(r);
const isNPEDHitA = cat === "A"; const isNPEDHitA = cat === "A" && isCatAEnabled;
const isNPEDHitB = cat === "B"; const isNPEDHitB = cat === "B" && isCatBEnabled;
const isNPEDHitC = cat === "C"; const isNPEDHitC = cat === "C" && isCatCEnabled;
const isNPEDHitD = cat === "D"; const isNPEDHitD = cat === "D" && isCatDEnabled;
return isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD; return isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD;
}); });
const firstHot = rows?.find((r) => { const firstHot = rows?.find((r) => {
@@ -176,20 +158,42 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
hasAutoOpenedRef.current = true; hasAutoOpenedRef.current = true;
} }
}, [enqueue, hotlistsound, npedSound, rows, setSelectedSighting, setSightingModalOpen]); }, [
enqueue,
rows,
setSelectedSighting,
setSightingModalOpen,
isCatAEnabled,
isCatBEnabled,
isCatCEnabled,
isCatDEnabled,
]);
useEffect(() => { useEffect(() => {
if (!isSightingModalOpen && modalQueue.length > 0) { if (!isSightingModalOpen && modalQueue.length > 0) {
const next = modalQueue[0]; const next = modalQueue[0];
if (next.kind === "NPED") npedSound();
else hotlistsound();
setSelectedSighting(next.sighting); setSelectedSighting(next.sighting);
setSightingModalOpen(true); setSightingModalOpen(true);
} }
}, [isSightingModalOpen, npedSound, hotlistsound, setSelectedSighting, setSightingModalOpen, modalQueue]); }, [isSightingModalOpen, setSelectedSighting, setSightingModalOpen, modalQueue]);
useEffect(() => {
rows?.forEach((obj) => {
const cat = getNPEDCategory(obj);
const isNPEDHitA = cat === "A";
const isNPEDHitB = cat === "B";
const isNPEDHitC = cat === "C";
const isNPEDHitD = cat === "D";
if (isNPEDHitA || isNPEDHitB || isNPEDHitC || isNPEDHitD) {
console.log("first");
dispatch({
type: "ADD",
payload: obj,
});
}
});
}, [dispatch, rows]);
const handleClose = () => { const handleClose = () => {
setSightingModalOpen(false); setSightingModalOpen(false);
@@ -212,6 +216,7 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
const isNPEDHitA = cat === "A"; const isNPEDHitA = cat === "A";
const isNPEDHitB = cat === "B"; const isNPEDHitB = cat === "B";
const isNPEDHitC = cat === "C"; const isNPEDHitC = cat === "C";
const isNPEDHitD = cat === "D";
const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY"; const motionAway = (obj?.motion ?? "").toUpperCase() === "AWAY";
const isHotListHit = checkIsHotListHit(obj); const isHotListHit = checkIsHotListHit(obj);
return ( return (
@@ -230,6 +235,7 @@ export default function SightingHistoryWidget({ className, title }: SightingHist
{isNPEDHitA && <img src={NPED_CAT_A} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isNPEDHitA && <img src={NPED_CAT_A} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitB && <img src={NPED_CAT_B} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isNPEDHitB && <img src={NPED_CAT_B} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitC && <img src={NPED_CAT_C} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isNPEDHitC && <img src={NPED_CAT_C} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{isNPEDHitD && <img src={NPED_CAT_D} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
<NumberPlate motion={motionAway} vrm={obj?.vrm} /> <NumberPlate motion={motionAway} vrm={obj?.vrm} />
</div> </div>
</div> </div>

View File

@@ -36,10 +36,16 @@ export const IntegrationsProvider = ({ children }: IntegrationsProviderType) =>
}); });
if (!isMounted) return; if (!isMounted) return;
if (!result?.result || typeof result.result === "string") return;
if (result?.result && typeof result.result !== "string") {
dispatch({ type: "UPDATE", payload: result.result }); dispatch({ type: "UPDATE", payload: result.result });
}
if (catResult?.result) {
dispatch({ type: "NPEDCATENABLED", payload: catResult.result }); dispatch({ type: "NPEDCATENABLED", payload: catResult.result });
} else {
console.log("Early return: catResult check failed");
}
} catch (error) { } catch (error) {
console.error("Error in fetchData:", error); console.error("Error in fetchData:", error);
} }

View File

@@ -88,8 +88,14 @@ export function useSightingFeed(url: string | undefined) {
const isNPEDHitA = cat === "A"; const isNPEDHitA = cat === "A";
const isNPEDHitB = cat === "B"; const isNPEDHitB = cat === "B";
const isNPEDHitC = cat === "C"; const isNPEDHitC = cat === "C";
const isNPEDHitD = cat === "D";
if ((isNPEDHitA && audioArmed) || (isNPEDHitB && audioArmed) || (isNPEDHitC && audioArmed)) { if (
(isNPEDHitA && audioArmed) ||
(isNPEDHitB && audioArmed) ||
(isNPEDHitC && audioArmed) ||
(isNPEDHitD && audioArmed)
) {
playNPEDHitSound(); playNPEDHitSound();
} else if (isHotListHit && audioArmed) { } else if (isHotListHit && audioArmed) {
playHotlistsound(); playHotlistsound();

View File

@@ -159,6 +159,8 @@ export function getHotlistName(obj: HotlistMatches | undefined) {
export const getNPEDCategory = (r?: SightingType | null) => export const getNPEDCategory = (r?: SightingType | null) =>
r?.metadata?.npedJSON?.["NPED CATEGORY"] as "A" | "B" | "C" | "D" | undefined; r?.metadata?.npedJSON?.["NPED CATEGORY"] as "A" | "B" | "C" | "D" | undefined;
export const getNPEDReason = (r?: SightingType | null) => r?.metadata?.npedJSON;
export const zoomMapping = (zoomLevel: number | undefined) => { export const zoomMapping = (zoomLevel: number | undefined) => {
switch (zoomLevel) { switch (zoomLevel) {
case 1: case 1: