Merged in enhancement/sessionpage (pull request #22)

Enhancement/sessionpage
This commit is contained in:
2025-10-17 07:29:49 +00:00
11 changed files with 119 additions and 119 deletions

View File

@@ -14,11 +14,7 @@ const FrontCameraOverviewCard = () => {
}); });
return ( return (
<Card <Card className={clsx("relative min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[70%] overflow-y-hidden")}>
className={clsx(
"relative min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[70%] overflow-y-hidden"
)}
>
<div className="w-full" {...handlers}> <div className="w-full" {...handlers}>
<SightingOverview /> <SightingOverview />
</div> </div>

View File

@@ -30,11 +30,7 @@ const OverviewVideoContainer = ({
trackMouse: true, trackMouse: true,
}); });
return ( return (
<Card <Card className={clsx("relative min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[70%] overflow-y-hidden")}>
className={clsx(
"relative min-h-[40vh] md:min-h-[60vh] max-h-[80vh] lg:w-[70%] overflow-y-hidden"
)}
>
<div className="w-full" {...handlers}> <div className="w-full" {...handlers}>
<SnapshotContainer <SnapshotContainer
side={side} side={side}

View File

@@ -1,7 +1,6 @@
import type { SightingType } from "../../types/types"; import type { SightingType } from "../../types/types";
import NumberPlate from "../PlateStack/NumberPlate"; import NumberPlate from "../PlateStack/NumberPlate";
import SightingModal from "../SightingModal/SightingModal"; import SightingModal from "../SightingModal/SightingModal";
import InfoBar from "../SightingsWidget/InfoBar";
import { useState } from "react"; import { useState } from "react";
import HotListImg from "/Hotlist_Hit.svg"; import HotListImg from "/Hotlist_Hit.svg";
import { useAlertHitContext } from "../../context/AlertHitContext"; import { useAlertHitContext } from "../../context/AlertHitContext";
@@ -9,7 +8,11 @@ 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 { checkIsHotListHit, getNPEDCategory } from "../../utils/utils"; import { checkIsHotListHit, formatAge, getNPEDCategory } from "../../utils/utils";
import { faX } from "@fortawesome/free-solid-svg-icons";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Badge from "../UI/Badge";
type AlertItemProps = { type AlertItemProps = {
item: SightingType; item: SightingType;
@@ -20,7 +23,6 @@ const AlertItem = ({ item }: AlertItemProps) => {
const { dispatch } = useAlertHitContext(); const { dispatch } = useAlertHitContext();
const { mutation } = useCameraBlackboard(); const { mutation } = useCameraBlackboard();
// const {d} = useCameraBlackboard();
const motionAway = (item?.motion ?? "").toUpperCase() === "AWAY"; const motionAway = (item?.motion ?? "").toUpperCase() === "AWAY";
const isHotListHit = checkIsHotListHit(item); const isHotListHit = checkIsHotListHit(item);
@@ -53,9 +55,15 @@ const AlertItem = ({ item }: AlertItemProps) => {
dispatch({ type: "REMOVE", payload: item }); dispatch({ type: "REMOVE", payload: item });
}; };
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full relative">
<div className="border border-gray-600 rounded-lg items-center py-1"> <div className="border border-gray-600 rounded-lg items-center p-4">
<InfoBar obj={item} /> <div className="flex flex-row space-x-3 ml-4">
<Badge text={`Seen: ${formatAge(item.timeStampMillis)}`} icon={faClock} />
</div>
<button onClick={() => handleDelete(item)} className="absolute right-2 top-3">
<FontAwesomeIcon icon={faX} size="xl" />
</button>
<div className="flex flex-row p-4 w-full mx-auto justify-between" onClick={handleClick}> <div className="flex flex-row p-4 w-full mx-auto justify-between" onClick={handleClick}>
{isHotListHit && <img src={HotListImg} alt="hotlistHit" className="h-20 object-contain rounded-md" />} {isHotListHit && <img src={HotListImg} alt="hotlistHit" className="h-20 object-contain rounded-md" />}
{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" />}
@@ -64,8 +72,10 @@ const AlertItem = ({ item }: AlertItemProps) => {
<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>
<div className="h-20">
<NumberPlate vrm={item.vrm} motion={motionAway} /> <NumberPlate vrm={item.vrm} motion={motionAway} />
</div> </div>
</div>
<SightingModal <SightingModal
isSightingModalOpen={isModalOpen} isSightingModalOpen={isModalOpen}
handleClose={closeModal} handleClose={closeModal}

View File

@@ -1,34 +1,14 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useAlertHitContext } from "../../context/AlertHitContext"; import { useAlertHitContext } from "../../context/AlertHitContext";
import { useCameraBlackboard } from "../../hooks/useCameraBlackboard"; import { useCameraBlackboard } from "../../hooks/useCameraBlackboard";
import type { CameraBlackBoardOptions, SightingType } from "../../types/types"; import type { CameraBlackBoardOptions } from "../../types/types";
import Card from "../UI/Card"; import Card from "../UI/Card";
import CardHeader from "../UI/CardHeader"; import CardHeader from "../UI/CardHeader";
import AlertItem from "./AlertItem"; import AlertItem from "./AlertItem";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
const HistoryList = () => { const HistoryList = () => {
const { state, dispatch, isLoading, error } = useAlertHitContext(); const { state, dispatch, isLoading, error } = useAlertHitContext();
const { mutation } = useCameraBlackboard(); const { mutation } = useCameraBlackboard();
const handleDeleteClick = async (deletedItem: SightingType) => {
const res = await mutation.mutateAsync({
operation: "VIEW",
path: "alertHistory",
});
const oldArray = res?.result;
const updatedArray = oldArray?.filter(
(item: SightingType) => item?.ref !== deletedItem?.ref
);
mutation.mutate({
operation: "INSERT",
path: "alertHistory",
value: updatedArray,
});
dispatch({ type: "REMOVE", payload: deletedItem });
};
const handleClearListClick = (listName: CameraBlackBoardOptions) => { const handleClearListClick = (listName: CameraBlackBoardOptions) => {
dispatch({ type: "DELETE", payload: [] }); dispatch({ type: "DELETE", payload: [] });
mutation.mutate({ mutation.mutate({
@@ -38,7 +18,7 @@ const HistoryList = () => {
}; };
return ( return (
<Card className="h-100 p-4"> <Card className="h-100 p-4 col-span-3">
<CardHeader title="Alert History" /> <CardHeader title="Alert History" />
<button <button
className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition md:w-[10%] mb-2" className="bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition md:w-[10%] mb-2"
@@ -50,18 +30,23 @@ const HistoryList = () => {
{error && <p className="text-red-500 px-2">Error: {error.message}</p>} {error && <p className="text-red-500 px-2">Error: {error.message}</p>}
<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 ? (
state?.alertList?.map((alertItem, index) => ( <div className="mt-3 grid grid-cols-1 gap-3">
<div key={index} className="flex flex-row space-x-2"> {state?.alertList?.map((alertItem) => (
<AlertItem item={alertItem} /> <AlertItem item={alertItem} key={alertItem.vrm} />
<button onClick={() => handleDeleteClick(alertItem)}> ))}
<div className="p-4">
<FontAwesomeIcon icon={faTrash} size="2x" />
</div> </div>
</button>
</div>
))
) : ( ) : (
<p>No Alert results</p> <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="mb-3 rounded-xl bg-slate-800 px-3 py-1 text-xs uppercase tracking-wider text-slate-400">
No Alert Results
</div>
<p className="max-w-md text-slate-300">
Alerts will appear here in real-time once there are <span className="text-emerald-400">Hotlist</span> or{" "}
<span className="text-amber-600">NPED</span> hits. Use{" "}
<span className="text-emerald-400">Start Session</span> to begin capturing results, or add a{" "}
<span className="text-emerald-400">Sighting</span> from the sighting list.
</p>
</div>
)} )}
</div> </div>
</Card> </Card>

View File

@@ -18,18 +18,9 @@ const RearCameraOverviewCard = ({ className }: CardProps) => {
}); });
const { mostRecent } = useSightingFeedContext(); const { mostRecent } = useSightingFeedContext();
return ( return (
<Card <Card className={clsx("relative min-h-[40vh] md:min-h-[60vh] h-auto", className)}>
className={clsx(
"relative min-h-[40vh] md:min-h-[60vh] h-auto",
className
)}
>
<div className="flex flex-col space-y-3 h-full" {...handlers}> <div className="flex flex-col space-y-3 h-full" {...handlers}>
<CardHeader <CardHeader title="Rear Overview" icon={faCamera} sighting={mostRecent} />
title="Rear Overview"
icon={faCamera}
sighting={mostRecent}
/>
<SightingOverview /> <SightingOverview />
</div> </div>
</Card> </Card>

View File

@@ -9,38 +9,36 @@ const SessionCard = () => {
const { dispatch } = useAlertHitContext(); const { dispatch } = useAlertHitContext();
return ( return (
<Card className="p-4"> <Card className="p-4 col-span-5">
<CardHeader title={"Hit Search"} /> <CardHeader title={"Hit Search"} />
<div className="flex flex-col gap-4 px-2"> <div className="flex flex-col gap-4 px-2">
<FormGroup> <label htmlFor="VRM" className="font-medium whitespace-nowrap md:w-1/2 text-left">
<label
htmlFor="VRM"
className="font-medium whitespace-nowrap md:w-1/2 text-left"
>
VRM (Min 2 letters) VRM (Min 2 letters)
</label> </label>
<div className="flex-1 flex justify-end md:w-2/3"> <FormGroup>
<div className="flex flex-row justify-between md:w-full space-x-3">
<input <input
id="VRMSelect" id="VRMSelect"
name="VRMSelect" name="VRMSelect"
type="text" type="text"
className="p-2 border border-gray-400 rounded-lg w-full max-w-xs" className="p-2 border border-gray-400 rounded-lg w-full max-w-[70%] focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30"
placeholder="Enter VRM" placeholder="Enter VRM"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
/> />
</div>
</FormGroup>
<button <button
className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-full mx-auto" className="bg-[#26B170] text-white px-4 py-2 rounded hover:bg-green-700 transition w-[30%] mx-3"
onClick={() => dispatch({ type: "SEARCH", payload: searchTerm })} onClick={() => dispatch({ type: "SEARCH", payload: searchTerm })}
disabled={searchTerm.trim().length < 2} disabled={searchTerm.trim().length < 2}
> >
Search Hit list Search Hit list
</button> </button>
</div>
</FormGroup>
{searchTerm && ( {searchTerm && (
<button <button
className="bg-gray-300 text-gray-900 px-4 py-2 rounded hover:bg-gray-700 transition w-full mx-auto" className="bg-gray-300 text-gray-900 px-4 py-2 rounded hover:bg-gray-700 transition w-[30%] "
onClick={() => { onClick={() => {
setSearchTerm(""); setSearchTerm("");
dispatch({ type: "SEARCH", payload: "" }); dispatch({ type: "SEARCH", payload: "" });

View File

@@ -9,37 +9,21 @@ const SessionCard = () => {
const handleStartClick = () => { const handleStartClick = () => {
setSessionStarted(!sessionStarted); setSessionStarted(!sessionStarted);
toast( toast(`${sessionStarted ? "Vehicle tracking session Ended" : "Vehicle tracking session Started"}`);
`${
sessionStarted
? "Vehicle tracking session Ended"
: "Vehicle tracking session Started"
}`
);
}; };
const sightings = [ const sightings = [...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle]))];
...new Map(sessionList.map((vehicle) => [vehicle.vrm, vehicle])),
];
const dedupedSightings = sightings.map((sighting) => sighting[1]); const dedupedSightings = sightings.map((sighting) => sighting[1]);
const vehicles = dedupedSightings.reduce< const vehicles = dedupedSightings.reduce<Record<string, ReducedSightingType[]>>(
Record<string, ReducedSightingType[]>
>(
(acc, item) => { (acc, item) => {
if (item.metadata?.npedJSON["NPED CATEGORY"] === "A") if (item.metadata?.npedJSON["NPED CATEGORY"] === "A") acc.npedCatA.push(item);
acc.npedCatA.push(item); if (item.metadata?.npedJSON["NPED CATEGORY"] === "B") acc.npedCatB.push(item);
if (item.metadata?.npedJSON["NPED CATEGORY"] === "B") if (item.metadata?.npedJSON["NPED CATEGORY"] === "C") acc.npedCatC.push(item);
acc.npedCatB.push(item); if (item.metadata?.npedJSON["NPED CATEGORY"] === "D") acc.npedCatD.push(item);
if (item.metadata?.npedJSON["NPED CATEGORY"] === "C") if (item.metadata?.npedJSON["TAX STATUS"] === false) acc.notTaxed.push(item);
acc.npedCatC.push(item); if (item.metadata?.npedJSON["MOT STATUS"] === false) acc.notMOT.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);
return acc; return acc;
}, },
@@ -54,13 +38,11 @@ const SessionCard = () => {
); );
return ( return (
<Card className="p-4"> <Card className="p-4 col-span-3">
<CardHeader title="Session" /> <CardHeader title="Session" />
<div className="flex flex-col gap-4 px-2"> <div className="flex flex-col gap-4 px-3">
<button <button
className={`${ className={`${sessionStarted ? "bg-red-600" : "bg-[#26B170]"} text-white px-4 py-2 rounded ${
sessionStarted ? "bg-red-600" : "bg-[#26B170]"
} text-white px-4 py-2 rounded ${
sessionStarted ? "hover:bg-red-700" : "hover:bg-green-700" sessionStarted ? "hover:bg-red-700" : "hover:bg-green-700"
} transition w-full`} } transition w-full`}
onClick={handleStartClick} onClick={handleStartClick}
@@ -69,12 +51,30 @@ const SessionCard = () => {
</button> </button>
<ul className="text-white space-y-2"> <ul className="text-white space-y-2">
<li>Number of Vehicles: {dedupedSightings.length} </li> <li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
<li>Vehicles without Tax: {vehicles.notTaxed.length}</li> <p>Number of Vehicles:</p>
<li>Vehicles without MOT: {vehicles.notMOT.length}</li> <span className="font-bold text-green-600 text-xl">{dedupedSightings.length}</span>
<li>Vehicles with NPED Cat A: {vehicles.npedCatA.length}</li> </li>
<li>Vehicles with NPED Cat B: {vehicles.npedCatB.length}</li> <li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
<li>Vehicles with NPED Cat C: {vehicles.npedCatC.length}</li> <p>Vehicles without Tax:</p>
<span className="font-bold text-amber-600 text-xl">{vehicles.notTaxed.length}</span>
</li>
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
<p>Vehicles without MOT:</p>{" "}
<span className="font-bold text-red-500 text-xl">{vehicles.notMOT.length}</span>
</li>
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
<p>Vehicles with NPED Cat A:</p>
<span className="font-bold text-gray-300 text-xl">{vehicles.npedCatA.length}</span>
</li>
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
<p>Vehicles with NPED Cat B:</p>{" "}
<span className="font-bold text-gray-300text-xl">{vehicles.npedCatB.length}</span>
</li>
<li className="rounded-xl border border-slate-800 bg-slate-800/60 p-3 shadow-sm flex flex-row justify-between">
Vehicles with NPED Cat C:{" "}
<span className="font-bold text-gray-300 text-xl">{vehicles.npedCatC.length}</span>
</li>
</ul> </ul>
</div> </div>
</Card> </Card>

View File

@@ -120,7 +120,11 @@ const SightingModal = ({ isSightingModalOpen, handleClose, sighting, onDelete }:
{hotlistName && ( {hotlistName && (
<div> <div>
<p className="text-gray-300">Hotlist</p> <p className="text-gray-300">Hotlist</p>
<p className="font-medium text-2xl break-all">{hotlistName ? hotlistName[0] : "-"}</p> <div className="items-center px-2.5 py-0.5 rounded-sm me-2 bg-amber-500">
<p className="font-medium text-2xl break-all text-amber-800">
{hotlistName ? hotlistName[0] : "-"}
</p>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -0,0 +1,18 @@
import type { Icon, IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type BadgeProps = {
icon?: Icon | IconDefinition;
text: string;
};
const Badge = ({ icon, text }: BadgeProps) => {
return (
<span className="text-md font-medium inline-flex items-center px-2.5 py-0.5 rounded-sm me-2 bg-blue-900 text-blue-200 border border-blue-500 space-x-2">
{icon && <FontAwesomeIcon icon={icon} />}
<span>{text}</span>
</span>
);
};
export default Badge;

View File

@@ -10,7 +10,7 @@ const Card = ({ children, className }: CardProps) => {
return ( return (
<div <div
className={clsx( className={clsx(
"bg-[#253445] rounded-lg mt-4 mx-2 shadow-2xl overflow-x-hidden md:row-span-1 px-2", "bg-[#253445] rounded-lg mt-4 mx-2 shadow-2xl overflow-x-hidden md:row-span-1 px-2 border border-gray-600 ",
className className
)} )}
> >

View File

@@ -11,8 +11,10 @@ const Session = () => {
return ( return (
<SightingFeedProvider> <SightingFeedProvider>
<div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full"> <div className="mx-auto grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 gap-2 px-1 sm:px-2 lg:px-0 w-full">
<div className="grid grid-cols-1 lg:grid-cols-8 col-span-2 w-full">
<HitSearchCard /> <HitSearchCard />
<SessionCard /> <SessionCard />
</div>
<div className="col-span-2"> <div className="col-span-2">
<HistoryList /> <HistoryList />
</div> </div>