From 3ea904166194e1c0da9676408bb0219ac6690774 Mon Sep 17 00:00:00 2001 From: Harish1604 Date: Wed, 12 Mar 2025 18:13:32 +0530 Subject: [PATCH 1/3] Employee page added --- backend/main/settings.py | 6 +- frontend/app/employee/page.tsx | 129 ++++++++++++++++++ .../components/employee/CancelOrderDialog.tsx | 65 +++++++++ .../components/employee/DeliveryStatus.tsx | 50 +++++++ frontend/components/employee/OrdersTable.tsx | 91 ++++++++++++ .../employee/UndeliverableOrders.tsx | 38 ++++++ frontend/components/employee/types.tsx | 22 +++ frontend/components/ui/checkbox.tsx | 30 ++++ frontend/components/ui/dialog.tsx | 122 +++++++++++++++++ frontend/components/ui/radio_group.tsx | 44 ++++++ 10 files changed, 594 insertions(+), 3 deletions(-) create mode 100644 frontend/app/employee/page.tsx create mode 100644 frontend/components/employee/CancelOrderDialog.tsx create mode 100644 frontend/components/employee/DeliveryStatus.tsx create mode 100644 frontend/components/employee/OrdersTable.tsx create mode 100644 frontend/components/employee/UndeliverableOrders.tsx create mode 100644 frontend/components/employee/types.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/radio_group.tsx diff --git a/backend/main/settings.py b/backend/main/settings.py index de2474d..8af251b 100644 --- a/backend/main/settings.py +++ b/backend/main/settings.py @@ -83,9 +83,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'database_name', - 'USER': 'user_name', - 'PASSWORD': 'password', + 'NAME': 'sony', + 'USER': 'sonyin', + 'PASSWORD': 'steamers', 'HOST': 'localhost', # If running PostgreSQL locally 'PORT': '5432', # Default PostgreSQL port } diff --git a/frontend/app/employee/page.tsx b/frontend/app/employee/page.tsx new file mode 100644 index 0000000..4bd54f6 --- /dev/null +++ b/frontend/app/employee/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import React from 'react'; +import { OrdersTable } from '@/components/employee/OrdersTable'; +import { UndeliverableOrders } from '@/components/employee/UndeliverableOrders'; +import { DeliveryStatus } from '@/components/employee/DeliveryStatus'; +import { CancelOrderDialog } from '@/components/employee/CancelOrderDialog'; +import { DeliveryOrder, UndeliverableOrder } from '@/components/employee/types'; + +interface PageProps { + params: { + id: string; + }; +} + +// Sample data generator based on employee ID +const generateOrdersForEmployee = (employeeId: string): DeliveryOrder[] => { + return [ + { + orderId: `ORD-${employeeId}-001`, + orderName: "John Smith", + phoneNumber: "555-0123", + address: "123 Main St, City, State", + isDelivered: false, + items: ["Laptop", "Mouse"] + }, + { + orderId: `ORD-${employeeId}-002`, + orderName: "Jane Doe", + phoneNumber: "555-0124", + address: "456 Oak St, City, State", + isDelivered: true, + items: ["Keyboard", "Monitor"] + } + ]; +}; + +const generateUndeliverableOrdersForEmployee = (employeeId: string): UndeliverableOrder[] => { + return [ + { + orderId: `ORD-${employeeId}-006`, + name: "Alice Johnson", + phone: "555-0128", + }, + { + orderId: `ORD-${employeeId}-007`, + name: "Tom Davis", + phone: "555-0129", + } + ]; +}; + +export default function EmployeePage({ params }: PageProps) { + const [mounted, setMounted] = React.useState(false); + const [dialogOpen, setDialogOpen] = React.useState(false); + const [selectedOrderId, setSelectedOrderId] = React.useState(null); + const [selectedReason, setSelectedReason] = React.useState(""); + const [orders, setOrders] = React.useState(generateOrdersForEmployee(params.id)); + const [undeliverableOrders] = React.useState( + generateUndeliverableOrdersForEmployee(params.id) + ); + + React.useEffect(() => { + setMounted(true); + }, []); + + const handleCancelClick = (orderId: string) => { + setSelectedOrderId(orderId); + setDialogOpen(true); + }; + + const handleCancelOrder = () => { + if (selectedOrderId && selectedReason) { + setOrders(prevOrders => + prevOrders.map(order => + order.orderId === selectedOrderId + ? { ...order, isCancelled: true, cancellationReason: selectedReason } + : order + ) + ); + setDialogOpen(false); + setSelectedOrderId(null); + setSelectedReason(""); + } + }; + + const calculatePieChartData = React.useMemo(() => { + const deliveredCount = orders.filter(order => order.isDelivered && !order.isCancelled).length; + const notDeliveredCount = orders.filter(order => !order.isDelivered && !order.isCancelled).length; + const cancelledCount = orders.filter(order => order.isCancelled).length; + const undeliverableCount = undeliverableOrders.length; + + return [ + { name: 'Delivered', value: deliveredCount, color: '#22c55e' }, + { name: 'Pending', value: notDeliveredCount, color: '#3b82f6' }, + { name: 'Cancelled', value: cancelledCount, color: '#eab308' }, + { name: 'Undeliverable', value: undeliverableCount, color: '#94a3b8' } + ]; + }, [orders, undeliverableOrders]); + + return ( +
+

