From 52f57f545d50c339ad4f1ca265ce144ed8818d11 Mon Sep 17 00:00:00 2001 From: Emmanuel Cobbinah Date: Sat, 17 May 2025 08:26:01 +0000 Subject: [PATCH 1/3] feat: add awaiting shipments UI with filters, dummy data, and modular components --- clientapp/src/components/SearchBar.tsx | 20 +++ clientapp/src/components/ShipmentList.tsx | 52 ++++++ clientapp/src/components/StatusFilter.tsx | 18 +++ .../src/features/shipments/PastShipments.tsx | 152 +++++++++++++++--- 4 files changed, 221 insertions(+), 21 deletions(-) create mode 100644 clientapp/src/components/SearchBar.tsx create mode 100644 clientapp/src/components/ShipmentList.tsx create mode 100644 clientapp/src/components/StatusFilter.tsx diff --git a/clientapp/src/components/SearchBar.tsx b/clientapp/src/components/SearchBar.tsx new file mode 100644 index 0000000..a5320bb --- /dev/null +++ b/clientapp/src/components/SearchBar.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +interface SearchBarProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; + className?: string; +} + +const SearchBar: React.FC = ({ value, onChange, placeholder, className }) => ( + +); + +export default SearchBar; diff --git a/clientapp/src/components/ShipmentList.tsx b/clientapp/src/components/ShipmentList.tsx new file mode 100644 index 0000000..9ae28cd --- /dev/null +++ b/clientapp/src/components/ShipmentList.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import RecentShipmentCard from "./RecentShipmentCard"; + +// Accepts a list of shipments and displays them using RecentShipmentCard +interface ShipmentListProps { + shipments: Array<{ + id: string; + trackingNumber?: string; + origin?: string; + destination?: string; + status?: string; + estimatedDelivery?: string; + updatedAt?: string; + description?: string; + // Accepts extra fields for flexibility + [key: string]: any; + }>; + emptyMessage?: string; +} + +const ShipmentList: React.FC = ({ shipments, emptyMessage }) => { + if (!shipments.length) { + return ( +
+ {emptyMessage || "No shipments found."} +
+ ); + } + + return ( +
+ {shipments.map((shipment) => ( + + ))} +
+ ); +}; + +export default ShipmentList; diff --git a/clientapp/src/components/StatusFilter.tsx b/clientapp/src/components/StatusFilter.tsx new file mode 100644 index 0000000..57aec09 --- /dev/null +++ b/clientapp/src/components/StatusFilter.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +interface StatusFilterProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + options: { value: string; label: string }[]; + className?: string; +} + +const StatusFilter: React.FC = ({ value, onChange, options, className }) => ( + +); + +export default StatusFilter; diff --git a/clientapp/src/features/shipments/PastShipments.tsx b/clientapp/src/features/shipments/PastShipments.tsx index 270322a..917e9af 100644 --- a/clientapp/src/features/shipments/PastShipments.tsx +++ b/clientapp/src/features/shipments/PastShipments.tsx @@ -1,4 +1,7 @@ import { useEffect, useState } from "react"; +import SearchBar from "../../components/SearchBar"; +import StatusFilter from "../../components/StatusFilter"; +import ShipmentList from "../../components/ShipmentList"; // // TODO: Fetch past shipments from API here// useEffect(() => {// API: Placeholder for fetching past shipments// import axios from "axios"; // Uncomment when ready to use APIs @@ -8,38 +11,145 @@ import { useEffect, useState } from "react"; interface Shipment { id: string; description: string; + trackingNumber?: string; + origin?: string; + destination?: string; + status?: string; + estimatedDelivery?: string; // Add estimatedDelivery field // Add more properties as needed } const PastShipments = () => { + const [loading, setLoading] = useState(true); const [shipments, setShipments] = useState([]); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState(""); + // Example statuses for filter dropdown + const STATUS_OPTIONS = [ + { value: "", label: "All Statuses" }, + { value: "pending", label: "Pending" }, + { value: "in-transit", label: "In Transit" }, + { value: "processing", label: "Processing" }, + { value: "delayed", label: "Delayed" }, + { value: "delivered", label: "Delivered" }, + ]; + + // Filtered shipments + const filteredShipments = shipments.filter((shipment) => { + const matchesSearch = + search === "" || + shipment.description?.toLowerCase().includes(search.toLowerCase()) || + shipment.id?.toLowerCase().includes(search.toLowerCase()) || + shipment.trackingNumber?.toLowerCase().includes(search.toLowerCase()) || + shipment.origin?.toLowerCase().includes(search.toLowerCase()) || + shipment.destination?.toLowerCase().includes(search.toLowerCase()); + const matchesStatus = + !status || + (shipment.status && shipment.status.toLowerCase() === status); + return matchesSearch && matchesStatus; + }); + + // Dummy data for demonstration useEffect(() => { - // Fetch past shipments from API here - const fetchShipments = async () => { - try { - const response = await fetch('/api/shipments/past'); // Replace with actual API endpoint - const data: Shipment[] = await response.json(); - setShipments(data); - } catch (error) { - console.error("Error fetching shipments:", error); - } - }; - - fetchShipments(); + const dummyShipments: Shipment[] = [ + { + id: "1", + description: "Electronics - Laptop", + trackingNumber: "TRK-10001", + origin: "Accra, Ghana", + destination: "London, UK", + status: "in-transit", + estimatedDelivery: "May 20, 2025", + }, + { + id: "2", + description: "Clothing - Summer Collection", + trackingNumber: "TRK-10002", + origin: "Takoradi, Ghana", + destination: "Tamale, Ghana", + status: "pending", + estimatedDelivery: "May 22, 2025", + }, + { + id: "3", + description: "Books - Educational", + trackingNumber: "TRK-10003", + origin: "Cape Coast, Ghana", + destination: "New York, USA", + status: "processing", + estimatedDelivery: "May 23, 2025", + }, + { + id: "4", + description: "Furniture - Office Chair", + trackingNumber: "TRK-10004", + origin: "Tema, Ghana", + destination: "Ho, Ghana", + status: "delayed", + estimatedDelivery: "May 25, 2025", + }, + { + id: "5", + description: "Food - Non-perishable", + trackingNumber: "TRK-10005", + origin: "Koforidua, Ghana", + destination: "Berlin, Germany", + status: "in-transit", + estimatedDelivery: "May 21, 2025", + }, + { + id: "6", + description: "Medical Supplies", + trackingNumber: "TRK-10006", + origin: "Wa, Ghana", + destination: "Dambai, Ghana", + status: "pending", + estimatedDelivery: "May 24, 2025", + }, + { + id: "7", + description: "Machinery Parts", + trackingNumber: "TRK-10007", + origin: "Sefwi Wiawso, Ghana", + destination: "Dubai, UAE", + status: "processing", + estimatedDelivery: "May 26, 2025", + }, + ]; + setShipments(dummyShipments); + setLoading(false); }, []); + if (loading) { + return ( +
+
+
+ ); + } + return ( -
-

Past Shipments

- {shipments.length > 0 ? ( -
    - {shipments.map((shipment) => ( -
  • {shipment.description}
  • - ))} -
+
+

Awaiting Shipments

+
+ setSearch(e.target.value)} + placeholder="Search by tracking number, description, origin, destination..." + className="w-full md:w-1/2 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" + /> + setStatus(e.target.value)} + options={STATUS_OPTIONS} + className="w-full md:w-1/4 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" + /> +
+ {filteredShipments.length > 0 ? ( + ) : ( -

No past shipments found.

+

No awaiting shipments found.

)}
); From e318f57620545dc7bdc98b3fc225a383aa6cf92e Mon Sep 17 00:00:00 2001 From: Emmanuel Cobbinah Date: Sat, 17 May 2025 13:27:56 +0000 Subject: [PATCH 2/3] feat(shipments): refactor shipment views and add filtering - Refactored PastShipments into a generic ShipmentsPage component - Added AwaitingShipments and ShippingHistory wrappers for clear routing - Updated router to use new components for /awaiting and /history - Implemented reusable SearchBar, StatusFilter, and ShipmentList components - Added dummy data with local and international destinations - Improved filtering by status and search - Deprecated PastShipments.tsx in favor of new structure --- clientapp/src/App.tsx | 7 +- .../features/shipments/AwaitingShipments.tsx | 15 ++ .../src/features/shipments/PastShipments.tsx | 3 + .../src/features/shipments/ShipmentsPage.tsx | 177 ++++++++++++++++++ .../features/shipments/ShippingHistory.tsx | 14 ++ 5 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 clientapp/src/features/shipments/AwaitingShipments.tsx create mode 100644 clientapp/src/features/shipments/ShipmentsPage.tsx create mode 100644 clientapp/src/features/shipments/ShippingHistory.tsx diff --git a/clientapp/src/App.tsx b/clientapp/src/App.tsx index e48a1e7..99cebd7 100644 --- a/clientapp/src/App.tsx +++ b/clientapp/src/App.tsx @@ -7,7 +7,8 @@ import "./App.css"; import DashboardLayout from "./components/layout/DashboardLayout"; import Dashboard from "./features/dashboard/Dashboard"; import SubmitGoods from "./features/shipments/SubmitGoods"; -import PastShipments from "./features/shipments/PastShipments"; +import AwaitingShipments from "./features/shipments/AwaitingShipments"; +import ShippingHistory from "./features/shipments/ShippingHistory"; import ReportIssue from "./features/report/ReportIssue"; import Settings from "./features/settings/Settings"; @@ -29,8 +30,8 @@ function AppRoutes() { } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/clientapp/src/features/shipments/AwaitingShipments.tsx b/clientapp/src/features/shipments/AwaitingShipments.tsx new file mode 100644 index 0000000..ef4e222 --- /dev/null +++ b/clientapp/src/features/shipments/AwaitingShipments.tsx @@ -0,0 +1,15 @@ +import ShipmentsPage from "./ShipmentsPage"; + +const AwaitingShipments = () => { + // Awaiting = not delivered + const awaitingStatuses = ["pending", "in-transit", "processing", "delayed"]; + return ( + + ); +}; + +export default AwaitingShipments; diff --git a/clientapp/src/features/shipments/PastShipments.tsx b/clientapp/src/features/shipments/PastShipments.tsx index 917e9af..707ab4e 100644 --- a/clientapp/src/features/shipments/PastShipments.tsx +++ b/clientapp/src/features/shipments/PastShipments.tsx @@ -3,6 +3,9 @@ import SearchBar from "../../components/SearchBar"; import StatusFilter from "../../components/StatusFilter"; import ShipmentList from "../../components/ShipmentList"; +// This file is now replaced by ShipmentsPage, AwaitingShipments, and ShippingHistory wrappers. +// You can remove this file or keep it for reference, but it is no longer used in the router. + // // TODO: Fetch past shipments from API here// useEffect(() => {// API: Placeholder for fetching past shipments// import axios from "axios"; // Uncomment when ready to use APIs // // Example: axios.get('/api/shipments/past').then(...) diff --git a/clientapp/src/features/shipments/ShipmentsPage.tsx b/clientapp/src/features/shipments/ShipmentsPage.tsx new file mode 100644 index 0000000..bf870bc --- /dev/null +++ b/clientapp/src/features/shipments/ShipmentsPage.tsx @@ -0,0 +1,177 @@ +import { useEffect, useState } from "react"; +import SearchBar from "../../components/SearchBar"; +import StatusFilter from "../../components/StatusFilter"; +import ShipmentList from "../../components/ShipmentList"; + +interface Shipment { + id: string; + description: string; + trackingNumber?: string; + origin?: string; + destination?: string; + status?: string; + estimatedDelivery?: string; +} + +interface ShipmentsPageProps { + title: string; + filterStatus: string[]; + emptyMessage?: string; +} + +const STATUS_OPTIONS = [ + { value: "", label: "All Statuses" }, + { value: "pending", label: "Pending" }, + { value: "in-transit", label: "In Transit" }, + { value: "processing", label: "Processing" }, + { value: "delayed", label: "Delayed" }, + { value: "delivered", label: "Delivered" }, +]; + +const ShipmentsPage = ({ title, filterStatus, emptyMessage }: ShipmentsPageProps) => { + const [loading, setLoading] = useState(true); + const [shipments, setShipments] = useState([]); + const [search, setSearch] = useState(""); + const [status, setStatus] = useState(""); + + // Dummy data for demonstration + useEffect(() => { + const dummyShipments: Shipment[] = [ + { + id: "1", + description: "Electronics - Laptop", + trackingNumber: "TRK-10001", + origin: "Accra, Ghana", + destination: "London, UK", + status: "in-transit", + estimatedDelivery: "May 20, 2025", + }, + { + id: "2", + description: "Clothing - Summer Collection", + trackingNumber: "TRK-10002", + origin: "Takoradi, Ghana", + destination: "Tamale, Ghana", + status: "pending", + estimatedDelivery: "May 22, 2025", + }, + { + id: "3", + description: "Books - Educational", + trackingNumber: "TRK-10003", + origin: "Cape Coast, Ghana", + destination: "New York, USA", + status: "processing", + estimatedDelivery: "May 23, 2025", + }, + { + id: "4", + description: "Furniture - Office Chair", + trackingNumber: "TRK-10004", + origin: "Tema, Ghana", + destination: "Ho, Ghana", + status: "delayed", + estimatedDelivery: "May 25, 2025", + }, + { + id: "5", + description: "Food - Non-perishable", + trackingNumber: "TRK-10005", + origin: "Koforidua, Ghana", + destination: "Berlin, Germany", + status: "in-transit", + estimatedDelivery: "May 21, 2025", + }, + { + id: "6", + description: "Medical Supplies", + trackingNumber: "TRK-10006", + origin: "Wa, Ghana", + destination: "Dambai, Ghana", + status: "pending", + estimatedDelivery: "May 24, 2025", + }, + { + id: "7", + description: "Machinery Parts", + trackingNumber: "TRK-10007", + origin: "Sefwi Wiawso, Ghana", + destination: "Dubai, UAE", + status: "processing", + estimatedDelivery: "May 26, 2025", + }, + { + id: "8", + description: "Shoes - Sneakers", + trackingNumber: "TRK-10008", + origin: "Paris, France", + destination: "Accra, Ghana", + status: "delivered", + estimatedDelivery: "May 10, 2025", + }, + { + id: "9", + description: "Phones - Smartphones", + trackingNumber: "TRK-10009", + origin: "Berlin, Germany", + destination: "Kumasi, Ghana", + status: "delivered", + estimatedDelivery: "May 12, 2025", + }, + ]; + setShipments(dummyShipments); + setLoading(false); + }, []); + + // Filtered shipments + const filteredShipments = shipments.filter((shipment) => { + // Only show shipments matching the filterStatus prop + const inStatusGroup = filterStatus.length === 0 || (shipment.status && filterStatus.includes(shipment.status)); + const matchesSearch = + search === "" || + shipment.description?.toLowerCase().includes(search.toLowerCase()) || + shipment.id?.toLowerCase().includes(search.toLowerCase()) || + shipment.trackingNumber?.toLowerCase().includes(search.toLowerCase()) || + shipment.origin?.toLowerCase().includes(search.toLowerCase()) || + shipment.destination?.toLowerCase().includes(search.toLowerCase()); + const matchesStatus = + !status || + (shipment.status && shipment.status.toLowerCase() === status); + return inStatusGroup && matchesSearch && matchesStatus; + }); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+

{title}

+
+ setSearch(e.target.value)} + placeholder="Search by tracking number, description, origin, destination..." + className="w-full md:w-1/2 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" + /> + setStatus(e.target.value)} + options={STATUS_OPTIONS} + className="w-full md:w-1/4 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" + /> +
+ {filteredShipments.length > 0 ? ( + + ) : ( +

{emptyMessage || "No shipments found."}

+ )} +
+ ); +}; + +export default ShipmentsPage; diff --git a/clientapp/src/features/shipments/ShippingHistory.tsx b/clientapp/src/features/shipments/ShippingHistory.tsx new file mode 100644 index 0000000..34958f2 --- /dev/null +++ b/clientapp/src/features/shipments/ShippingHistory.tsx @@ -0,0 +1,14 @@ +import ShipmentsPage from "./ShipmentsPage"; + +const ShippingHistory = () => { + // History = delivered only + return ( + + ); +}; + +export default ShippingHistory; From 8eb7f1a91afe9192bf661080d3f81b965e9bbc0d Mon Sep 17 00:00:00 2001 From: Emmanuel Cobbinah Date: Sat, 17 May 2025 13:32:57 +0000 Subject: [PATCH 3/3] refactor(shipments): split PastShipments into generic ShipmentsPage with AwaitingShipments and ShippingHistory wrappers - Created ShipmentsPage component to handle generic shipment listing, filtering, and search - Added AwaitingShipments and ShippingHistory wrappers for clear route separation - Updated router to use new components for /awaiting and /history - Left PastShipments.tsx as deprecated with a note for reference - Improved maintainability and future extensibility of shipment views --- .../src/features/shipments/PastShipments.tsx | 29 +++++++++++---- .../src/features/shipments/ShipmentsPage.tsx | 35 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/clientapp/src/features/shipments/PastShipments.tsx b/clientapp/src/features/shipments/PastShipments.tsx index 707ab4e..dee548e 100644 --- a/clientapp/src/features/shipments/PastShipments.tsx +++ b/clientapp/src/features/shipments/PastShipments.tsx @@ -48,13 +48,25 @@ const PastShipments = () => { shipment.origin?.toLowerCase().includes(search.toLowerCase()) || shipment.destination?.toLowerCase().includes(search.toLowerCase()); const matchesStatus = - !status || - (shipment.status && shipment.status.toLowerCase() === status); + !status || (shipment.status && shipment.status.toLowerCase() === status); return matchesSearch && matchesStatus; }); // Dummy data for demonstration useEffect(() => { + // Fetch past shipments from API here + // const fetchShipments = async () => { + // try { + // const response = await fetch('/api/shipments/past'); // Replace with actual API endpoint + // const data: Shipment[] = await response.json(); + // setShipments(data); + // } catch (error) { + // console.error("Error fetching shipments:", error); + // } + // }; + + // fetchShipments(); + const dummyShipments: Shipment[] = [ { id: "1", @@ -134,23 +146,28 @@ const PastShipments = () => { return (
-

Awaiting Shipments

+

+ Awaiting Shipments +

setSearch(e.target.value)} + onChange={(e) => setSearch(e.target.value)} placeholder="Search by tracking number, description, origin, destination..." className="w-full md:w-1/2 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" /> setStatus(e.target.value)} + onChange={(e) => setStatus(e.target.value)} options={STATUS_OPTIONS} className="w-full md:w-1/4 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" />
{filteredShipments.length > 0 ? ( - + ) : (

No awaiting shipments found.

)} diff --git a/clientapp/src/features/shipments/ShipmentsPage.tsx b/clientapp/src/features/shipments/ShipmentsPage.tsx index bf870bc..ccf0e41 100644 --- a/clientapp/src/features/shipments/ShipmentsPage.tsx +++ b/clientapp/src/features/shipments/ShipmentsPage.tsx @@ -28,7 +28,11 @@ const STATUS_OPTIONS = [ { value: "delivered", label: "Delivered" }, ]; -const ShipmentsPage = ({ title, filterStatus, emptyMessage }: ShipmentsPageProps) => { +const ShipmentsPage = ({ + title, + filterStatus, + emptyMessage, +}: ShipmentsPageProps) => { const [loading, setLoading] = useState(true); const [shipments, setShipments] = useState([]); const [search, setSearch] = useState(""); @@ -36,6 +40,19 @@ const ShipmentsPage = ({ title, filterStatus, emptyMessage }: ShipmentsPageProps // Dummy data for demonstration useEffect(() => { + // Fetch past shipments from API here + // const fetchShipments = async () => { + // try { + // const response = await fetch('/api/shipments/past'); // Replace with actual API endpoint + // const data: Shipment[] = await response.json(); + // setShipments(data); + // } catch (error) { + // console.error("Error fetching shipments:", error); + // } + // }; + + // fetchShipments(); + const dummyShipments: Shipment[] = [ { id: "1", @@ -126,7 +143,9 @@ const ShipmentsPage = ({ title, filterStatus, emptyMessage }: ShipmentsPageProps // Filtered shipments const filteredShipments = shipments.filter((shipment) => { // Only show shipments matching the filterStatus prop - const inStatusGroup = filterStatus.length === 0 || (shipment.status && filterStatus.includes(shipment.status)); + const inStatusGroup = + filterStatus.length === 0 || + (shipment.status && filterStatus.includes(shipment.status)); const matchesSearch = search === "" || shipment.description?.toLowerCase().includes(search.toLowerCase()) || @@ -135,8 +154,7 @@ const ShipmentsPage = ({ title, filterStatus, emptyMessage }: ShipmentsPageProps shipment.origin?.toLowerCase().includes(search.toLowerCase()) || shipment.destination?.toLowerCase().includes(search.toLowerCase()); const matchesStatus = - !status || - (shipment.status && shipment.status.toLowerCase() === status); + !status || (shipment.status && shipment.status.toLowerCase() === status); return inStatusGroup && matchesSearch && matchesStatus; }); @@ -154,19 +172,22 @@ const ShipmentsPage = ({ title, filterStatus, emptyMessage }: ShipmentsPageProps
setSearch(e.target.value)} + onChange={(e) => setSearch(e.target.value)} placeholder="Search by tracking number, description, origin, destination..." className="w-full md:w-1/2 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" /> setStatus(e.target.value)} + onChange={(e) => setStatus(e.target.value)} options={STATUS_OPTIONS} className="w-full md:w-1/4 rounded-lg border border-gray-300 dark:border-gray-700 px-4 py-2 text-gray-900 dark:text-gray-100 bg-white dark:bg-gray-800 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-700 transition-all" />
{filteredShipments.length > 0 ? ( - + ) : (

{emptyMessage || "No shipments found."}

)}