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/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/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 270322a..dee548e 100644 --- a/clientapp/src/features/shipments/PastShipments.tsx +++ b/clientapp/src/features/shipments/PastShipments.tsx @@ -1,4 +1,10 @@ import { useEffect, useState } from "react"; +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 @@ -8,38 +14,162 @@ 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 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.

)}
); diff --git a/clientapp/src/features/shipments/ShipmentsPage.tsx b/clientapp/src/features/shipments/ShipmentsPage.tsx new file mode 100644 index 0000000..ccf0e41 --- /dev/null +++ b/clientapp/src/features/shipments/ShipmentsPage.tsx @@ -0,0 +1,198 @@ +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(() => { + // 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", + }, + { + 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;