Employee Dashboard - ID: {params.id}

+ + + + + +
+ + {mounted && ( + + )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/employee/CancelOrderDialog.tsx b/frontend/components/employee/CancelOrderDialog.tsx new file mode 100644 index 0000000..3d4891d --- /dev/null +++ b/frontend/components/employee/CancelOrderDialog.tsx @@ -0,0 +1,65 @@ +// components/employee/CancelOrderDialog.tsx (continued) +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio_group"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Check } from "lucide-react"; + +interface CancelOrderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedReason: string; + onReasonChange: (reason: string) => void; + onConfirm: () => void; +} + +export const CancelOrderDialog = ({ + open, + onOpenChange, + selectedReason, + onReasonChange, + onConfirm +}: CancelOrderDialogProps) => { + return ( + + + + Cancel Order + +
+ +
+ + {selectedReason === "customer_choice" && ( + + )} + + +
+
+ + {selectedReason === "other_incident" && ( + + )} + + +
+
+
+ +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/components/employee/DeliveryStatus.tsx b/frontend/components/employee/DeliveryStatus.tsx new file mode 100644 index 0000000..53b71ce --- /dev/null +++ b/frontend/components/employee/DeliveryStatus.tsx @@ -0,0 +1,50 @@ +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { PieChart, Pie, Cell, Legend, ResponsiveContainer } from 'recharts'; +import { PieChartData } from "./types"; + +interface DeliveryStatusProps { + data: PieChartData[]; + totalOrders: number; +} + +export const DeliveryStatus = ({ data, totalOrders }: DeliveryStatusProps) => { + return ( + + + Delivery Status + + +
+ + + + {data.map((entry, index) => ( + + ))} + + {value}} + /> + + +
+ +
+
+ Total Orders: {totalOrders} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/components/employee/OrdersTable.tsx b/frontend/components/employee/OrdersTable.tsx new file mode 100644 index 0000000..ef97a37 --- /dev/null +++ b/frontend/components/employee/OrdersTable.tsx @@ -0,0 +1,91 @@ +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { BadgeCheck, XCircle, AlertCircle } from "lucide-react"; +import { DeliveryOrder } from "./types"; + +interface OrdersTableProps { + orders: DeliveryOrder[]; + onCancelClick: (orderId: string) => void; +} + +export const OrdersTable = ({ orders, onCancelClick }: OrdersTableProps) => { + const getStatusColor = (order: DeliveryOrder) => { + if (order.isCancelled) return "text-yellow-500"; + if (order.isDelivered) return "text-green-500"; + return "text-blue-500"; + }; + + const getStatusIcon = (order: DeliveryOrder) => { + if (order.isCancelled) return ; + if (order.isDelivered) return ; + return ; + }; + + return ( + + + Today's Deliveries + + +
+ + + + + + + + + + + + + + {orders.map((order) => ( + + + + + + + + + + ))} + +
StatusOrder IDOrder NamePhone NumberAddressItemsActions
+
+ {getStatusIcon(order)} + + {order.isCancelled ? 'Cancelled' : order.isDelivered ? 'Delivered' : 'Pending'} + +
+
{order.orderId}{order.orderName}{order.phoneNumber}{order.address} + {order.items?.join(", ")} + + {!order.isCancelled && !order.isDelivered && ( + + )} + {order.isDelivered && ( + + Cannot be cancelled + + )} + {order.isCancelled && ( + + Reason: {order.cancellationReason === 'customer_choice' ? 'Customer Choice' : 'Other Incident'} + + )} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/components/employee/UndeliverableOrders.tsx b/frontend/components/employee/UndeliverableOrders.tsx new file mode 100644 index 0000000..f12e5aa --- /dev/null +++ b/frontend/components/employee/UndeliverableOrders.tsx @@ -0,0 +1,38 @@ +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { UndeliverableOrder } from "./types"; + +interface UndeliverableOrdersProps { + orders: UndeliverableOrder[]; +} + +export const UndeliverableOrders = ({ orders }: UndeliverableOrdersProps) => { + return ( + + + Cannot be delivered today + + +
+ + + + + + + + + + {orders.map((order) => ( + + + + + + ))} + +
Order IDNamePhone
{order.orderId}{order.name}{order.phone}
+
+
+
+ ); +}; diff --git a/frontend/components/employee/types.tsx b/frontend/components/employee/types.tsx new file mode 100644 index 0000000..c63e9de --- /dev/null +++ b/frontend/components/employee/types.tsx @@ -0,0 +1,22 @@ +export interface PieChartData { + name: string; + value: number; + color: string; +} + +export interface DeliveryOrder { + orderId: string; + orderName: string; + phoneNumber: string; + address: string; + isDelivered: boolean; + isCancelled?: boolean; + cancellationReason?: string; + items: string[]; +} + +export interface UndeliverableOrder { + orderId: string; + name: string; + phone: string; +} \ No newline at end of file diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx new file mode 100644 index 0000000..f855ec6 --- /dev/null +++ b/frontend/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = "Checkbox" + +export { Checkbox } \ No newline at end of file diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx new file mode 100644 index 0000000..7203b5c --- /dev/null +++ b/frontend/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} \ No newline at end of file diff --git a/frontend/components/ui/radio_group.tsx b/frontend/components/ui/radio_group.tsx new file mode 100644 index 0000000..0968c2a --- /dev/null +++ b/frontend/components/ui/radio_group.tsx @@ -0,0 +1,44 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } \ No newline at end of file From ec11138dd251194ab1deedb49c1780df753f49a3 Mon Sep 17 00:00:00 2001 From: Harish1604 Date: Fri, 21 Mar 2025 03:35:28 +0530 Subject: [PATCH 2/3] Added employe id and completed the orders details section in the employee dashboard --- backend/app/urls.py | 5 +- backend/app/views.py | 14 +- backend/main/settings.py | 6 +- frontend/app/employee/page.tsx | 128 ++++++++++----- frontend/components/employee/OrderDetails.tsx | 152 ++++++++++++++++++ .../employee/UndeliverableOrders.tsx | 38 ----- 6 files changed, 260 insertions(+), 83 deletions(-) create mode 100644 frontend/components/employee/OrderDetails.tsx delete mode 100644 frontend/components/employee/UndeliverableOrders.tsx diff --git a/backend/app/urls.py b/backend/app/urls.py index c29d169..7932ac5 100644 --- a/backend/app/urls.py +++ b/backend/app/urls.py @@ -2,7 +2,7 @@ from rest_framework_simplejwt.views import TokenRefreshView from rest_framework.urlpatterns import format_suffix_patterns # ✅ For better API format handling from .views import ( - CustomAuthToken, logout_view, get_employees, get_retailers,get_counts, + CustomAuthToken, get_employee_id, logout_view, get_employees, get_retailers,get_counts, get_orders,get_users,get_employee_orders,get_employee_shipments,update_shipment_status,get_logged_in_user,allocate_orders, get_trucks, get_shipments,get_stock_data,category_stock_data,store_qr_code ) @@ -29,7 +29,8 @@ path('user_detail/', get_logged_in_user, name='get_logged_in_user'), path('employee_shipments/', get_employee_shipments, name='employee_shipments'), path('update_shipment_status/', update_shipment_status, name='update-shipment-status'), - path('employee_orders/', get_employee_orders, name='get_employee_orders') + path('employee_orders/', get_employee_orders, name='get_employee_orders'), + path('employee_id/', get_employee_id, name='get_employee_id') ] # ✅ Support API requests with format suffixes (e.g., /orders.json, /orders.xml) diff --git a/backend/app/views.py b/backend/app/views.py index 4928663..f35d71d 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -363,4 +363,16 @@ def get_employee_orders(request): return Response(serializer.data) def redirect_view(request): - return redirect('/admin/') \ No newline at end of file + return redirect('/admin/') + + +@api_view(['GET']) +@permission_classes([IsAuthenticated,IsEmployeeUser]) +def get_employee_id(request): + user = request.user + + try: + employee = Employee.objects.get(user=user) # Get employee linked to logged-in user + return Response({"employee_id": employee.employee_id}) # ✅ Use employee_id instead of id + except Employee.DoesNotExist: + return Response({"error": "Employee not found"}, status=404) \ No newline at end of file diff --git a/backend/main/settings.py b/backend/main/settings.py index d96d6da..8af251b 100644 --- a/backend/main/settings.py +++ b/backend/main/settings.py @@ -83,9 +83,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'your_database_name', - 'USER': 'your_database_user', - 'PASSWORD': 'your_database_password', + 'NAME': 'sony', + 'USER': 'sonyin', + 'PASSWORD': 'steamers', 'HOST': 'localhost', # If running PostgreSQL locally 'PORT': '5432', # Default PostgreSQL port } diff --git a/frontend/app/employee/page.tsx b/frontend/app/employee/page.tsx index b0f9e7b..6cfe9df 100644 --- a/frontend/app/employee/page.tsx +++ b/frontend/app/employee/page.tsx @@ -2,10 +2,10 @@ import React, { useEffect, useState, useMemo, useCallback } from "react"; import { OrdersTable } from "@/components/employee/OrdersTable"; -import { UndeliverableOrders } from "@/components/employee/UndeliverableOrders"; +import { OrderDetails } from "@/components/employee/OrderDetails"; import { DeliveryStatus } from "@/components/employee/DeliveryStatus"; import { CancelOrderDialog } from "@/components/employee/CancelOrderDialog"; -import { DeliveryOrder, UndeliverableOrder } from "@/components/employee/types"; +import { DeliveryOrder } from "@/components/employee/types"; interface PageProps { params: { @@ -13,7 +13,7 @@ interface PageProps { }; } -const API_URL = "http://127.0.0.1:8000/api"; +const API_URL = process.env.REACT_APP_API_URL || "http://127.0.0.1:8000/api"; const getRefreshToken = () => localStorage.getItem("refresh_token"); @@ -53,7 +53,9 @@ export default function EmployeePage({ params }: PageProps) { const [selectedOrderId, setSelectedOrderId] = useState(null); const [selectedReason, setSelectedReason] = useState(""); const [orders, setOrders] = useState([]); - const [undeliverableOrders, setUndeliverableOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [employeeId, setEmployeeId] = useState(null); const getAuthToken = useCallback(async () => { let token = localStorage.getItem("access_token"); @@ -65,7 +67,8 @@ export default function EmployeePage({ params }: PageProps) { const fetchWithAuth = async (url: string, options: RequestInit = {}) => { let token = await getAuthToken(); - if (!token) throw new Error("Authentication token not found. Please log in again."); + if (!token) + throw new Error("Authentication token not found. Please log in again."); const response = await fetch(url, { ...options, @@ -78,7 +81,8 @@ export default function EmployeePage({ params }: PageProps) { if (response.status === 401) { token = await refreshAccessToken(); - if (!token) throw new Error("Authentication token not found. Please log in again."); + if (!token) + throw new Error("Authentication token not found. Please log in again."); return fetch(url, { ...options, @@ -94,34 +98,61 @@ export default function EmployeePage({ params }: PageProps) { }; useEffect(() => { + async function fetchEmployeeId() { + try { + const response = await fetchWithAuth(`${API_URL}/employee_id/`); + if (!response.ok) { + throw new Error("Failed to fetch employee ID"); + } + const data = await response.json(); + setEmployeeId(data.employee_id); + } catch (error) { + setError( + error instanceof Error ? error.message : "Unknown error occurred" + ); + } + } + + fetchEmployeeId(); setMounted(true); }, []); useEffect(() => { async function fetchShipments() { + if (!employeeId) return; + try { - const response = await fetchWithAuth(`${API_URL}/employee_shipments?employeeId=${params.id}`); + setLoading(true); + setError(null); + const response = await fetchWithAuth( + `${API_URL}/employee_shipments?employeeId=${employeeId}` + ); const data = await response.json(); const mappedOrders: DeliveryOrder[] = data.map((shipment: any) => ({ orderId: `SHIP-${shipment.shipment_id}`, orderName: `Order-${shipment.order}`, - phoneNumber: "N/A", // No phone number in API, set default or fetch separately - address: "N/A", // No address in API, set default or fetch separately + phoneNumber: "N/A", + address: "N/A", isDelivered: shipment.status === "delivered", - items: [`Order-${shipment.order}`], // Assuming order details are not available in shipment data + items: [`Order-${shipment.order}`], isCancelled: shipment.status === "cancelled", - cancellationReason: shipment.status === "cancelled" ? "Unknown" : undefined, + cancellationReason: + shipment.status === "cancelled" ? "Unknown" : undefined, })); setOrders(mappedOrders); } catch (error) { - console.error("Failed to fetch shipments:", error); + setError( + error instanceof Error ? error.message : "Unknown error occurred" + ); + } finally { + setLoading(false); } } fetchShipments(); - }, [params.id]); + }, [employeeId]); const handleCancelClick = (orderId: string) => { setSelectedOrderId(orderId); @@ -130,10 +161,14 @@ export default function EmployeePage({ params }: PageProps) { const handleCancelOrder = () => { if (selectedOrderId && selectedReason) { - setOrders(prevOrders => - prevOrders.map(order => + setOrders((prevOrders) => + prevOrders.map((order) => order.orderId === selectedOrderId - ? { ...order, isCancelled: true, cancellationReason: selectedReason } + ? { + ...order, + isCancelled: true, + cancellationReason: selectedReason, + } : order ) ); @@ -145,49 +180,60 @@ export default function EmployeePage({ params }: PageProps) { const handleUpdateStatus = async (shipmentId: number) => { try { - const response = await fetchWithAuth(`${API_URL}/update_shipment_status/`, { - method: "POST", - body: JSON.stringify({ - shipment_id: shipmentId, - status: "delivered", - }), - }); + const response = await fetchWithAuth( + `${API_URL}/update_shipment_status/`, + { + method: "POST", + body: JSON.stringify({ + shipment_id: shipmentId, + status: "delivered", + }), + } + ); if (!response.ok) { throw new Error(`Failed to update status: ${response.statusText}`); } - // Update the local state to reflect the change - setOrders(prevOrders => - prevOrders.map(order => + setOrders((prevOrders) => + prevOrders.map((order) => order.orderId === `SHIP-${shipmentId}` ? { ...order, isDelivered: true } : order ) ); } catch (error) { - console.error("Failed to update shipment status:", error); + setError( + error instanceof Error ? error.message : "Unknown error occurred" + ); } }; const calculatePieChartData = useMemo(() => { - const deliveredCount = orders.filter(order => order.isDelivered && !order.isCancelled).length; - const notDeliveredCount = orders.filter(order => !order.isDelivered && !order.isCancelled).length; - const cancelledCount = orders.filter(order => order.isCancelled).length; - const undeliverableCount = undeliverableOrders.length; + const deliveredCount = orders.filter( + (order) => order.isDelivered && !order.isCancelled + ).length; + const notDeliveredCount = orders.filter( + (order) => !order.isDelivered && !order.isCancelled + ).length; + const cancelledCount = orders.filter((order) => order.isCancelled).length; return [ { name: "Delivered", value: deliveredCount, color: "#22c55e" }, { name: "Pending", value: notDeliveredCount, color: "#3b82f6" }, { name: "Cancelled", value: cancelledCount, color: "#eab308" }, - { name: "Undeliverable", value: undeliverableCount, color: "#94a3b8" } ]; - }, [orders, undeliverableOrders]); + }, [orders]); return (
-

Employee Dashboard - ID: {params.id}

- +

+ Employee Dashboard - ID : {employeeId || "Loading..."} +

+ + {error &&

Error: {error}

} + {loading &&

Loading shipments...

} + - +
- + {mounted && ( )}
); -} \ No newline at end of file +} diff --git a/frontend/components/employee/OrderDetails.tsx b/frontend/components/employee/OrderDetails.tsx new file mode 100644 index 0000000..8a255db --- /dev/null +++ b/frontend/components/employee/OrderDetails.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from "react"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; + +interface Order { + order_id: number; + required_qty: number; + order_date: string; + status: string; + retailer: number; + product: number; +} + +export const OrderDetails = () => { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchOrders = async () => { + try { + let token = localStorage.getItem("access_token"); + if (!token) throw new Error("No authentication token found"); + + let response = await fetchOrdersWithToken(token); + + if (response.status === 401) { + console.log("Access token expired. Trying to refresh..."); + const newToken = await refreshAccessToken(); + + if (newToken) { + localStorage.setItem("access_token", newToken); + response = await fetchOrdersWithToken(newToken); // Retry with new token + } else { + throw new Error("Failed to refresh token"); + } + } + + const data: Order[] = await response.json(); + if (!Array.isArray(data)) throw new Error("Invalid data format from API"); + + setOrders(data); + } catch (error) { + setError( + error instanceof Error ? error.message : "Unknown error occurred" + ); + } finally { + setLoading(false); + } + }; + + const fetchOrdersWithToken = async (token: string) => { + return fetch("http://127.0.0.1:8000/api/employee_orders/", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + }; + + const refreshAccessToken = async (): Promise => { + const refreshToken = localStorage.getItem("refresh_token"); + if (!refreshToken) return null; + + try { + const response = await fetch("http://127.0.0.1:8000/api/token/refresh/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ refresh: refreshToken }), + }); + + if (!response.ok) { + console.error("Failed to refresh token"); + return null; + } + + const data = await response.json(); + return data.access; // Return the new access token + } catch (error) { + console.error("Error refreshing token:", error); + return null; + } + }; + + useEffect(() => { + fetchOrders(); + const interval = setInterval(fetchOrders, 5000); // Poll every 10 seconds + return () => clearInterval(interval); + }, []); + + return ( + + + Orders Details + + +
+ {loading ? ( +

Loading orders...

+ ) : error ? ( +

Error: {error}

+ ) : orders.length === 0 ? ( +

No orders allocated.

+ ) : ( + + + + + + + + + + + + + {orders.map((order) => ( + + + + + + + + + ))} + +
+ Order ID + + Required Quantity + + Order Date + + Retailer ID + + Product ID + + Status +
{order.order_id}{order.required_qty} + {new Date(order.order_date).toLocaleString()} + {order.retailer}{order.product}{order.status}
+ )} +
+
+
+ ); +}; diff --git a/frontend/components/employee/UndeliverableOrders.tsx b/frontend/components/employee/UndeliverableOrders.tsx deleted file mode 100644 index f12e5aa..0000000 --- a/frontend/components/employee/UndeliverableOrders.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; -import { UndeliverableOrder } from "./types"; - -interface UndeliverableOrdersProps { - orders: UndeliverableOrder[]; -} - -export const UndeliverableOrders = ({ orders }: UndeliverableOrdersProps) => { - return ( - - - Cannot be delivered today - - -
- - - - - - - - - - {orders.map((order) => ( - - - - - - ))} - -
Order IDNamePhone
{order.orderId}{order.name}{order.phone}
-
-
-
- ); -}; From d17031b303beeb2f1874754a1c4dd33b65cacb42 Mon Sep 17 00:00:00 2001 From: Harish1604 Date: Mon, 24 Mar 2025 10:22:32 +0530 Subject: [PATCH 3/3] Added notification --- backend/app/urls.py | 6 +- backend/app/views.py | 25 +- backend/main/settings.py | 6 +- frontend/app/manufacturer/page.tsx | 292 ++++++++++-------- frontend/components/employee/OrdersTable.tsx | 4 +- .../manufacturer/stockcount/SidePanel.tsx | 2 +- 6 files changed, 203 insertions(+), 132 deletions(-) diff --git a/backend/app/urls.py b/backend/app/urls.py index 7932ac5..27c416f 100644 --- a/backend/app/urls.py +++ b/backend/app/urls.py @@ -3,7 +3,7 @@ from rest_framework.urlpatterns import format_suffix_patterns # ✅ For better API format handling from .views import ( CustomAuthToken, get_employee_id, logout_view, get_employees, get_retailers,get_counts, - get_orders,get_users,get_employee_orders,get_employee_shipments,update_shipment_status,get_logged_in_user,allocate_orders, get_trucks, get_shipments,get_stock_data,category_stock_data,store_qr_code + get_orders,get_users,get_employee_orders,get_employee_shipments, recent_actions,update_shipment_status,get_logged_in_user,allocate_orders, get_trucks, get_shipments,get_stock_data,category_stock_data,store_qr_code ) urlpatterns = [ @@ -30,7 +30,9 @@ path('employee_shipments/', get_employee_shipments, name='employee_shipments'), path('update_shipment_status/', update_shipment_status, name='update-shipment-status'), path('employee_orders/', get_employee_orders, name='get_employee_orders'), - path('employee_id/', get_employee_id, name='get_employee_id') + path('employee_id/', get_employee_id, name='get_employee_id'), + + path('recent_actions/', recent_actions, name='recent_actions') ] # ✅ Support API requests with format suffixes (e.g., /orders.json, /orders.xml) diff --git a/backend/app/views.py b/backend/app/views.py index f35d71d..784e11b 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -20,6 +20,7 @@ from django.contrib.auth.models import User from django.http import JsonResponse from .permissions import IsEmployeeUser +from django.contrib.admin.models import LogEntry; # ✅ Custom Pagination Class class StandardPagination(PageNumberPagination): @@ -375,4 +376,26 @@ def get_employee_id(request): employee = Employee.objects.get(user=user) # Get employee linked to logged-in user return Response({"employee_id": employee.employee_id}) # ✅ Use employee_id instead of id except Employee.DoesNotExist: - return Response({"error": "Employee not found"}, status=404) \ No newline at end of file + return Response({"error": "Employee not found"}, status=404) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated, IsAdminUser]) +def recent_actions(request): + # Fetch the last 10 actions performed in the admin panel + actions = LogEntry.objects.select_related('content_type', 'user').order_by('-action_time')[:10] + + # Prepare JSON response + recent_actions_list = [ + { + 'time': action.action_time, + 'user': action.user.username, + 'content_type': action.content_type.model, + 'object_id': action.object_id, + 'object_repr': action.object_repr, + 'action_flag': action.get_action_flag_display(), + } + for action in actions + ] + + return Response({'recent_actions': recent_actions_list}) \ No newline at end of file diff --git a/backend/main/settings.py b/backend/main/settings.py index 6c493f0..68f4908 100644 --- a/backend/main/settings.py +++ b/backend/main/settings.py @@ -83,9 +83,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'database_name', - 'USER': 'username', - 'PASSWORD': 'password', + 'NAME': 'sony', + 'USER': 'sonyin', + 'PASSWORD': 'steamers', 'HOST': 'localhost', # If running PostgreSQL locally 'PORT': '5432', # Default PostgreSQL port diff --git a/frontend/app/manufacturer/page.tsx b/frontend/app/manufacturer/page.tsx index 0c09b27..cd45b20 100644 --- a/frontend/app/manufacturer/page.tsx +++ b/frontend/app/manufacturer/page.tsx @@ -36,6 +36,9 @@ import { UsersIcon, } from "lucide-react"; +// API URL for fetching counts +const API_URL = "http://127.0.0.1:8000/api"; + // Interfaces interface OverviewCard { totalOrders: number; @@ -86,6 +89,13 @@ interface ShipmentResponse { results: Shipment[]; } +type Payment = { + id: string; + amount: number; + status: "pending" | "processing" | "success" | "failed"; + email: string; +}; + // Hardcoded data for fallback const testData: OverviewCard = { totalOrders: 0, @@ -128,24 +138,6 @@ const chartConfig = { }, } satisfies ChartConfig; -const notifications: Notification[] = [ - { id: 1, message: "New order placed: #12345", date: "2025-02-15" }, - { id: 2, message: "Low stock alert for Store #24", date: "2025-02-14" }, - { - id: 3, - message: "Delivery agent #12 completed order #67890", - date: "2025-02-14", - }, - { id: 4, message: "New customer feedback received", date: "2025-02-13" }, -]; - -type Payment = { - id: string; - amount: number; - status: "pending" | "processing" | "success" | "failed"; - email: string; -}; - export const payments: Payment[] = [ { id: "728ed52f", @@ -179,21 +171,110 @@ export const payments: Payment[] = [ }, ]; -// API URL for fetching counts -const API_URL = "http://127.0.0.1:8000/api"; - const Dashboard: React.FC = () => { + // State for overview data const [overviewData, setOverviewData] = useState(testData); - const [analytics] = useState(analyticsData); - const [reports] = useState(reportData); - const [notif] = useState(notifications); + const [analytics, setAnalytics] = useState(analyticsData); + const [reports, setReports] = useState(reportData); + + // Loading and error states const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + + // Shipment states const [shipments, setShipments] = useState([]); const [shipmentsLoading, setShipmentsLoading] = useState(true); const [shipmentsError, setShipmentsError] = useState(null); + + // Allocate order states const [allocateLoading, setAllocateLoading] = useState(false); const [allocateError, setAllocateError] = useState(null); + + // Notification states + const [notifications, setNotifications] = useState([]); + const [notificationsLoading, setNotificationsLoading] = + useState(true); + + // Authentication utilities + const getRefreshToken = useCallback(() => { + return localStorage.getItem("refresh_token"); + }, []); + + const refreshAccessToken = useCallback(async (): Promise => { + const refreshToken = getRefreshToken(); + if (!refreshToken) return null; + + try { + const response = await fetch(`${API_URL}/token/refresh/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh: refreshToken }), + }); + + if (!response.ok) { + console.error("Failed to refresh token"); + localStorage.removeItem("access_token"); + localStorage.removeItem("refresh_token"); + return null; + } + + const data = await response.json(); + if (data.access) { + localStorage.setItem("access_token", data.access); + return data.access; + } + } catch (error) { + console.error("Error refreshing token:", error); + } + + return null; + }, [getRefreshToken]); + + const getAuthToken = useCallback(async (): Promise => { + const token = localStorage.getItem("access_token"); + if (!token) { + return await refreshAccessToken(); + } + return token; + }, [refreshAccessToken]); + + const fetchWithAuth = useCallback( + async (url: string, options: RequestInit = {}) => { + let token = await getAuthToken(); + if (!token) + throw new Error("Authentication token not found. Please log in again."); + + const response = await fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (response.status === 401) { + token = await refreshAccessToken(); + if (!token) + throw new Error( + "Authentication token not found. Please log in again." + ); + + return fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + } + + return response; + }, + [getAuthToken, refreshAccessToken] + ); + // Define columns for the shipment data table const shipmentColumns = [ { @@ -245,83 +326,11 @@ const Dashboard: React.FC = () => { }, ]; - // Get auth token with error handling - const getAuthToken = useCallback(() => { - const token = localStorage.getItem("access_token"); - if (!token) { - refreshAccessToken(); - } - return token; - }, []); - - const getRefreshToken = () => localStorage.getItem("refresh_token"); - - const refreshAccessToken = async (): Promise => { - const refreshToken = getRefreshToken(); - if (!refreshToken) return null; - - try { - const response = await fetch(`${API_URL}/token/refresh/`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ refresh: refreshToken }), - }); - - if (!response.ok) { - console.error("Failed to refresh token"); - localStorage.removeItem("access_token"); - localStorage.removeItem("refresh_token"); - return null; - } - - const data = await response.json(); - if (data.access) { - localStorage.setItem("access_token", data.access); - return data.access; - } - } catch (error) { - console.error("Error refreshing token:", error); - } - - return null; - }; - const fetchWithAuth = async (url: string, options: RequestInit = {}) => { - let token = await getAuthToken(); - if (!token) throw new Error("Authentication token not found. Please log in again."); - - const response = await fetch(url, { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }); - - if (response.status === 401) { - token = await refreshAccessToken(); - if (!token) throw new Error("Authentication token not found. Please log in again."); - - return fetch(url, { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }); - } - - return response; - }; - // Fetch shipments data with improved error handling const fetchShipments = useCallback(async () => { try { setShipmentsLoading(true); - const token = getAuthToken(); - - const response = await fetchWithAuth("http://127.0.0.1:8000/api/shipments/"); + const response = await fetchWithAuth(`${API_URL}/shipments/`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -339,16 +348,54 @@ const Dashboard: React.FC = () => { } finally { setShipmentsLoading(false); } - }, [getAuthToken]); + }, [fetchWithAuth]); + + // Fetch notifications + // Update the fetchNotifications function to use fetchWithAuth + const fetchNotifications = useCallback(async () => { + try { + setNotificationsLoading(true); + const response = await fetchWithAuth(`${API_URL}/recent_actions/`); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.detail || `Server responded with status ${response.status}` + ); + } + + const data = await response.json(); + const formattedData = data.recent_actions.map( + ( + action: { + action_flag: any; + content_type: any; + object_repr: any; + time: string; + }, + index: number + ) => ({ + id: index + 1, + message: `${action.action_flag} on ${action.content_type}: ${action.object_repr}`, + date: action.time.split("T")[0], // Extract only the date + }) + ); + + setNotifications(formattedData); + } catch (error) { + console.error("Error fetching notifications:", error); + } finally { + setNotificationsLoading(false); + } + }, [fetchWithAuth]); // Handle allocate orders with improved error handling and loading state const handleAllocateOrders = async () => { try { setAllocateLoading(true); setAllocateError(null); - const token = getAuthToken(); - console.log(token); - const response = await fetchWithAuth("http://127.0.0.1:8000/api/allocate-orders/", { + + const response = await fetchWithAuth(`${API_URL}/allocate-orders/`, { method: "POST", body: JSON.stringify({}), }); @@ -359,7 +406,8 @@ const Dashboard: React.FC = () => { setAllocateError(errorData.error); // Set the specific error message } else { throw new Error( - errorData.detail || `Server responded with status ${response.status}` + errorData.detail || + `Server responded with status ${response.status}` ); } } else { @@ -370,7 +418,7 @@ const Dashboard: React.FC = () => { } catch (err) { console.error("Error allocating orders:", err); if (!allocateError) { - alert(`Failed to allocate orders: ${(err as Error).message}`); + setAllocateError((err as Error).message); } } finally { setAllocateLoading(false); @@ -381,8 +429,6 @@ const Dashboard: React.FC = () => { const fetchCounts = useCallback(async () => { try { setLoading(true); - const token = getAuthToken(); - const response = await fetchWithAuth(`${API_URL}/count`); if (!response.ok) { @@ -393,12 +439,12 @@ const Dashboard: React.FC = () => { } const countsData: CountsResponse = await response.json(); - setOverviewData((prevData) => ({ - totalOrders: countsData.orders_placed, // Keep existing value since it's not in the API + setOverviewData({ + totalOrders: countsData.orders_placed, numStores: countsData.retailers_available, deliveryAgents: countsData.employees_available, pendingOrders: countsData.pending_orders, - })); + }); setError(null); } catch (err) { @@ -407,24 +453,27 @@ const Dashboard: React.FC = () => { } finally { setLoading(false); } - }, [getAuthToken]); + }, [fetchWithAuth]); - // Set up polling with cleanup + // Set up data fetching and polling useEffect(() => { // Initial fetch fetchCounts(); fetchShipments(); + fetchNotifications(); // Set up polling const countsIntervalId = setInterval(fetchCounts, 5000); const shipmentsIntervalId = setInterval(fetchShipments, 30000); + const notificationsIntervalId = setInterval(fetchNotifications, 10000); // Clean up intervals on unmount return () => { clearInterval(countsIntervalId); clearInterval(shipmentsIntervalId); + clearInterval(notificationsIntervalId); }; - }, [fetchCounts, fetchShipments]); + }, [fetchCounts, fetchShipments, fetchNotifications]); return (
@@ -456,8 +505,6 @@ const Dashboard: React.FC = () => {
)} - - {/* Other Errors */} {error && !error.includes("Authentication") && (
@@ -556,13 +603,12 @@ const Dashboard: React.FC = () => {

- Order Details - + Order + Details

)} - {shipmentsLoading && ( -

Loading transaction data...

- )} + + {shipmentsError && (
@@ -686,15 +731,16 @@ const Dashboard: React.FC = () => {
- {/* Notifications Tab */}

Recent Notifications

- {notif.length > 0 ? ( + {notificationsLoading ? ( +

+ ) : notifications.length > 0 ? (
    - {notif.map((note) => ( + {notifications.map((note) => (
  • {note.message}

    {note.date}

    diff --git a/frontend/components/employee/OrdersTable.tsx b/frontend/components/employee/OrdersTable.tsx index e93dd74..17ce3da 100644 --- a/frontend/components/employee/OrdersTable.tsx +++ b/frontend/components/employee/OrdersTable.tsx @@ -62,14 +62,14 @@ export const OrdersTable = ({ orders, onCancelClick, onUpdateStatus }: OrdersTab {!order.isCancelled && !order.isDelivered && ( <> - + */}