From 1d82ad3d6cf3a1df8e227cf1af99d61139db2b42 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Tue, 18 Nov 2025 13:40:16 +0300 Subject: [PATCH 1/3] feat: update teh service list --- .../api/service_order.py | 75 ++++ .../doctype/service_order/service_order.json | 7 +- .../doctype/service_order/service_order.py | 48 ++- .../src/components/layout/sidebar-menu.tsx | 2 +- .../src/components/schedule/gantt-view.tsx | 22 +- .../schedule/schedule-left-panel.tsx | 57 +-- .../components/schedule/technicians-view.tsx | 10 +- .../service-request/product-tracking.tsx | 337 ++++++++++++++++++ .../service-request/service-requests-view.tsx | 136 +++---- schedule/src/hooks/use-service-requests.ts | 14 +- schedule/src/pages/schedule/schedule.tsx | 39 +- schedule/src/pages/schedule/types.ts | 26 +- schedule/src/store/schedule-store.ts | 55 ++- 13 files changed, 661 insertions(+), 167 deletions(-) create mode 100644 beveren_fsm/field_service_management/api/service_order.py create mode 100644 schedule/src/components/service-request/product-tracking.tsx diff --git a/beveren_fsm/field_service_management/api/service_order.py b/beveren_fsm/field_service_management/api/service_order.py new file mode 100644 index 0000000..050a4bc --- /dev/null +++ b/beveren_fsm/field_service_management/api/service_order.py @@ -0,0 +1,75 @@ +import frappe +from frappe.utils import getdate + + +@frappe.whitelist() +def get_service_orders_for_tracking( + status=None, + start_date=None, + end_date=None, + search=None, + limit_page_length: int | None = 50, +): + filters = {"docstatus": ["!=", 2]} + + if status and status != "all": + filters["status"] = status + + if start_date and end_date: + filters["posting_date"] = ["between", [getdate(start_date), getdate(end_date)]] + elif start_date: + filters["posting_date"] = [">=", getdate(start_date)] + elif end_date: + filters["posting_date"] = ["<=", getdate(end_date)] + + or_filters = [] + if search: + search_term = f"%{search.strip()}%" + or_filters = [ + ["Service Order", "name", "like", search_term], + ["Service Order", "customer", "like", search_term], + ["Service Order", "serial_no", "like", search_term], + ["Service Order", "item_code", "like", search_term], + ] + + fields = [ + "name", + "customer", + "status", + "posting_date", + "due_date", + "serial_no", + "item_code", + "priority", + "type", + "product_location", + ] + + limit = int(limit_page_length) if limit_page_length else None + + orders = frappe.get_all( + "Service Order", + filters=filters, + or_filters=or_filters, + fields=fields, + order_by="posting_date desc", + limit_page_length=limit, + ) + + for order in orders: + doc = frappe.get_doc("Service Order", order.name) + order["product_movement"] = [ + { + "name": movement.name, + "movement_type": movement.movement_type, + "destination": movement.destination, + "movement_date": movement.movement_date, + "linked_document_type": movement.linked_document_type, + "linked_document": movement.linked_document, + "handled_by": movement.handled_by, + "service_order": order.name, + } + for movement in doc.product_movement + ] + + return orders diff --git a/beveren_fsm/field_service_management/doctype/service_order/service_order.json b/beveren_fsm/field_service_management/doctype/service_order/service_order.json index b8d9d58..52efde7 100644 --- a/beveren_fsm/field_service_management/doctype/service_order/service_order.json +++ b/beveren_fsm/field_service_management/doctype/service_order/service_order.json @@ -18,7 +18,6 @@ "column_break_llgp", "posting_date", "due_date", - "amc_contract", "column_break_ralr", "priority", "company", @@ -88,6 +87,7 @@ "column_break_gakd", "warranty_amc_status", "warranty_expiry_date", + "amc_contract", "amc_expiry_date", "appointment_preference_section", "preferred_date_1", @@ -756,6 +756,7 @@ "options": "Service Area" }, { + "depends_on": "eval:doc.warranty_amc_status==\"Under AMC\"", "fieldname": "amc_contract", "fieldtype": "Link", "label": "AMC Contract", @@ -813,6 +814,7 @@ "fieldtype": "Table", "ignore_user_permissions": 1, "label": "Product Tracker", + "no_copy": 1, "options": "Product Movement", "read_only": 1 }, @@ -822,6 +824,7 @@ "fieldname": "product_location", "fieldtype": "Data", "label": "Product Location", + "no_copy": 1, "read_only": 1 } ], @@ -837,7 +840,7 @@ "link_fieldname": "custom_reference_service_document" } ], - "modified": "2025-11-18 03:10:34.985668", + "modified": "2025-11-18 05:19:20.178318", "modified_by": "Administrator", "module": "Field Service Management", "name": "Service Order", diff --git a/beveren_fsm/field_service_management/doctype/service_order/service_order.py b/beveren_fsm/field_service_management/doctype/service_order/service_order.py index 941af31..66f53fd 100644 --- a/beveren_fsm/field_service_management/doctype/service_order/service_order.py +++ b/beveren_fsm/field_service_management/doctype/service_order/service_order.py @@ -5,7 +5,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from frappe.utils import flt, today +from frappe.utils import flt, getdate, today LOCATION_STATUS_MAP = { "delivered to customer": "Review", @@ -489,10 +489,12 @@ def make_delivery_note(service_order: str, items=None, product_location: str | N def make_purchase_receipt(service_order: str, items=None, product_location: str | None = None): order = frappe.get_doc("Service Order", service_order) - if not order.service_request: - frappe.throw(_("Service Order {0} is not linked to a Service Request").format(order.name)) - - service_request = frappe.get_doc("Service Request", order.service_request) + service_request = None + if order.service_request: + try: + service_request = frappe.get_doc("Service Request", order.service_request) + except frappe.DoesNotExistError: + service_request = None # if not service_request.repair_vendor: # frappe.throw( @@ -504,8 +506,12 @@ def make_purchase_receipt(service_order: str, items=None, product_location: str purchase_receipt = frappe.new_doc("Purchase Receipt") purchase_receipt.company = order.company purchase_receipt.posting_date = today() - purchase_receipt.supplier = service_request.repair_vendor - purchase_receipt.supplier_address = service_request.customer_address + purchase_receipt.supplier = getattr(order, "repair_vendor", None) or getattr( + service_request, "repair_vendor", None + ) + purchase_receipt.supplier_address = getattr(order, "supplier_address", None) or getattr( + service_request, "customer_address", None + ) purchase_receipt.tc_name = getattr(order, "tc_name", None) purchase_receipt.terms = getattr(order, "terms", None) purchase_receipt.custom_service_order = order.name @@ -656,10 +662,12 @@ def get(self, key, default=None): def make_purchase_order(service_order: str, items=None, product_location: str | None = None): order = frappe.get_doc("Service Order", service_order) - if not order.service_request: - frappe.throw(_("Service Order {0} is not linked to a Service Request").format(order.name)) - - service_request = frappe.get_doc("Service Request", order.service_request) + service_request = None + if order.service_request: + try: + service_request = frappe.get_doc("Service Request", order.service_request) + except frappe.DoesNotExistError: + service_request = None # if not service_request.repair_vendor: # frappe.throw( @@ -672,7 +680,9 @@ def make_purchase_order(service_order: str, items=None, product_location: str | purchase_order.company = order.company purchase_order.transaction_date = today() purchase_order.schedule_date = today() - purchase_order.supplier = service_request.repair_vendor + purchase_order.supplier = getattr(order, "repair_vendor", None) or getattr( + service_request, "repair_vendor", None + ) purchase_order.tc_name = getattr(order, "tc_name", None) purchase_order.terms = getattr(order, "terms", None) purchase_order.custom_service_order = order.name @@ -821,10 +831,12 @@ def get(self, key, default=None): def make_purchase_invoice(service_order: str, items=None, product_location: str | None = None): order = frappe.get_doc("Service Order", service_order) - if not order.service_request: - frappe.throw(_("Service Order {0} is not linked to a Service Request").format(order.name)) - - service_request = frappe.get_doc("Service Request", order.service_request) + service_request = None + if order.service_request: + try: + service_request = frappe.get_doc("Service Request", order.service_request) + except frappe.DoesNotExistError: + service_request = None # if not service_request.repair_vendor: # frappe.throw( @@ -836,7 +848,9 @@ def make_purchase_invoice(service_order: str, items=None, product_location: str purchase_invoice = frappe.new_doc("Purchase Invoice") purchase_invoice.company = order.company purchase_invoice.posting_date = today() - purchase_invoice.supplier = service_request.repair_vendor + purchase_invoice.supplier = getattr(order, "repair_vendor", None) or getattr( + service_request, "repair_vendor", None + ) purchase_invoice.tc_name = getattr(order, "tc_name", None) purchase_invoice.terms = getattr(order, "terms", None) purchase_invoice.custom_service_order = order.name diff --git a/schedule/src/components/layout/sidebar-menu.tsx b/schedule/src/components/layout/sidebar-menu.tsx index a2b028b..0af10f9 100644 --- a/schedule/src/components/layout/sidebar-menu.tsx +++ b/schedule/src/components/layout/sidebar-menu.tsx @@ -30,7 +30,7 @@ export function SidebarMenu({ const menuItems: MenuItem[] = [ { icon: Home, label: "Home", key: "home", onClick: onScheduleClick }, - { icon: ClipboardList, label: "Requests", key: "requests", onClick: onRequestsClick }, + { icon: ClipboardList, label: "Product Movement", key: "requests", onClick: onRequestsClick }, { icon: Users, label: "Technicians", key: "technicians", onClick: onTechniciansClick }, { icon: Settings, label: "Settings", key: "settings", onClick: onSettingsClick }, ]; diff --git a/schedule/src/components/schedule/gantt-view.tsx b/schedule/src/components/schedule/gantt-view.tsx index 81b42de..3ebe87b 100644 --- a/schedule/src/components/schedule/gantt-view.tsx +++ b/schedule/src/components/schedule/gantt-view.tsx @@ -552,11 +552,29 @@ export function GanttView({
- setCreateStart(new Date(e.target.value))} /> + setCreateStart(new Date(e.target.value))} + />
- setCreateFinish(new Date(e.target.value))} /> + setCreateFinish(new Date(e.target.value))} + />
diff --git a/schedule/src/components/schedule/schedule-left-panel.tsx b/schedule/src/components/schedule/schedule-left-panel.tsx index 1b035c9..373cb6e 100644 --- a/schedule/src/components/schedule/schedule-left-panel.tsx +++ b/schedule/src/components/schedule/schedule-left-panel.tsx @@ -25,18 +25,25 @@ import { fetchServiceOrderDetail } from "../../hooks/use-appointments"; const STATUS_OPTIONS: AppointmentStatus[] = [ "Open", - "Scheduled", - "Dispatched", - "In Progress", - "Completed", - "Cancelled" + "Quotation", + "Converted", + "Due Soon", + "Overdue", + "On Hold", + "Closed", ]; const getStatusColor = (status: AppointmentStatus): string => { const colors: Record = { Open: "bg-blue-100 text-blue-800 border-blue-300", + Quotation: "bg-indigo-100 text-indigo-800 border-indigo-300", + Converted: "bg-emerald-100 text-emerald-800 border-emerald-300", + "Due Soon": "bg-amber-100 text-amber-800 border-amber-300", + Overdue: "bg-red-100 text-red-800 border-red-300", + "On Hold": "bg-gray-200 text-gray-800 border-gray-300", + Closed: "bg-slate-100 text-slate-800 border-slate-300", Scheduled: "bg-blue-100 text-blue-800 border-blue-300", - Dispatched: "bg-orange-100 text-orange-800 border-orange-300", + Dispatched: "bg-purple-100 text-purple-800 border-purple-300", "In Progress": "bg-orange-100 text-orange-800 border-orange-300", Completed: "bg-green-100 text-green-800 border-green-300", Cancelled: "bg-gray-100 text-gray-800 border-gray-300", @@ -147,23 +154,18 @@ export function ScheduleLeftPanel({ } }; - const getOrderStatusColor = (status?: string) => { - if (!status) return "border-border text-muted-foreground"; - const normalized = status.toLowerCase(); - if (normalized.includes("open") || normalized.includes("pending")) { - return "bg-blue-50 text-blue-700 border-blue-200"; - } - if (normalized.includes("progress") || normalized.includes("dispatch")) { - return "bg-orange-50 text-orange-700 border-orange-200"; - } - if (normalized.includes("complete") || normalized.includes("closed")) { - return "bg-green-50 text-green-700 border-green-200"; - } - if (normalized.includes("cancel")) { - return "bg-gray-50 text-gray-600 border-gray-200"; - } - return "border-border text-muted-foreground"; - }; +const getOrderStatusColor = (status?: string) => { + if (!status) return "border-border text-muted-foreground"; + const normalized = status.toLowerCase(); + if (normalized === "open") return "bg-cyan-50 text-cyan-700 border-cyan-200"; + if (normalized === "scheduled") return "bg-blue-50 text-blue-700 border-blue-200"; + if (normalized === "dispatched") return "bg-purple-50 text-purple-700 border-purple-200"; + if (normalized === "in progress") return "bg-orange-50 text-orange-700 border-orange-200"; + if (normalized === "review") return "bg-pink-50 text-pink-700 border-pink-200"; + if (normalized === "completed") return "bg-green-50 text-green-700 border-green-200"; + if (normalized === "cancelled") return "bg-gray-100 text-gray-700 border-gray-200"; + return "border-border text-muted-foreground"; +}; const getOrderPriorityColor = (priority?: string) => { if (!priority) return "border-border text-muted-foreground"; @@ -567,9 +569,7 @@ export function ScheduleLeftPanel({ />
- - {appointment.service_order || appointment.name} - + {appointment.name}
+ {appointment.service_order && ( +

+ Order: {appointment.service_order} +

+ )}

{getShortDescription(appointment)}

diff --git a/schedule/src/components/schedule/technicians-view.tsx b/schedule/src/components/schedule/technicians-view.tsx index 168f6d8..a57fde6 100644 --- a/schedule/src/components/schedule/technicians-view.tsx +++ b/schedule/src/components/schedule/technicians-view.tsx @@ -126,11 +126,11 @@ export function TechniciansView({ {/* Technician Info */}
-

{technician.full_name}

+

{technician.full_name}

{techAppointments.length > 0 && ( - - {techAppointments.length} {techAppointments.length === 1 ? "appointment" : "appointments"} - + + {techAppointments.length} {techAppointments.length === 1 ? "appt" : "appts"} + )}
{technician.service_area && ( @@ -162,7 +162,7 @@ export function TechniciansView({ }} >
- + {appointment.service_order || appointment.name} = { + Open: "bg-cyan-100 text-cyan-800 border-cyan-300", + Scheduled: "bg-blue-100 text-blue-800 border-blue-300", + Dispatched: "bg-purple-100 text-purple-800 border-purple-300", + "In Progress": "bg-orange-100 text-orange-800 border-orange-300", + Review: "bg-pink-100 text-pink-800 border-pink-300", + Completed: "bg-green-100 text-green-800 border-green-300", + Cancelled: "bg-gray-200 text-gray-700 border-gray-300", +}; + +const formatDate = (value?: string) => { + if (!value) return "—"; + try { + return format(new Date(value), "MMM d, yyyy"); + } catch { + return value; + } +}; + +export function ServiceOrdersView() { + const [orders, setOrders] = useState([]); + const [selectedOrderId, setSelectedOrderId] = useState(null); + const [statusFilter, setStatusFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadOrders(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [statusFilter]); + + const loadOrders = async () => { + try { + setLoading(true); + const data = await fetchServiceOrdersForTracking({ + status: statusFilter, + limit: 100, + }); + setOrders(data); + if (!selectedOrderId && data.length) { + setSelectedOrderId(data[0].name); + } else if (selectedOrderId && !data.find((req) => req.name === selectedOrderId)) { + setSelectedOrderId(data[0]?.name ?? null); + } + } catch (error) { + console.error("Failed to load service orders", error); + } finally { + setLoading(false); + } + }; + + const filteredOrders = useMemo(() => { + if (!searchQuery.trim()) { + return orders; + } + const term = searchQuery.toLowerCase(); + return orders.filter((req) => { + return ( + req.name?.toLowerCase().includes(term) || + req.customer?.toLowerCase().includes(term) || + req.serial_no?.toLowerCase().includes(term) || + req.item_code?.toLowerCase().includes(term) || + req.product_movement?.some((mv: ServiceRequestMovement) => { + const dest = mv.destination?.toLowerCase() || ""; + const type = mv.movement_type?.toLowerCase() || ""; + const so = mv.service_order?.toLowerCase() || ""; + return dest.includes(term) || type.includes(term) || so.includes(term); + }) + ); + }); + }, [orders, searchQuery]); + + useEffect(() => { + if (!filteredOrders.length) { + setSelectedOrderId(null); + return; + } + if (!selectedOrderId || !filteredOrders.find((req) => req.name === selectedOrderId)) { + setSelectedOrderId(filteredOrders[0].name); + } + }, [filteredOrders, selectedOrderId]); + + const selectedOrder = + filteredOrders.find((req) => req.name === selectedOrderId) || filteredOrders[0] || null; + + return ( +
+ {/* Left Pane */} +
+
+
+
+

Service Orders

+

+ Track orders and their movement history +

+
+ {filteredOrders.length} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ +
+ +
+ +
+ {loading && + [1, 2, 3, 4].map((i) => ( +
+ + + +
+ ))} + + {!loading && filteredOrders.length === 0 && ( +
+ No service orders found +
+ )} + + {!loading && + filteredOrders.map((req) => { + const isActive = selectedOrder?.name === req.name; + const badgeColor = + statusColors[req.status] || "bg-gray-100 text-gray-800 border-gray-300"; + return ( + + ); + })} +
+
+
+ + {/* Right Pane */} +
+ {selectedOrder ? ( +
+
+
+ +

+ {selectedOrder.name} +

+ +
+ + {selectedOrder.status || "Unknown"} + +
+ +
+
+
+

Serial No

+

+ {selectedOrder.serial_no || "Not linked"} +

+
+
+

Item

+

+ {selectedOrder.item_code || "Not set"} +

+
+
+

Current Location

+

+ {selectedOrder.product_location || "Unknown"} +

+
+
+

Due Date

+

{formatDate(selectedOrder.due_date)}

+
+
+ + {selectedOrder.description && ( +
+
+

Notes

+
+

+ {selectedOrder.description} +

+
+ )} + +
+
+
+

Movement Tracker

+

+ Latest known location changes for this item +

+
+ + {selectedOrder.product_movement?.length || 0} entries + +
+ + {selectedOrder.product_movement && selectedOrder.product_movement.length ? ( +
+ + + + Date + Destination + Linked Document + Service Order + Handled By + + + + {[...selectedOrder.product_movement] + .sort((a, b) => { + const aTime = a.movement_date ? new Date(a.movement_date).getTime() : 0; + const bTime = b.movement_date ? new Date(b.movement_date).getTime() : 0; + return bTime - aTime; + }) + .map((movement) => ( + + {formatDate(movement.movement_date)} + {movement.destination || movement.movement_type || "—"} + + {movement.linked_document ? ( + + + {movement.linked_document_type || ""} + + {movement.linked_document} + + ) : ( + "—" + )} + + {movement.service_order || "—"} + {movement.handled_by || "—"} + + ))} + +
+
+ ) : ( +
+ No movement entries recorded yet for this request. +
+ )} +
+
+
+
+ ) : ( +
+ No service order selected +
+ )} +
+
+ ); +} diff --git a/schedule/src/components/service-request/service-requests-view.tsx b/schedule/src/components/service-request/service-requests-view.tsx index 18131a8..8d9557b 100644 --- a/schedule/src/components/service-request/service-requests-view.tsx +++ b/schedule/src/components/service-request/service-requests-view.tsx @@ -9,8 +9,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; import { Skeleton } from "../ui/skeleton"; import { RefreshCcw, Search } from "lucide-react"; -import { fetchServiceRequests } from "../../hooks/use-service-requests"; -import { ServiceRequest } from "../../pages/schedule/types"; +import { fetchServiceOrdersForTracking } from "../../hooks/use-service-requests"; +import { ServiceOrderSummary, ServiceRequestMovement } from "../../pages/schedule/types"; import { format } from "date-fns"; const STATUS_OPTIONS = [ @@ -42,71 +42,71 @@ const formatDate = (value?: string) => { } }; -export function ServiceRequestsView() { - const [requests, setRequests] = useState([]); - const [selectedRequestId, setSelectedRequestId] = useState(null); +export function ServiceOrdersView() { + const [orders, setOrders] = useState([]); + const [selectedOrderId, setSelectedOrderId] = useState(null); const [statusFilter, setStatusFilter] = useState("all"); const [searchQuery, setSearchQuery] = useState(""); const [loading, setLoading] = useState(false); useEffect(() => { - loadRequests(); + loadOrders(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [statusFilter]); - const loadRequests = async () => { + const loadOrders = async () => { try { setLoading(true); - const data = await fetchServiceRequests({ + const data = await fetchServiceOrdersForTracking({ status: statusFilter, limit: 100, }); - setRequests(data); - if (!selectedRequestId && data.length) { - setSelectedRequestId(data[0].name); - } else if (selectedRequestId && !data.find((req) => req.name === selectedRequestId)) { - setSelectedRequestId(data[0]?.name ?? null); + setOrders(data); + if (!selectedOrderId && data.length) { + setSelectedOrderId(data[0].name); + } else if (selectedOrderId && !data.find((req) => req.name === selectedOrderId)) { + setSelectedOrderId(data[0]?.name ?? null); } } catch (error) { - console.error("Failed to load service requests", error); + console.error("Failed to load service orders", error); } finally { setLoading(false); } }; - const filteredRequests = useMemo(() => { + const filteredOrders = useMemo(() => { if (!searchQuery.trim()) { - return requests; + return orders; } const term = searchQuery.toLowerCase(); - return requests.filter((req) => { + return orders.filter((req) => { return ( req.name?.toLowerCase().includes(term) || - req.subject?.toLowerCase().includes(term) || req.customer?.toLowerCase().includes(term) || req.serial_no?.toLowerCase().includes(term) || req.item_code?.toLowerCase().includes(term) || - req.product_movement?.some( - (mv) => - mv.destination?.toLowerCase().includes(term) || - mv.movement_type?.toLowerCase().includes(term) - ) + req.product_movement?.some((mv: ServiceRequestMovement) => { + const dest = mv.destination?.toLowerCase() || ""; + const type = mv.movement_type?.toLowerCase() || ""; + const so = mv.service_order?.toLowerCase() || ""; + return dest.includes(term) || type.includes(term) || so.includes(term); + }) ); }); - }, [requests, searchQuery]); + }, [orders, searchQuery]); useEffect(() => { - if (!filteredRequests.length) { - setSelectedRequestId(null); + if (!filteredOrders.length) { + setSelectedOrderId(null); return; } - if (!selectedRequestId || !filteredRequests.find((req) => req.name === selectedRequestId)) { - setSelectedRequestId(filteredRequests[0].name); + if (!selectedOrderId || !filteredOrders.find((req) => req.name === selectedOrderId)) { + setSelectedOrderId(filteredOrders[0].name); } - }, [filteredRequests, selectedRequestId]); + }, [filteredOrders, selectedOrderId]); - const selectedRequest = - filteredRequests.find((req) => req.name === selectedRequestId) || filteredRequests[0] || null; + const selectedOrder = + filteredOrders.find((req) => req.name === selectedOrderId) || filteredOrders[0] || null; return (
@@ -115,12 +115,12 @@ export function ServiceRequestsView() {
-

Service Requests

+

Service Orders

- Track requests and their movement history + Track orders and their movement history

- {filteredRequests.length} + {filteredOrders.length}
@@ -132,7 +132,7 @@ export function ServiceRequestsView() { className="pl-9" />
-
@@ -161,15 +161,15 @@ export function ServiceRequestsView() {
))} - {!loading && filteredRequests.length === 0 && ( + {!loading && filteredOrders.length === 0 && (
No service requests found
)} {!loading && - filteredRequests.map((req) => { - const isActive = selectedRequest?.name === req.name; + filteredOrders.map((req) => { + const isActive = selectedOrder?.name === req.name; const badgeColor = statusColors[req.status] || "bg-gray-100 text-gray-800 border-gray-300"; return ( @@ -178,15 +178,15 @@ export function ServiceRequestsView() { className={`w-full text-left border rounded-lg p-3 transition-colors ${ isActive ? "border-primary bg-primary/5" : "hover:bg-muted/50" }`} - onClick={() => setSelectedRequestId(req.name)} + onClick={() => setSelectedOrderId(req.name)} > -
-

- {req.subject || req.name} -

- - {req.status} - +
+

+ {req.name} +

+ + {req.status || "Unknown"} +

{req.customer || "No customer"} @@ -207,22 +207,22 @@ export function ServiceRequestsView() { {/* Right Pane */}

- {selectedRequest ? ( + {selectedOrder ? (

- Service Request + Service Order

- {selectedRequest.subject || selectedRequest.name} + {selectedOrder.name}

- {selectedRequest.customer || "No customer specified"} + {selectedOrder.customer || "No customer specified"}

- - {selectedRequest.status} + + {selectedOrder.status || "Unknown"}
@@ -231,34 +231,34 @@ export function ServiceRequestsView() {

Serial No

- {selectedRequest.serial_no || "Not linked"} + {selectedOrder.serial_no || "Not linked"}

Item

- {selectedRequest.item_code || "Not set"} + {selectedOrder.item_code || "Not set"}

Current Location

- {selectedRequest.current_product_location || "Unknown"} + {selectedOrder.current_product_location || "Unknown"}

Due Date

-

{formatDate(selectedRequest.due_date)}

+

{formatDate(selectedOrder.due_date)}

- {selectedRequest.description && ( + {selectedOrder.description && (

Notes

- {selectedRequest.description} + {selectedOrder.description}

)} @@ -272,24 +272,24 @@ export function ServiceRequestsView() {

- {selectedRequest.product_movement?.length || 0} entries + {selectedOrder.product_movement?.length || 0} entries
- {selectedRequest.product_movement && selectedRequest.product_movement.length ? ( + {selectedOrder.product_movement && selectedOrder.product_movement.length ? (
Date - Current Location Destination Linked Document + Service Order Handled By - {[...selectedRequest.product_movement] + {[...selectedOrder.product_movement] .sort((a, b) => { const aTime = a.movement_date ? new Date(a.movement_date).getTime() : 0; const bTime = b.movement_date ? new Date(b.movement_date).getTime() : 0; @@ -298,18 +298,20 @@ export function ServiceRequestsView() { .map((movement) => ( {formatDate(movement.movement_date)} - {movement.movement_type || "—"} - {movement.destination || "—"} + {movement.destination || movement.movement_type || "—"} {movement.linked_document ? ( - - {movement.linked_document_type || ""}{" "} + + + {movement.linked_document_type || ""} + {movement.linked_document} ) : ( "—" )} + {movement.service_order || "—"} {movement.handled_by || "—"} ))} @@ -327,7 +329,7 @@ export function ServiceRequestsView() { ) : (
- No service request selected + No service order selected
)} diff --git a/schedule/src/hooks/use-service-requests.ts b/schedule/src/hooks/use-service-requests.ts index 6939c67..3d6ec09 100644 --- a/schedule/src/hooks/use-service-requests.ts +++ b/schedule/src/hooks/use-service-requests.ts @@ -1,6 +1,6 @@ -import { ServiceRequest } from "../pages/schedule/types"; +import { ServiceOrderSummary } from "../pages/schedule/types"; -interface ServiceRequestFilters { +interface ServiceOrderFilters { status?: string; startDate?: Date | null; endDate?: Date | null; @@ -8,7 +8,9 @@ interface ServiceRequestFilters { limit?: number; } -export async function fetchServiceRequests(filters: ServiceRequestFilters = {}): Promise { +export async function fetchServiceOrdersForTracking( + filters: ServiceOrderFilters = {} +): Promise { try { //eslint-disable-next-line @typescript-eslint/no-explicit-any const csrfToken = (window as any).csrf_token; @@ -34,7 +36,7 @@ export async function fetchServiceRequests(filters: ServiceRequestFilters = {}): params.append("limit_page_length", String(filters.limit)); } - const url = `/api/method/beveren_fsm.field_service_management.api.service_request.get_service_requests?${ + const url = `/api/method/beveren_fsm.field_service_management.api.service_order.get_service_orders_for_tracking?${ params.toString() }`; @@ -48,13 +50,13 @@ export async function fetchServiceRequests(filters: ServiceRequestFilters = {}): }); if (!response.ok) { - throw new Error(`Failed to fetch service requests: ${response.statusText}`); + throw new Error(`Failed to fetch service orders: ${response.statusText}`); } const result = await response.json(); return result.message || []; } catch (error) { - console.error("Error fetching service requests:", error); + console.error("Error fetching service orders:", error); throw error; } } diff --git a/schedule/src/pages/schedule/schedule.tsx b/schedule/src/pages/schedule/schedule.tsx index ae671be..a719b04 100644 --- a/schedule/src/pages/schedule/schedule.tsx +++ b/schedule/src/pages/schedule/schedule.tsx @@ -8,8 +8,8 @@ import { SidebarMenu } from "../../components/layout/sidebar-menu"; import { useScheduleStore } from "../../store"; import { fetchAppointmentsWithFilter, fetchServiceOrders } from "../../hooks/use-appointments"; import { Toaster } from "../../components/ui/sonner"; -import { useEffect, useState } from "react"; -import { ServiceRequestsView } from "../../components/service-request/service-requests-view"; +import { useCallback, useEffect, useState } from "react"; +import { ServiceOrdersView } from "../../components/service-request/product-tracking"; export default function SchedulePage() { const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr'); @@ -64,20 +64,11 @@ export default function SchedulePage() { setRequestsView, setServiceOrders, setServiceOrdersLoading, - toggleAppointmentSelection, selectAllAppointments, clearSelectedAppointments, } = useScheduleStore(); - useEffect(() => { - loadAppointments(); - }, [appointmentDateRange.startDate, appointmentDateRange.endDate, statusFilter]); - - useEffect(() => { - loadServiceOrders(); - }, []); - - const loadAppointments = async () => { + const loadAppointments = useCallback(async () => { try { setLoading(true); const data = await fetchAppointmentsWithFilter( @@ -91,9 +82,15 @@ export default function SchedulePage() { } finally { setLoading(false); } - }; + }, [ + appointmentDateRange.startDate, + appointmentDateRange.endDate, + statusFilter, + setAppointments, + setLoading, + ]); - const loadServiceOrders = async () => { + const loadServiceOrders = useCallback(async () => { try { setServiceOrdersLoading(true); const data = await fetchServiceOrders(); @@ -103,7 +100,15 @@ export default function SchedulePage() { } finally { setServiceOrdersLoading(false); } - }; + }, [setServiceOrders, setServiceOrdersLoading]); + + useEffect(() => { + loadAppointments(); + }, [loadAppointments]); + + useEffect(() => { + loadServiceOrders(); + }, [loadServiceOrders]); const handleAppointmentSelect = (appointmentId: string, checked: boolean) => { if (checked) { @@ -169,9 +174,9 @@ export default function SchedulePage() {
setSettingsView(false)} />
- ) : requestsView ? ( + ) : requestsView ? (
- +
) : ( <> diff --git a/schedule/src/pages/schedule/types.ts b/schedule/src/pages/schedule/types.ts index 92188d3..d6f603c 100644 --- a/schedule/src/pages/schedule/types.ts +++ b/schedule/src/pages/schedule/types.ts @@ -32,6 +32,12 @@ export type ViewType = "gantt" | "grid" | "maps" | "calendar"; export type AppointmentStatus = | "Open" + | "Quotation" + | "Converted" + | "Due Soon" + | "Overdue" + | "On Hold" + | "Closed" | "Scheduled" | "Dispatched" | "In Progress" @@ -49,30 +55,23 @@ export interface ServiceRequestMovement { service_order?: string; } -export interface ServiceRequest { +export interface ServiceOrderSummary { name: string; subject?: string; customer?: string; - status: string; + status?: string; + priority?: string; posting_date?: string; due_date?: string; + type?: string; serial_no?: string; item_code?: string; - item_name?: string; current_product_location?: string; + product_location?: string; description?: string; product_movement?: ServiceRequestMovement[]; } -export interface ServiceOrderSummary { - name: string; - customer?: string; - status?: string; - priority?: string; - posting_date?: string; - type?: string; -} - export interface ServiceOrderItem { item_code?: string; item_name?: string; @@ -94,8 +93,5 @@ export interface ServiceOrderDetail extends ServiceOrderSummary { service_area?: string; items?: ServiceOrderItem[]; notes?: string; - priority?: string; - status?: string; customer_address?: string; - posting_date?: string; } diff --git a/schedule/src/store/schedule-store.ts b/schedule/src/store/schedule-store.ts index 510a6b2..326f4ba 100644 --- a/schedule/src/store/schedule-store.ts +++ b/schedule/src/store/schedule-store.ts @@ -29,7 +29,7 @@ interface ScheduleState { leftPanelView: "appointments" | "technicians"; // Track left panel view mode leftListMode: "orders" | "appointments"; // Track list content within schedule view settingsView: boolean; // Track if settings view is open - requestsView: boolean; // Track if service requests view is active + requestsView: boolean; // Track if service orders tracker view is active // Actions setAppointments: (appointments: Appointment[]) => void; @@ -71,10 +71,23 @@ export const useScheduleStore = create((set, get) => ({ statusFilter: "all", serviceOrderStatusFilter: "all", viewType: getInitialViewType(), - leftPanelView: "appointments", - leftListMode: "orders", - settingsView: false, // Settings view closed by default - requestsView: false, + leftPanelView: ((): "appointments" | "technicians" => { + if (typeof window === "undefined") return "appointments"; + const saved = localStorage.getItem("schedule-left-panel-view"); + return saved === "technicians" ? "technicians" : "appointments"; + })(), + leftListMode: + (typeof window !== "undefined" && localStorage.getItem("schedule-left-list-mode") === "appointments") + ? "appointments" + : (typeof window !== "undefined" && localStorage.getItem("schedule-left-list-mode") === "orders") + ? "orders" + : "appointments", + settingsView: + (typeof window !== "undefined" && localStorage.getItem("schedule-settings-view") === "1") || + false, + requestsView: + (typeof window !== "undefined" && localStorage.getItem("schedule-requests-view") === "1") || + false, // Actions setAppointments: (appointments) => set({ appointments }), @@ -110,10 +123,34 @@ export const useScheduleStore = create((set, get) => ({ localStorage.setItem("schedule-view-type", view); } }, - setLeftPanelView: (view) => set({ leftPanelView: view }), - setLeftListMode: (mode) => set({ leftListMode: mode }), - setSettingsView: (open) => set({ settingsView: open }), - setRequestsView: (open) => set({ requestsView: open }), + setLeftPanelView: (view) => + set(() => { + if (typeof window !== "undefined") { + localStorage.setItem("schedule-left-panel-view", view); + } + return { leftPanelView: view }; + }), + setLeftListMode: (mode) => + set(() => { + if (typeof window !== "undefined") { + localStorage.setItem("schedule-left-list-mode", mode); + } + return { leftListMode: mode }; + }), + setSettingsView: (open) => + set(() => { + if (typeof window !== "undefined") { + localStorage.setItem("schedule-settings-view", open ? "1" : "0"); + } + return { settingsView: open }; + }), + setRequestsView: (open) => + set(() => { + if (typeof window !== "undefined") { + localStorage.setItem("schedule-requests-view", open ? "1" : "0"); + } + return { requestsView: open }; + }), // Helper getters isAppointmentSelected: (appointmentId) => { From f7833a87429a9f2ab110c793f9c87b98a622743d Mon Sep 17 00:00:00 2001 From: maniamartial Date: Tue, 18 Nov 2025 13:53:09 +0300 Subject: [PATCH 2/3] feat: Update the behaviour --- .../src/components/schedule/gantt-view.tsx | 15 ++++++ .../schedule/service-order-detail-sheet.tsx | 49 ++++++++++++++----- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/schedule/src/components/schedule/gantt-view.tsx b/schedule/src/components/schedule/gantt-view.tsx index 3ebe87b..0aa2c36 100644 --- a/schedule/src/components/schedule/gantt-view.tsx +++ b/schedule/src/components/schedule/gantt-view.tsx @@ -76,6 +76,21 @@ export function GanttView({ loadTechnicians(); }, []); + useEffect(() => { + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ service_order: string; customer?: string }>).detail; + if (!detail) return; + setCreateServiceOrder(detail.service_order); + if (detail.customer) { + setCreateCustomer(detail.customer); + } + setCreateOpen(true); + }; + + window.addEventListener("open-create-appointment", handler); + return () => window.removeEventListener("open-create-appointment", handler); + }, []); + const loadTechnicians = async () => { try { const data = await fetchTechnicians(); diff --git a/schedule/src/components/schedule/service-order-detail-sheet.tsx b/schedule/src/components/schedule/service-order-detail-sheet.tsx index e3c037a..a2b4af5 100644 --- a/schedule/src/components/schedule/service-order-detail-sheet.tsx +++ b/schedule/src/components/schedule/service-order-detail-sheet.tsx @@ -7,6 +7,7 @@ import { SheetHeader, SheetTitle, } from "../ui/sheet"; +import { Button } from "../ui/button"; import { Badge } from "../ui/badge"; import { Separator } from "../ui/separator"; import { format } from "date-fns"; @@ -23,18 +24,13 @@ interface ServiceOrderDetailSheetProps { const getStatusBadgeColor = (status?: string) => { if (!status) return "border-border text-muted-foreground"; const normalized = status.toLowerCase(); - if (normalized.includes("open") || normalized.includes("pending")) { - return "bg-blue-100 text-blue-800 border-blue-200"; - } - if (normalized.includes("progress") || normalized.includes("dispatch")) { - return "bg-orange-100 text-orange-800 border-orange-200"; - } - if (normalized.includes("complete") || normalized.includes("close")) { - return "bg-green-100 text-green-800 border-green-200"; - } - if (normalized.includes("cancel")) { - return "bg-gray-100 text-gray-700 border-gray-200"; - } + if (normalized === "open") return "bg-cyan-50 text-cyan-700 border-cyan-200"; + if (normalized === "scheduled") return "bg-blue-50 text-blue-700 border-blue-200"; + if (normalized === "dispatched") return "bg-purple-50 text-purple-700 border-purple-200"; + if (normalized === "in progress") return "bg-orange-50 text-orange-700 border-orange-200"; + if (normalized === "review") return "bg-pink-50 text-pink-700 border-pink-200"; + if (normalized === "completed") return "bg-green-50 text-green-700 border-green-200"; + if (normalized === "cancelled") return "bg-gray-100 text-gray-700 border-gray-200"; return "border-border text-muted-foreground"; }; @@ -61,6 +57,23 @@ export function ServiceOrderDetailSheet({
Service Order Details {order?.name} + {order?.status?.toLowerCase() === "open" && ( + + )}
{order?.status && ( @@ -92,6 +105,18 @@ export function ServiceOrderDetailSheet({ {order.type} )} + {order.product_location && ( +
+ Product Location + {order.product_location} +
+ )} + {order.current_product_location && ( +
+ Current Location + {order.current_product_location} +
+ )} {formatDate(order.posting_date) && (
Posting Date From ba9185078a55686a947a34de2ef189eadddf4781 Mon Sep 17 00:00:00 2001 From: maniamartial Date: Tue, 18 Nov 2025 14:54:10 +0300 Subject: [PATCH 3/3] feat: update the miscrosfit of appointment button --- .../schedule/schedule-left-panel.tsx | 51 +++++++------------ schedule/src/store/schedule-store.ts | 8 ++- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/schedule/src/components/schedule/schedule-left-panel.tsx b/schedule/src/components/schedule/schedule-left-panel.tsx index 373cb6e..bcf9b88 100644 --- a/schedule/src/components/schedule/schedule-left-panel.tsx +++ b/schedule/src/components/schedule/schedule-left-panel.tsx @@ -12,7 +12,6 @@ import { ServiceOrderSummary, } from "../../pages/schedule/types"; import { Skeleton } from "../ui/skeleton"; -import { AppointmentDetailSheet } from "./appointment-detail-sheet"; import { ServiceOrderDetailSheet } from "./service-order-detail-sheet"; import { MassActionsDropdown } from "./mass-actions-dropdown"; import { Filter, CalendarIcon } from "lucide-react"; @@ -90,7 +89,6 @@ export function ScheduleLeftPanel({ serviceOrders, serviceOrdersLoading, }: ScheduleLeftPanelProps) { - const [selectedAppointment, setSelectedAppointment] = useState(null); const [selectedServiceOrder, setSelectedServiceOrder] = useState(null); const [serviceOrderSheetOpen, setServiceOrderSheetOpen] = useState(false); const [serviceOrderDetailLoading, setServiceOrderDetailLoading] = useState(false); @@ -140,11 +138,6 @@ export function ScheduleLeftPanel({ return "No description"; }; - const handleAppointmentClick = (appointment: Appointment) => { - setSelectedAppointment(appointment); - onAppointmentClick(appointment); - }; - const formatOrderDate = (dateString?: string) => { if (!dateString) return null; try { @@ -287,33 +280,31 @@ const getOrderStatusColor = (status?: string) => { <>
{/* Header */} -
-
-
+
+
+

{isOrderMode ? "Service Orders" : "Service Appointments"}

-
-
{isOrderMode ? serviceOrders.length : appointments.length} -
+ {isOrderMode ? (
@@ -532,7 +523,7 @@ const getOrderStatusColor = (status?: string) => { ${isSelected ? "bg-primary/5 border-primary" : "hover:bg-muted/50"} ${isCompleted ? "opacity-80" : ""} `} - onClick={() => handleAppointmentClick(appointment)} + onClick={() => onAppointmentClick(appointment)} draggable={!isCompleted} onDragStart={(e) => { if (isCompleted) { @@ -617,14 +608,6 @@ const getOrderStatusColor = (status?: string) => {
{/* Detail Sheet */} - {selectedAppointment && ( - !open && setSelectedAppointment(null)} - /> - )} - ((set, get) => ({ return saved === "technicians" ? "technicians" : "appointments"; })(), leftListMode: - (typeof window !== "undefined" && localStorage.getItem("schedule-left-list-mode") === "appointments") - ? "appointments" - : (typeof window !== "undefined" && localStorage.getItem("schedule-left-list-mode") === "orders") - ? "orders" - : "appointments", + typeof window !== "undefined" && localStorage.getItem("schedule-left-list-mode") + ? (localStorage.getItem("schedule-left-list-mode") as "orders" | "appointments") + : "orders", settingsView: (typeof window !== "undefined" && localStorage.getItem("schedule-settings-view") === "1") || false,