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..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();
@@ -552,11 +567,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/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
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) => {