diff --git a/beveren_fsm/field_service_management/api/schedule.py b/beveren_fsm/field_service_management/api/schedule.py
index 177f495..1bf7d60 100644
--- a/beveren_fsm/field_service_management/api/schedule.py
+++ b/beveren_fsm/field_service_management/api/schedule.py
@@ -3,6 +3,28 @@
import frappe
+@frappe.whitelist()
+def get_unassigned_service_orders(limit=50):
+ filters = {"docstatus": 1}
+ assigned_orders = frappe.get_all(
+ "Service Appointment",
+ filters={"service_order": ["is", "set"], "docstatus": ["!=", 2]},
+ pluck="service_order",
+ limit_page_length=0,
+ )
+ if assigned_orders:
+ filters["name"] = ["not in", assigned_orders]
+
+ orders = frappe.get_all(
+ "Service Order",
+ fields=["name", "customer", "priority", "posting_date", "status", "type"],
+ filters=filters,
+ order_by="posting_date desc",
+ limit_page_length=limit,
+ )
+ return orders
+
+
@frappe.whitelist()
def create_appointment_from_api(
posting_date,
diff --git a/beveren_fsm/field_service_management/api/service_appointment.py b/beveren_fsm/field_service_management/api/service_appointment.py
index 033c7ea..c7e861b 100644
--- a/beveren_fsm/field_service_management/api/service_appointment.py
+++ b/beveren_fsm/field_service_management/api/service_appointment.py
@@ -59,7 +59,7 @@ def get_appointments(
"Service Appointment",
filters=filters,
fields=requested_fields,
- order_by="scheduled_start_datetime asc",
+ order_by="scheduled_start_datetime desc",
limit_page_length=limit_page_length if limit_page_length > 0 else None,
)
diff --git a/beveren_fsm/field_service_management/api/service_request.py b/beveren_fsm/field_service_management/api/service_request.py
new file mode 100644
index 0000000..e85b1b4
--- /dev/null
+++ b/beveren_fsm/field_service_management/api/service_request.py
@@ -0,0 +1,77 @@
+import frappe
+from frappe.utils import getdate
+
+
+@frappe.whitelist()
+def get_service_requests(
+ 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 Request", "name", "like", search_term],
+ ["Service Request", "customer", "like", search_term],
+ ["Service Request", "serial_no", "like", search_term],
+ ["Service Request", "item_code", "like", search_term],
+ ]
+
+ fields = [
+ "name",
+ "subject",
+ "customer",
+ "status",
+ "posting_date",
+ "due_date",
+ "serial_no",
+ "item_code",
+ "item_name",
+ "current_product_location",
+ "description",
+ ]
+
+ limit = int(limit_page_length) if limit_page_length else None
+
+ requests = frappe.get_all(
+ "Service Request",
+ filters=filters,
+ or_filters=or_filters,
+ fields=fields,
+ order_by="posting_date desc",
+ limit_page_length=limit,
+ )
+
+ results = []
+ for req in requests:
+ doc = frappe.get_doc("Service Request", req.name)
+ req["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,
+ }
+ for movement in doc.product_movement
+ ]
+ results.append(req)
+
+ return results
diff --git a/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json b/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json
index 3f7255f..ba4e42c 100644
--- a/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json
+++ b/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json
@@ -78,6 +78,8 @@
"fetch_from": "movement_type.destination",
"fieldname": "destination",
"fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
"label": "Destination"
}
],
@@ -85,7 +87,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2025-11-12 04:12:53.341700",
+ "modified": "2025-11-17 04:21:43.163670",
"modified_by": "Administrator",
"module": "Field Service Management",
"name": "Product Movement",
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 0d85a86..fde64e1 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
@@ -774,7 +774,8 @@
{
"fieldname": "service_total",
"fieldtype": "Currency",
- "label": "Service Total"
+ "label": "Service Total",
+ "read_only": 1
},
{
"default": "0",
@@ -796,7 +797,8 @@
{
"fieldname": "spareparts_total",
"fieldtype": "Currency",
- "label": "SpareParts Total"
+ "label": "SpareParts Total",
+ "read_only": 1
},
{
"allow_on_submit": 1,
@@ -819,7 +821,7 @@
"link_fieldname": "custom_reference_service_document"
}
],
- "modified": "2025-11-12 03:31:42.698792",
+ "modified": "2025-11-17 04:08:15.600873",
"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 e3fef51..3d905aa 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
@@ -8,8 +8,8 @@
from frappe.utils import flt, today
LOCATION_STATUS_MAP = {
- "delivered to customer": "Completed",
- "deliver to customer": "Completed", # fallback for legacy value
+ "delivered to customer": "Review",
+ "deliver to customer": "Review", # fallback for legacy value
"receive from vendor": "In Progress",
"receive from customer": "In Progress",
"received from customer": "In Progress",
diff --git a/beveren_fsm/field_service_management/doctype/service_request/service_request.json b/beveren_fsm/field_service_management/doctype/service_request/service_request.json
index 555ab68..d607288 100644
--- a/beveren_fsm/field_service_management/doctype/service_request/service_request.json
+++ b/beveren_fsm/field_service_management/doctype/service_request/service_request.json
@@ -310,6 +310,7 @@
"label": "Warranty Expiry Date"
},
{
+ "depends_on": "amc_contract",
"fetch_from": "serial_no.amc_expiry_date",
"fetch_if_empty": 1,
"fieldname": "amc_expiry_date",
@@ -335,6 +336,7 @@
"fieldname": "repair_vendor",
"fieldtype": "Link",
"label": "Repair Vendor",
+ "no_copy": 1,
"options": "Supplier"
},
{
@@ -343,6 +345,7 @@
"fieldname": "current_product_location",
"fieldtype": "Link",
"label": "Current Product Location",
+ "no_copy": 1,
"options": "Product Location"
},
{
@@ -355,6 +358,7 @@
"fieldname": "product_movement",
"fieldtype": "Table",
"label": "Product Movement",
+ "no_copy": 1,
"options": "Product Movement",
"read_only": 1
}
@@ -371,7 +375,7 @@
"link_fieldname": "service_request"
}
],
- "modified": "2025-11-17 03:13:18.909917",
+ "modified": "2025-11-17 04:17:34.797562",
"modified_by": "Administrator",
"module": "Field Service Management",
"name": "Service Request",
diff --git a/beveren_fsm/field_service_management/page/__init__.py b/beveren_fsm/field_service_management/page/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/beveren_fsm/field_service_management/page/dispatch/__init__.py b/beveren_fsm/field_service_management/page/dispatch/__init__.py
deleted file mode 100644
index 2d638b2..0000000
--- a/beveren_fsm/field_service_management/page/dispatch/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def get_context(context):
- pass
diff --git a/beveren_fsm/field_service_management/page/dispatch/dispatch.css b/beveren_fsm/field_service_management/page/dispatch/dispatch.css
deleted file mode 100644
index 4c26eab..0000000
--- a/beveren_fsm/field_service_management/page/dispatch/dispatch.css
+++ /dev/null
@@ -1,441 +0,0 @@
-
-/* Full height layout */
-html, body {
- height: 100%;
- margin: 0;
- padding: 0;
-}
-
-.dispatch-page {
- font-family: "Open Sans", "Roboto", "Helvetica Neue", Arial, sans-serif;
- background-color: #f5f7fa;
- color: #444;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-/* Navbar */
-.dispatch-navbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background-color: #fff;
- border-bottom: 1px solid #e0e0e0;
- box-shadow: 0 2px 3px rgba(0, 0, 0, 0.05);
- height: 56px;
- padding: 0 1rem;
- flex-shrink: 0;
-}
-
-.sidebar-toggle-btn {
- background: transparent;
- border: none;
- color: #666;
- font-size: 1.2rem;
- cursor: pointer;
- padding: 6px 8px;
- border-radius: 4px;
- transition: background-color 0.2s, color 0.2s;
-}
-
-.sidebar-toggle-btn:hover {
- background-color: #e8f2ff;
- color: #0080ff;
-}
-
-.nav-view-list {
- list-style: none;
- display: flex;
- margin: 0;
- padding: 0;
-}
-
-.nav-view-list li {
- margin-left: 0.75rem;
-}
-
-.nav-view-btn {
- background-color: transparent;
- border: none;
- font-weight: 500;
- padding: 8px 12px;
- border-radius: 4px;
- color: #555;
- cursor: pointer;
- transition: background-color 0.2s, color 0.2s, border-bottom 0.2s;
- outline: none;
- font-size: 0.95rem;
-}
-
-.nav-view-btn:hover {
- background-color: #e8f2ff;
- color: #0080ff;
-}
-
-.nav-view-btn.active {
- color: #0080ff;
- border-bottom: 2px solid #0080ff;
-}
-
-/* Body layout */
-.dispatch-body {
- flex: 1;
- display: flex;
- height: calc(100% - 56px);
- overflow: hidden;
-}
-
-/* Sidebar with its own scroller */
-.dispatch-sidebar {
- width: 300px;
- background-color: #fff;
- border-right: 1px solid #e0e0e0;
- max-height: calc(100vh - 56px);
- overflow-y: auto;
- transition: width 0.3s ease, padding 0.3s ease;
- position: relative;
- box-shadow: 0 0 2px rgba(0, 0, 0, 0.06);
- flex-shrink: 0;
- padding: 0 0.75rem;
-}
-
-.dispatch-sidebar.collapsed {
- width: 0;
- padding: 0;
-}
-
-/* Sidebar header with more padding */
-.sidebar-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.75rem 0;
- border-bottom: 1px solid #eee;
- position: sticky;
- top: 0;
- background: #fff;
- z-index: 10;
-}
-
-.sidebar-heading-group {
- display: flex;
- align-items: center;
-}
-
-.sidebar-heading {
- margin: 0;
- font-size: 1rem;
- font-weight: 600;
- color: #333;
- display: inline-block;
-}
-
-.sidebar-caret {
- margin-left: 0.5rem;
- cursor: pointer;
- color: #666;
- transition: color 0.2s;
-}
-
-.sidebar-caret:hover {
- color: #0080ff;
-}
-
-.sidebar-icons {
- display: flex;
- align-items: center;
- gap: 0.25rem;
-}
-
-.sidebar-icon {
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 4px 6px;
- cursor: pointer;
- transition: background-color 0.2s, border-color 0.2s, color 0.2s;
-}
-
-.sidebar-icon i {
- font-size: 1rem;
- color: #666;
-}
-
-.sidebar-icon:hover {
- background-color: #e8f2ff;
- border-color: #0080ff;
- color: #0080ff;
-}
-
-/* Dropdown */
-.sidebar-dropdown {
- position: absolute;
- top: 60px;
- left: 0;
- right: 0;
- background-color: #fff;
- border: 1px solid #ddd;
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
- padding: 0.75rem;
- display: none;
- z-index: 100;
-}
-
-.sidebar-dropdown.open {
- display: block;
-}
-
-.sidebar-dropdown-tabs {
- display: flex;
- gap: 1rem;
- margin-bottom: 0.5rem;
-}
-
-.dropdown-tab {
- font-weight: 500;
- cursor: pointer;
- color: #555;
- position: relative;
-}
-
-.dropdown-tab.active {
- color: #0080ff;
- border-bottom: 2px solid #0080ff;
-}
-
-.sidebar-dropdown-search {
- margin-bottom: 0.75rem;
-}
-
-.sidebar-dropdown-search input {
- width: 100%;
- padding: 6px 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 0.9rem;
-}
-
-.sidebar-inline-search {
- display: none;
- margin: 0.75rem 0;
-}
-
-.sidebar-inline-search.visible {
- display: block;
-}
-
-.sidebar-inline-search input {
- width: 100%;
- padding: 6px 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 0.9rem;
-}
-
-/* Sidebar list items */
-.sidebar-list {
- list-style: none;
- margin: 0.5rem 0 0 0;
- padding: 0;
-}
-
-.sidebar-list li {
- margin-bottom: 0.5rem;
-}
-
-.sidebar-list li a {
- display: block;
- color: #444;
- text-decoration: none;
- padding: 6px 8px;
- border-radius: 4px;
- transition: background-color 0.2s;
-}
-
-.sidebar-list li a:hover {
- background-color: #f0f4f8;
- color: #0080ff;
-}
-
-.small-text {
- font-size: 0.85rem;
- color: #777;
-}
-
-/* Pill for priority/status, smaller and rounder */
-.pill {
- display: inline-block;
- color: #fff;
- padding: 2px 6px;
- border-radius: 12px;
- font-size: 0.75rem;
- margin-top: 2px;
-}
-
-/* Main content */
-.dispatch-content {
- flex: 1;
- padding: 1rem;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- min-width: 0;
-}
-
-.dispatch-view-content {
- background-color: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 6px;
- flex: 1;
- padding: 1.5rem;
- box-shadow: 0 2px 3px rgba(0, 0, 0, 0.04);
-}
-
-.placeholder-content {
- text-align: center;
- margin-top: 2rem;
-}
-
-/* The tooltip for right-click on sidebar items */
-.sidebar-tooltip {
- position: absolute;
- background: #ffffff;
- border: 1px solid #ccc;
- border-radius: 6px;
- box-shadow: 0 6px 12px rgba(0,0,0,0.15);
- padding: 0.75rem;
- width: 300px;
- display: none;
- z-index: 9999;
-}
-
-.tooltip-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0.5rem;
-}
-
-.tooltip-buttons .btn {
- margin-left: 4px;
- padding: 4px 6px;
- font-size: 0.75rem;
-}
-
-.tooltip-body p {
- margin: 0.25rem 0;
- font-size: 0.85rem;
-}
-
-/* Gantt view */
-.gantt-view .schedule-grid-container {
- position: relative;
- width: 100%;
- overflow-x: auto;
- overflow-y: hidden;
-}
-
-/* Timeline header, time labels, etc. */
-.gantt-view .timeline-header {
- position: relative;
- margin-left: 20%;
- width: 80%;
- height: 40px;
- border-bottom: 1px solid #ddd;
-}
-
-.gantt-view .time-label {
- position: absolute;
- transform: translateX(-50%);
- font-size: 12px;
- color: #555;
-}
-
-/* Technician rows */
-.gantt-view .technician-row {
- display: flex;
- height: 50px;
- border-bottom: 1px solid #ddd;
-}
-
-.gantt-view .technician-name {
- width: 20%;
- background: #f0f0f0;
- text-align: center;
- line-height: 50px;
- border-right: 1px solid #ddd;
-}
-
-/* Timeline cell */
-.gantt-view .timeline-cell {
- width: 80%;
- position: relative;
-}
-
-/* Timeline background */
-.gantt-view .timeline-background {
- position: absolute;
- left: 20%;
- top: 40px;
- width: 80%;
- bottom: 0;
- pointer-events: none;
-}
-
-/* Schedule event styling */
-.gantt-view .schedule-event {
- background: #007bff;
- color: white;
- padding: 2px;
- border-radius: 3px;
- cursor: grab;
- overflow: hidden;
- font-size: 10px;
- text-align: center;
- z-index: 10;
- opacity: 0.9;
- display: flex;
- align-items: center;
- justify-content: center;
- position: absolute;
- top: 0;
- height: 100%;
-}
-
-/* For resizing and dragging */
-.gantt-view .schedule-event.dragging {
- border: 2px dashed #ff9900;
- opacity: 0.7;
-}
-
-.gantt-view .resize-handle {
- position: absolute;
- background: rgba(0, 0, 0, 0.3);
- top: 0;
- bottom: 0;
- cursor: ew-resize;
- z-index: 20;
- width: 3px;
-}
-
-.gantt-view .left-handle {
- left: 0;
-}
-
-.gantt-view .right-handle {
- right: 0;
-}
-
-/* Date table styling */
-.gantt-view .selected-date {
- background-color: #343a40 !important;
- color: white !important;
- font-weight: bold;
- border-radius: 4px;
-}
-
-.gantt-view #date-table th {
- text-align: center;
- vertical-align: middle;
-}
diff --git a/beveren_fsm/field_service_management/page/dispatch/dispatch.html b/beveren_fsm/field_service_management/page/dispatch/dispatch.html
deleted file mode 100644
index 71a2af8..0000000
--- a/beveren_fsm/field_service_management/page/dispatch/dispatch.html
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
Welcome to Dispatch
-
Select a view (Gantt, Calendar, or Map) to get started.
-
-
-
-
-
-
-
-
diff --git a/beveren_fsm/field_service_management/page/dispatch/dispatch.js b/beveren_fsm/field_service_management/page/dispatch/dispatch.js
deleted file mode 100644
index 4db2b2b..0000000
--- a/beveren_fsm/field_service_management/page/dispatch/dispatch.js
+++ /dev/null
@@ -1,549 +0,0 @@
-frappe.pages["dispatch"].on_page_load = function (wrapper) {
- frappe.ui.make_app_page({
- parent: wrapper,
- single_column: true,
- });
-
- $(wrapper).html(frappe.render_template("dispatch"));
-
- frappe.require(
- [
- "assets/beveren_fsm/js/dispatch/gantt.js",
- // Calendar file
- // Map file
- ],
- () => {
- new DispatchController();
- }
- );
-};
-
-class DispatchController {
- constructor() {
- this.sidebarMode = "Service Order";
- this.sidebarCollapsed = false;
- this.currentView = "gantt";
- this.cachedDocs = {};
-
- // Flags for view initialization
- this.viewInitialized = {
- gantt: false,
- calendar: false,
- map: false,
- };
-
- this.setup_navbar_buttons();
- this.setup_sidebar_header_events();
-
- // Create containers for each view
- this.createViewContainers();
-
- // Load sidebar items for default mode
- this.loadSidebarItems("Service Order");
-
- // Initialize default view (Gantt)
- this.switchView("gantt");
- }
-
- createViewContainers() {
- // Create and append containers for each view; they all reside in #view-content.
- const viewContent = $("#view-content");
- viewContent.empty();
-
- this.$ganttView = $(`
-
- `);
-
- this.$calendarView = $(`
-
- `);
- this.$mapView = $(`
-
-
-
Map View
-
Map UI goes here.
-
-
- `);
-
- viewContent.append(this.$ganttView, this.$calendarView, this.$mapView);
- }
-
- // NAVBAR
- setup_navbar_buttons() {
- $("#sidebar-toggle-btn").on("click", () => {
- this.toggleSidebar();
- });
- $("#btn-gantt").on("click", () => {
- this.switchView("gantt");
- });
- $("#btn-calendar").on("click", () => {
- this.switchView("calendar");
- });
- $("#btn-map").on("click", () => {
- this.switchView("map");
- });
- }
-
- toggleSidebar() {
- this.sidebarCollapsed = !this.sidebarCollapsed;
- const $sidebar = $("#dispatch-sidebar");
- if (this.sidebarCollapsed) {
- $sidebar.addClass("collapsed");
- $("#sidebar-toggle-btn i")
- .removeClass("fa-angle-double-left")
- .addClass("fa-angle-double-right");
- } else {
- $sidebar.removeClass("collapsed");
- $("#sidebar-toggle-btn i")
- .removeClass("fa-angle-double-right")
- .addClass("fa-angle-double-left");
- }
- }
-
- // Instead of re-building views on every switch, we hide all and then show the target view.
- switchView(view) {
- $(".nav-view-btn").removeClass("active");
- $(`#btn-${view}`).addClass("active");
- this.currentView = view;
-
- // Hide all views
- $(".view-container").hide();
-
- if (view === "gantt") {
- this.$ganttView.show();
- if (!this.viewInitialized.gantt) {
- // Load Gantt from separate file (gantt.js)
- init_gantt("#gantt-page-body");
- this.viewInitialized.gantt = true;
- }
- } else if (view === "calendar") {
- this.$calendarView.show();
- if (!this.viewInitialized.calendar) {
- init_calendar_code("#calendar-page-body");
- this.viewInitialized.calendar = true;
- }
- } else if (view === "map") {
- this.$mapView.show();
- if (!this.viewInitialized.map) {
- // Initialize your Map view if needed.
- this.viewInitialized.map = true;
- }
- }
- }
-
- // SIDEBAR and other methods below remain largely unchanged...
- setup_sidebar_header_events() {
- $("#sidebar-dropdown-toggle").on("click", () => {
- $("#sidebar-dropdown").toggleClass("open");
- });
-
- $("#sidebar-add-icon").on("click", () => {
- this.open_create_schedule_dialog();
- });
-
- $("#sidebar-search-toggle").on("click", () => {
- $("#sidebar-inline-search").toggleClass("visible");
- if ($("#sidebar-inline-search").hasClass("visible")) {
- $("#sidebar-search-input").focus();
- }
- });
-
- $("#sidebar-refresh").on("click", () => {
- this.loadSidebarItems(this.sidebarMode, true);
- });
-
- $(".dropdown-tab").on("click", (e) => {
- let mode = $(e.currentTarget).data("mode");
- this.switchSidebarMode(mode);
-
- $(".dropdown-tab").removeClass("active");
- $(e.currentTarget).addClass("active");
- });
-
- $("#sidebar-search-input").on("keyup", () => {
- let val = $("#sidebar-search-input").val().toLowerCase();
- this.filterSidebarDocs(val);
- });
- }
-
- switchSidebarMode(mode) {
- this.sidebarMode = mode;
- $("#sidebar-selected-mode").text(mode);
- $("#sidebar-dropdown").removeClass("open");
- this.loadSidebarItems(mode, true);
-
- let placeholderText =
- mode === "Service Order"
- ? "Search Service Orders..."
- : "Search Service Appointments...";
- $("#sidebar-search-input").attr("placeholder", placeholderText);
- }
-
- loadSidebarItems(mode, forceRefresh = false) {
- let $sidebarList = $("#sidebar-list");
- $sidebarList.empty();
-
- frappe.call({
- method:
- "beveren_fsm.field_service_management.page.dispatch.dispatch.get_sidebar_data",
- args: { mode },
- callback: (r) => {
- if (!r.exc) {
- this.cachedDocs[mode] = r.message || [];
- this.renderSidebarDocs(this.cachedDocs[mode], mode);
- }
- },
- });
- }
-
- renderSidebarDocs(docs, mode) {
- let $sidebarList = $("#sidebar-list");
- $sidebarList.empty();
-
- docs.forEach((doc) => {
- let pillHtml = this.getPillHtml(doc, mode);
- let moreInfoHtml = this.getMoreInfoHtml(doc, mode);
-
- let li = $(`
-
- `);
- $sidebarList.append(li);
- });
-
- this.setupSidebarRightClick();
- }
-
- filterSidebarDocs(searchVal) {
- let filtered = (this.cachedDocs[this.sidebarMode] || []).filter((doc) => {
- let docStr = JSON.stringify(doc).toLowerCase();
- return docStr.includes(searchVal);
- });
- this.renderSidebarDocs(filtered, this.sidebarMode);
- }
-
- setupSidebarRightClick() {
- $(document)
- .off("click.sidebarTooltip")
- .on("click.sidebarTooltip", () => {
- $("#sidebar-tooltip").hide();
- });
-
- $(".sidebar-doc-item")
- .off("contextmenu")
- .on("contextmenu", (e) => {
- e.preventDefault();
- let $li = $(e.currentTarget);
- let docStr = $li.attr("data-doc");
- let doc = JSON.parse(docStr || "{}");
- let mode = $li.attr("data-mode");
- this.showSidebarTooltip(e.pageX, e.pageY, doc, mode);
- });
- }
-
- getPillHtml(doc, mode) {
- if (mode === "Service Order") {
- let priority = (doc.priority || "").toLowerCase();
- let color = "#6c757d";
- if (priority === "high") color = "#dc3545";
- if (priority === "medium") color = "#ffc107";
- if (priority === "low") color = "#28a745";
- return `${
- doc.priority || ""
- }`;
- } else {
- let status = (doc.status || "").toLowerCase();
- let color = "#6c757d";
- if (status === "scheduled") color = "#007bff";
- if (status === "in progress") color = "#ffc107";
- if (status === "completed") color = "#28a745";
- if (status === "cancelled") color = "#dc3545";
- if (status === "dispatched") color = "#17a2b8";
- return `${
- doc.status || ""
- }`;
- }
- }
-
- getMoreInfoHtml(doc, mode) {
- if (mode === "Service Order") {
- let date = doc.posting_date || "";
- let customer = doc.customer || "";
- return `${date} - ${customer}
`;
- } else {
- let date = doc.posting_date || "";
- let start = doc.scheduled_start_datetime
- ? doc.scheduled_start_datetime.split(" ")[1]
- : "";
- let end = doc.scheduled_finish_datetime
- ? doc.scheduled_finish_datetime.split(" ")[1]
- : "";
- return `${date} - ${start} to ${end}
`;
- }
- }
-
- showSidebarTooltip(x, y, doc, mode) {
- let tooltip = $("#sidebar-tooltip");
- tooltip.empty().show();
-
- let itemsIcon = ``;
- let techsIcon = ``;
- let addIcon = ``;
-
- let itemsBtn = ``;
- let techsBtn =
- mode === "Service Appointment"
- ? ``
- : ``;
- let addBtn = ``;
- if (mode === "Service Order") {
- if ((doc.status || "").toLowerCase() !== "completed") {
- addBtn = ``;
- }
- } else {
- addBtn = ``;
- }
-
- let headerHtml = `
-
- `;
-
- let bodyHtml = ``;
- if (mode === "Service Order") {
- bodyHtml += `
-
Customer: ${doc.customer || "N/A"}
-
Date: ${doc.posting_date || ""}
-
Status: ${doc.status || ""}
-
Priority: ${doc.priority || ""}
- `;
- } else {
- bodyHtml += `
-
Posting Date: ${doc.posting_date || ""}
-
Status: ${doc.status || ""}
-
Start: ${
- doc.scheduled_start_datetime || ""
- }
-
Finish: ${
- doc.scheduled_finish_datetime || ""
- }
- `;
- }
- bodyHtml += `
`;
-
- tooltip.html(headerHtml + bodyHtml);
-
- let tooltipWidth = tooltip.outerWidth();
- let tooltipHeight = tooltip.outerHeight();
- let finalX = x + 10;
- let finalY = y;
- if (finalX + tooltipWidth > $(window).width()) {
- finalX = x - tooltipWidth - 10;
- }
- if (finalY + tooltipHeight > $(window).height()) {
- finalY = $(window).height() - tooltipHeight - 20;
- }
- tooltip.css({ left: finalX + "px", top: finalY + "px" });
-
- tooltip.find("[data-action='view-items']").on("click", () => {
- this.showItemsDialog(doc, mode);
- });
- if (mode === "Service Appointment") {
- tooltip.find("[data-action='view-techs']").on("click", () => {
- this.showTechniciansDialog(doc);
- });
- }
- tooltip.find("[data-action='add-item']").on("click", () => {
- if (mode === "Service Order") {
- this.create_event_for_order(doc);
- } else {
- frappe.confirm("Create an invoice for this appointment?", () => {
- frappe.msgprint("Proceed with invoice creation...");
- });
- }
- });
- }
-
- showItemsDialog(doc) {
- let dialog = new frappe.ui.Dialog({
- title: "Service Items",
- fields: [
- {
- fieldname: "service_items",
- fieldtype: "Table",
- label: __("Service Items"),
- in_place_edit: true,
- reqd: 1,
- fields: [
- {
- fieldname: "item_code",
- label: __("Item"),
- fieldtype: "Link",
- options: "Item",
- reqd: 1,
- in_list_view: 1,
- },
- {
- fieldname: "qty",
- label: __("Qty"),
- fieldtype: "Data",
- reqd: 1,
- in_list_view: 1,
- },
- {
- fieldname: "invoice_status",
- label: __("Invoice Status"),
- fieldtype: "Data",
- reqd: 1,
- in_list_view: 1,
- },
- ],
- },
- ],
- primary_action_label: "Close",
- primary_action: () => dialog.hide(),
- });
-
- let tableField = dialog.get_field("service_items");
- tableField.df.data = doc.items;
- tableField.grid.refresh();
-
- dialog.show();
- }
-
- showTechniciansDialog(doc) {
- let dialog = new frappe.ui.Dialog({
- title: "Technicians",
- fields: [
- {
- fieldname: "service_technicians",
- fieldtype: "Table",
- label: __("Service Technicians"),
- options: "Service Technician Item",
- in_place_edit: true,
- reqd: 1,
- fields: [
- {
- fieldname: "service_technician",
- label: __("Item"),
- fieldtype: "Link",
- options: "Item",
- reqd: 1,
- in_list_view: 1,
- },
- {
- fieldname: "full_name",
- label: __("Full Name"),
- fieldtype: "Data",
- fetch_from: "service_technician.full_name",
- reqd: 1,
- in_list_view: 1,
- },
- ],
- },
- ],
- primary_action_label: "Close",
- primary_action: () => dialog.hide(),
- });
-
- let tableField = dialog.get_field("service_technicians");
- tableField.df.data = doc.service_technicians;
- tableField.grid.refresh();
-
- dialog.show();
- }
-
- create_event_for_order(orderDoc = null) {
- if (!orderDoc) {
- frappe.prompt(
- [
- {
- fieldname: "service_order",
- label: "Service Order",
- fieldtype: "Link",
- options: "Service Order",
- reqd: 1,
- },
- ],
- (values) => {
- this.open_create_schedule_dialog(values.service_order);
- },
- "Create Schedule",
- "Create"
- );
- } else {
- this.open_create_schedule_dialog(orderDoc.name);
- }
- }
-
- open_create_schedule_dialog(OrderName = "") {
- let selectedDate = frappe.datetime.get_today();
- let d = new frappe.ui.Dialog({
- title: "Create Schedule",
- fields: [
- {
- fieldname: "service_order",
- fieldtype: "Link",
- options: "Service Order",
- label: "Service Order",
- default: OrderName,
- reqd: 1,
- },
- { fieldtype: "Column Break" },
- {
- fieldname: "selected_date",
- fieldtype: "Date",
- label: "Selected Date",
- default: selectedDate,
- },
- {
- fieldname: "start_time",
- fieldtype: "Time",
- label: "Start Time",
- default: "09:00",
- },
- {
- fieldname: "finish_time",
- fieldtype: "Time",
- label: "Finish Time",
- default: "10:00",
- },
- ],
- primary_action_label: "Schedule & Dispatch",
- primary_action: (values) => {
- let scheduledStart = values.selected_date + " " + values.start_time;
- let scheduledFinish = values.selected_date + " " + values.finish_time;
- create_appointment(
- values.selected_date,
- values.service_order,
- scheduledStart,
- scheduledFinish,
- "TECH-0001",
- (r) => {
- frappe.msgprint("Appointment created for " + values.service_order);
- }
- );
- d.hide();
- },
- });
- d.show();
- }
-}
diff --git a/beveren_fsm/field_service_management/page/dispatch/dispatch.json b/beveren_fsm/field_service_management/page/dispatch/dispatch.json
deleted file mode 100644
index c51fb02..0000000
--- a/beveren_fsm/field_service_management/page/dispatch/dispatch.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "content": null,
- "creation": "2025-02-17 20:56:52.652328",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2025-02-17 20:56:52.652328",
- "modified_by": "Administrator",
- "module": "Field Service Management",
- "name": "dispatch",
- "owner": "Administrator",
- "page_name": "dispatch",
- "roles": [],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 0
-}
diff --git a/beveren_fsm/field_service_management/page/dispatch/dispatch.py b/beveren_fsm/field_service_management/page/dispatch/dispatch.py
deleted file mode 100644
index 6ecafcc..0000000
--- a/beveren_fsm/field_service_management/page/dispatch/dispatch.py
+++ /dev/null
@@ -1,232 +0,0 @@
-import frappe
-from frappe.utils import get_datetime, getdate
-
-
-@frappe.whitelist()
-def get_schedule_data(selected_date, all_dates=False):
- technicians = frappe.get_all("Service Technician", fields=["name", "full_name"])
-
- selected_date = getdate(selected_date)
-
- if all_dates:
- appointments = frappe.get_all(
- "Service Appointment",
- fields=[
- "name",
- "posting_date",
- "service_order",
- "scheduled_start_datetime",
- "scheduled_finish_datetime",
- "status",
- ],
- filters={"docstatus": 1},
- order_by="posting_date desc",
- )
- else:
- appointments = frappe.get_all(
- "Service Appointment",
- filters={
- "posting_date": selected_date,
- },
- fields=[
- "name",
- "posting_date",
- "service_order",
- "scheduled_start_datetime",
- "scheduled_finish_datetime",
- "status",
- ],
- )
-
- meta = frappe.get_meta("Service Appointment")
- appointments_with_technicians = []
- for appointment in appointments:
- appointment_doc = frappe.get_doc("Service Appointment", appointment.name)
- service_technicians = [
- frappe.get_doc("Service Technician", tech.service_technician).name
- for tech in appointment_doc.service_technicians
- ]
- start_time = get_datetime(appointment_doc.scheduled_start_datetime)
- finish_time = get_datetime(appointment_doc.scheduled_finish_datetime)
-
- state_color = None
- if meta.states:
- for s in meta.states:
- if s.title == appointment_doc.status:
- state_color = s.color
- break
-
- structured_appointment = {
- "name": appointment.name,
- "posting_date": appointment.posting_date,
- "service_order": appointment.service_order,
- "start_time": start_time.strftime("%H:%M"),
- "finish_time": finish_time.strftime("%H:%M"),
- "service_technicians": service_technicians,
- "status": appointment_doc.status,
- "color": state_color,
- }
- appointments_with_technicians.append(structured_appointment)
-
- return {"technicians": technicians, "appointments": appointments_with_technicians}
-
-
-@frappe.whitelist()
-def create_service_appointment(
- selected_date, service_order, scheduled_start_datetime, scheduled_finish_datetime, technician, dispatch=0
-):
- from frappe.utils import get_datetime, getdate
-
- scheduled_start_datetime = get_datetime(scheduled_start_datetime)
- scheduled_finish_datetime = get_datetime(scheduled_finish_datetime)
-
- appointment_list = frappe.get_all(
- "Service Appointment",
- filters={
- "posting_date": getdate(selected_date),
- "service_order": service_order,
- "scheduled_start_datetime": scheduled_start_datetime,
- "scheduled_finish_datetime": scheduled_finish_datetime,
- },
- limit=10,
- )
-
- found = None
- for app in appointment_list:
- app_doc = frappe.get_doc("Service Appointment", app.name)
- for row in app_doc.get("service_technicians"):
- if row.service_technician == technician:
- found = app_doc
- break
- if found:
- break
-
- if found:
- appointment = found
- else:
- service_order_doc = frappe.get_doc("Service Order", service_order)
- appointment = frappe.new_doc("Service Appointment")
- appointment.posting_date = getdate(selected_date)
- appointment.service_order = service_order
- appointment.scheduled_start_datetime = scheduled_start_datetime
- appointment.scheduled_finish_datetime = scheduled_finish_datetime
- appointment.customer = service_order_doc.customer
- for item in service_order_doc.get("items") or []:
- appointment.append(
- "items",
- {
- "item_code": item.item_code,
- "qty": item.qty,
- "uom": item.uom,
- "invoice_status": item.invoice_status,
- },
- )
- appointment.append("service_technicians", {"service_technician": technician})
- appointment.save()
- appointment.submit()
- # Dispatch
- if int(dispatch) == 1:
- appointment.status = "Dispatched"
- appointment.save()
-
- meta = frappe.get_meta("Service Appointment")
- state_color = None
- if meta.states:
- for s in meta.states:
- if s.title == appointment.status:
- state_color = s.color
- break
-
- return {"name": appointment.name, "status": appointment.status, "color": state_color}
-
-
-@frappe.whitelist()
-def update_service_appointment(
- appointment_id,
- selected_date,
- service_order,
- scheduled_start_datetime,
- scheduled_finish_datetime,
- technician,
-):
- from frappe.utils import get_datetime, getdate
-
- appointment = frappe.get_doc("Service Appointment", appointment_id)
-
- appointment.update(
- {
- "posting_date": getdate(selected_date),
- "service_order": service_order,
- "scheduled_start_datetime": get_datetime(scheduled_start_datetime),
- "scheduled_finish_datetime": get_datetime(scheduled_finish_datetime),
- }
- )
-
- appointment.set("service_technicians", [])
- appointment.append("service_technicians", {"service_technician": technician})
-
- appointment.save()
- return appointment.name
-
-
-@frappe.whitelist()
-def start_work(appointment_id):
- appointment = frappe.get_doc("Service Appointment", appointment_id)
- if appointment.status == "Dispatched":
- appointment.status = "In Progress"
- appointment.save(ignore_permissions=True)
- return appointment.name
- else:
- frappe.throw("Appointment is not in Dispatched status. Cannot start work.")
-
-
-@frappe.whitelist()
-def get_sidebar_data(mode):
- if mode == "Service Order":
- orders = frappe.get_all(
- "Service Order",
- fields=["name", "customer", "priority", "posting_date", "status"],
- filters={"docstatus": 1},
- order_by="posting_date desc",
- limit=30,
- )
- for o in orders:
- items = frappe.get_all(
- "Service Order Item",
- filters={"parent": o.name},
- fields=["item_code", "item_name", "qty", "uom", "invoice_status"],
- )
- o["items"] = items
- return orders
-
- elif mode == "Service Appointment":
- appointments = frappe.get_all(
- "Service Appointment",
- fields=[
- "name",
- "posting_date",
- "status",
- "scheduled_start_datetime",
- "scheduled_finish_datetime",
- ],
- filters={"docstatus": 1},
- order_by="posting_date desc",
- limit=30,
- )
- for a in appointments:
- items = frappe.get_all(
- "Service Order Item",
- filters={"parent": a.name},
- fields=["item_code", "item_name", "qty", "uom", "invoice_status"],
- )
- techs = frappe.get_all(
- "Service Technician Item",
- filters={"parent": a.name},
- fields=["service_technician", "full_name"],
- )
- a["items"] = items
- a["service_technicians"] = techs
- return appointments
-
- else:
- return []
diff --git a/beveren_fsm/field_service_management/page/service_scheduling/__init__.py b/beveren_fsm/field_service_management/page/service_scheduling/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.css b/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.css
deleted file mode 100644
index e2a4a41..0000000
--- a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.css
+++ /dev/null
@@ -1,114 +0,0 @@
-/* Container for the entire schedule grid */
-.schedule-grid-container {
- position: relative;
- width: 100%;
- }
-
- /* Timeline header that holds the time labels */
- .timeline-header {
- position: relative;
- margin-left: 20%;
- width: 80%;
- height: 40px;
- border-bottom: 1px solid #ddd;
- }
-
- /* Time labels in the timeline header */
- .time-label {
- position: absolute;
- transform: translateX(-50%);
- font-size: 12px;
- color: #555;
- }
-
- /* Technician rows container */
- .technician-rows { }
-
- /* Each technician row */
- .technician-row {
- display: flex;
- height: 50px;
- border-bottom: 1px solid #ddd;
- }
-
- /* Technician name column */
- .technician-name {
- width: 20%;
- background: #f0f0f0;
- text-align: center;
- line-height: 50px;
- border-right: 1px solid #ddd;
- }
-
- /* Timeline cell */
- .timeline-cell {
- width: 80%;
- position: relative;
- }
-
- /* Timeline background */
- .timeline-background {
- position: absolute;
- left: 20%;
- top: 40px;
- width: 80%;
- bottom: 0;
- pointer-events: none;
- }
-
- /* Schedule event styling */
- .schedule-event {
- background: #007bff;
- color: white;
- padding: 2px;
- border-radius: 3px;
- cursor: grab;
- overflow: hidden;
- font-size: 10px;
- text-align: center;
- z-index: 10;
- opacity: 0.9;
- display: flex;
- align-items: center;
- justify-content: center;
- position: absolute;
- top: 0;
- height: 100%;
- }
-
- /* Styling for the selected date */
- .selected-date {
- background-color: #343a40 !important;
- color: white !important;
- font-weight: bold;
- border-radius: 4px;
- }
-
- .schedule-event.dragging {
- border: 2px dashed #ff9900;
- opacity: 0.7;
- }
-
- #date-table th {
- text-align: center;
- vertical-align: middle;
- }
-
- /* Resize handles (moved from inline to CSS) */
- .resize-handle {
- position: absolute;
- background: rgba(0, 0, 0, 0.3);
- top: 0;
- bottom: 0;
- cursor: ew-resize;
- z-index: 20;
- width: 3px;
- }
-
- .left-handle {
- left: 0;
- }
-
- .right-handle {
- right: 0;
- }
diff --git a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.html b/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.html
deleted file mode 100644
index 27ce433..0000000
--- a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
diff --git a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.js b/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.js
deleted file mode 100644
index c2ce0d1..0000000
--- a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.js
+++ /dev/null
@@ -1,1026 +0,0 @@
-// Global constants and variables
-const START_TIME_MINUTES = 420; // 07:00 in minutes
-const END_TIME_MINUTES = 1140; // 19:00 in minutes
-const TOTAL_WORKING_MINUTES = END_TIME_MINUTES - START_TIME_MINUTES; // 720 minutes
-
-var status_colors = {
- Scheduled: "#007bff",
- Rescheduled: "#28a745",
- Completed: "#6c757d",
- Cancelled: "#dc3545",
-};
-
-let isResizing = false;
-let justResized = false;
-let currentSelectedDate = frappe.datetime.get_today();
-
-// Mobile detection and date range helpers
-function isMobile() {
- return window.innerWidth < 768;
-}
-
-function generate_date_range(selected_date) {
- let dates = [];
- if (isMobile()) {
- // Show 5 days: 2 before, current, 2 after.
- for (let i = -2; i <= 2; i++) {
- dates.push(frappe.datetime.add_days(selected_date, i));
- }
- } else {
- // Default range: from -10 to +9 days (20 days)
- let today_index = 10;
- for (let i = -today_index; i <= 20 - today_index - 1; i++) {
- dates.push(frappe.datetime.add_days(selected_date, i));
- }
- }
- return dates;
-}
-
-function openDatePicker() {
- let d = new frappe.ui.Dialog({
- title: "Select Date",
- fields: [
- {
- fieldname: "selected_date",
- fieldtype: "Date",
- label: "Date",
- default: currentSelectedDate,
- },
- ],
- primary_action_label: "Go",
- primary_action: (values) => {
- currentSelectedDate = values.selected_date;
- load_schedule(values.selected_date);
- d.hide();
- },
- });
- d.show();
-}
-
-// Helper functions
-const timeStringToMinutes = (timeStr) => {
- const parts = timeStr.split(":");
- return parseInt(parts[0]) * 60 + parseInt(parts[1]);
-};
-
-const minutesToTimeString = (minutes) => {
- const hrs = Math.floor(minutes / 60);
- const mins = minutes % 60;
- return ("0" + hrs).slice(-2) + ":" + ("0" + mins).slice(-2);
-};
-
-const roundToNearestTen = (mins) => Math.round(mins / 10) * 10;
-
-const formatDatetime = (date, timeStr) => {
- const parts = timeStr.split(":");
- if (parts.length === 2) {
- return date + " " + timeStr + ":00";
- }
- return date + " " + timeStr;
-};
-
-const calculatePosition = (startMins, endMins) => {
- const leftPercent =
- ((startMins - START_TIME_MINUTES) / TOTAL_WORKING_MINUTES) * 100;
- const widthPercent = ((endMins - startMins) / TOTAL_WORKING_MINUTES) * 100;
- return { leftPercent, widthPercent };
-};
-
-const debounce = (func, delay) => {
- let timeout;
- return function (...args) {
- clearTimeout(timeout);
- timeout = setTimeout(() => func.apply(this, args), delay);
- };
-};
-
-// Transform a service order number.
-// For example, "SVC-APP-2025-00043" becomes "ORD-00043".
-function transformServiceOrder(order) {
- if (!order) return "";
- let parts = order.split("-");
- if (parts.length >= 1) {
- return "ORD-" + parts[parts.length - 1];
- }
- return order;
-}
-
-// Consolidated drag & drop functions
-function drag(event) {
- event.dataTransfer.setData("text", event.target.outerHTML);
- event.target.classList.add("dragging");
-}
-
-function dragEnd(event) {
- event.target.classList.remove("dragging");
-}
-
-const allowDrop = (event) => {
- event.preventDefault();
-};
-
-function is_overlapping(technician, startMins, endMins) {
- let overlapping = false;
- $(`.technician-row[data-tech='${technician}'] .schedule-event`).each(
- function () {
- const eventStart = timeStringToMinutes($(this).attr("data-start"));
- const eventEnd = timeStringToMinutes($(this).attr("data-end"));
- if (startMins < eventEnd && endMins > eventStart) {
- overlapping = true;
- return false;
- }
- }
- );
- return overlapping;
-}
-
-function is_overlapping_excluding(technician, startMins, endMins, excludeId) {
- let overlapping = false;
- $(`.technician-row[data-tech='${technician}'] .schedule-event`).each(
- function () {
- if ($(this).data("appointment") == excludeId) return;
- const eventStart = timeStringToMinutes($(this).attr("data-start"));
- const eventEnd = timeStringToMinutes($(this).attr("data-end"));
- if (startMins < eventEnd && endMins > eventStart) {
- overlapping = true;
- return false;
- }
- }
- );
- return overlapping;
-}
-
-// Page load initialization
-frappe.pages["service-scheduling"].on_page_load = function (wrapper) {
- let page = frappe.ui.make_app_page({
- parent: wrapper,
- single_column: true,
- });
-
- // Breadcrumbs
- $(
- 'HOME >
Schedule & Dispatch '
- ).appendTo(page.body);
-
- // Header controls with "Select Date" button (using calendar icon)
- let header_controls = $(`
-
-
-
-
-
-
-
-
- `).appendTo(page.body);
-
- $("#select-date-btn").on("click", () => {
- openDatePicker();
- });
-
- let month_row = $(
- ``
- ).appendTo(page.body);
- let date_table = $(``).appendTo(page.body);
-
- // The merged header (with search and time labels) will be built in render_schedule_grid.
- let schedule_grid = $(``);
- // For mobile, allow horizontal scrolling.
- if (isMobile()) {
- schedule_grid.css("overflow-x", "auto");
- }
- schedule_grid.appendTo(page.body);
-
- currentSelectedDate = frappe.datetime.get_today();
- load_schedule(currentSelectedDate);
-
- $("#today-btn").on("click", () => {
- const selected_date = frappe.datetime.get_today();
- currentSelectedDate = selected_date;
- load_schedule(selected_date);
- });
-
- $("#tomorrow-btn").on("click", () => {
- const selected_date = frappe.datetime.add_days(
- frappe.datetime.get_today(),
- 1
- );
- currentSelectedDate = selected_date;
- load_schedule(selected_date);
- });
-};
-
-// Event resizing functions
-function startResizing(e, eventElement, side) {
- e.stopPropagation();
- isResizing = true;
- $(eventElement).attr("draggable", false);
- const initialX = e.pageX;
- const initialStart = timeStringToMinutes($(eventElement).data("start"));
- const initialEnd = timeStringToMinutes($(eventElement).data("end"));
-
- const onMouseMove = (e) => {
- const delta = e.pageX - initialX;
- const timelineWidth = $(eventElement).parent().width();
- const deltaMins = (delta / timelineWidth) * TOTAL_WORKING_MINUTES;
- if (side === "left") {
- let newStart = initialStart + deltaMins;
- if (newStart < START_TIME_MINUTES) newStart = START_TIME_MINUTES;
- if (newStart >= initialEnd - 30) newStart = initialEnd - 30;
- $(eventElement).attr("data-start", minutesToTimeString(newStart));
- const pos = calculatePosition(newStart, initialEnd);
- $(eventElement).css({
- left: pos.leftPercent + "%",
- width: pos.widthPercent + "%",
- });
- } else if (side === "right") {
- let newEnd = initialEnd + deltaMins;
- if (newEnd > END_TIME_MINUTES) newEnd = END_TIME_MINUTES;
- if (newEnd <= initialStart + 30) newEnd = initialStart + 30;
- $(eventElement).attr("data-end", minutesToTimeString(newEnd));
- const pos = calculatePosition(initialStart, newEnd);
- $(eventElement).css({ width: pos.widthPercent + "%" });
- }
- };
-
- const onMouseUp = (e) => {
- $(document).off("mousemove", onMouseMove);
- $(document).off("mouseup", onMouseUp);
- $(eventElement).attr("draggable", true);
- isResizing = false;
- justResized = true;
- setTimeout(() => {
- justResized = false;
- }, 300);
-
- const newStartStr = $(eventElement).attr("data-start");
- const newEndStr = $(eventElement).attr("data-end");
- if (!newStartStr || !newEndStr) {
- frappe.msgprint("Error reading time data after resizing.");
- return;
- }
- const newStart = timeStringToMinutes(newStartStr);
- const newEnd = timeStringToMinutes(newEndStr);
- const technician = $(eventElement).data("tech");
- const appointmentId = $(eventElement).data("appointment");
- if (is_overlapping_excluding(technician, newStart, newEnd, appointmentId)) {
- frappe.msgprint(
- "Time overlap detected during resizing. Reverting changes."
- );
- $(eventElement).attr("data-start", minutesToTimeString(initialStart));
- $(eventElement).attr("data-end", minutesToTimeString(initialEnd));
- const pos = calculatePosition(initialStart, initialEnd);
- $(eventElement).css({
- left: pos.leftPercent + "%",
- width: pos.widthPercent + "%",
- });
- return;
- }
- update_appointment(
- appointmentId,
- currentSelectedDate,
- $(eventElement).data("service-order"),
- formatDatetime(currentSelectedDate, minutesToTimeString(newStart)),
- formatDatetime(currentSelectedDate, minutesToTimeString(newEnd)),
- $(eventElement).data("tech")
- );
- };
-
- $(document).on("mousemove", onMouseMove);
- $(document).on("mouseup", onMouseUp);
-}
-
-function attachResizeHandles(eventElement) {
- if ($(eventElement).find(".resize-handle").length === 0) {
- $(eventElement).append('');
- $(eventElement).append('');
- }
- $(eventElement)
- .find(".left-handle")
- .on("mousedown", (e) => {
- startResizing(e, eventElement, "left");
- });
- $(eventElement)
- .find(".right-handle")
- .on("mousedown", (e) => {
- startResizing(e, eventElement, "right");
- });
-}
-
-// Create event function
-function create_event(event, timelineCell) {
- if ($(event.target).hasClass("schedule-event") || isResizing || justResized)
- return;
- const technician = $(timelineCell).closest(".technician-row").data("tech");
- const timelineOffset = $(timelineCell).offset();
- const clickX = event.pageX - timelineOffset.left;
- const timelineWidth = $(timelineCell).width();
- let minutesFromStart = (clickX / timelineWidth) * TOTAL_WORKING_MINUTES;
- minutesFromStart = roundToNearestTen(minutesFromStart);
- const eventStartMinutes = START_TIME_MINUTES + minutesFromStart;
- const eventEndMinutes = eventStartMinutes + 30;
- const selectedDate = currentSelectedDate || frappe.datetime.get_today();
-
- const d = new frappe.ui.Dialog({
- title: "Create Schedule",
- fields: [
- {
- fieldname: "appointment",
- fieldtype: "Link",
- options: "Service Appointment",
- label: "Appointment",
- read_only: 1,
- default: "",
- hidden: 1,
- },
- {
- fieldname: "service_order",
- fieldtype: "Link",
- options: "Service Order",
- label: "Service Order",
- reqd: 1,
- },
- {
- fieldname: "technician",
- fieldtype: "Link",
- options: "Service Technician",
- label: "Technician",
- read_only: 1,
- default: technician,
- },
- { fieldtype: "Column Break" },
- {
- fieldname: "selected_date",
- fieldtype: "Date",
- label: "Selected Date",
- default: selectedDate,
- read_only: 1,
- },
- {
- fieldname: "start_time",
- fieldtype: "Time",
- label: "Start Time",
- default: minutesToTimeString(eventStartMinutes),
- reqd: 1,
- },
- {
- fieldname: "finish_time",
- fieldtype: "Time",
- label: "Finish Time",
- default: minutesToTimeString(eventEndMinutes),
- reqd: 1,
- },
- ],
- primary_action_label: "Schedule & Dispatch",
- primary_action: (values) => {
- const startMins = timeStringToMinutes(values.start_time);
- const endMins = timeStringToMinutes(values.finish_time);
- if (
- startMins >= endMins ||
- startMins < START_TIME_MINUTES ||
- endMins > END_TIME_MINUTES
- ) {
- frappe.msgprint(
- "Invalid time range. Select a valid start and finish time between 07:00 - 19:00."
- );
- return;
- }
- if (endMins - startMins < 30) {
- frappe.msgprint("Time range must be at least 30 minutes.");
- return;
- }
- if (is_overlapping(technician, startMins, endMins)) {
- frappe.msgprint(
- "Time overlap detected! Please select a different time."
- );
- return;
- }
- const scheduledStartDatetime = formatDatetime(
- values.selected_date,
- values.start_time
- );
- const scheduledFinishDatetime = formatDatetime(
- values.selected_date,
- values.finish_time
- );
- create_appointment(
- values.selected_date,
- values.service_order,
- scheduledStartDatetime,
- scheduledFinishDatetime,
- technician,
- (appointment_data) => {
- const appointment_id = appointment_data.name;
- const appointment_status = appointment_data.status || "Dispatched";
- const event_color =
- appointment_data.color ||
- status_colors[appointment_status] ||
- "#007bff";
- const duration = endMins - startMins;
- const pos = calculatePosition(startMins, endMins);
- const $eventEl = $(`
-
- ${transformServiceOrder(values.service_order)} (${values.start_time} - ${
- values.finish_time
- })
-
- `);
- $eventEl.css({
- background: event_color,
- left: pos.leftPercent + "%",
- width: pos.widthPercent + "%",
- });
- $(timelineCell).append($eventEl);
- attachResizeHandles($eventEl);
- d.set_value("appointment", appointment_id);
- d.hide();
- }
- );
- },
- });
- d.show();
-}
-
-// Render schedule grid
-function render_schedule_grid(technicians, selected_date, appointments) {
- const gridContainer = $(`
-
- `);
-
- // Create a merged header row with search input and bold time labels.
- const headerRow = $(`
-
- `);
- // Insert bold time labels inside the timeline-cell; on mobile display only the hour (e.g., "7") with a smaller font.
- for (let m = 0; m < TOTAL_WORKING_MINUTES; m += 60) {
- const rawTime = minutesToTimeString(START_TIME_MINUTES + m);
- const displayTime = isMobile() ? parseInt(rawTime.split(":")[0]) : rawTime;
- const leftPercent = (m / TOTAL_WORKING_MINUTES) * 100;
- const labelDiv = $(`
-
- ${displayTime}
-
- `);
- headerRow.find(".timeline-cell").append(labelDiv);
- }
- gridContainer.append(headerRow);
-
- // Separator: horizontal line spanning full width.
- const separator = $(`
-
- `);
- gridContainer.append(separator);
-
- // Technician rows container.
- const techRowsContainer = $(``);
- technicians.forEach((tech) => {
- const techRow = $(`
-
-
- ${tech.full_name}
-
-
-
- `);
- techRowsContainer.append(techRow);
- });
- gridContainer.append(techRowsContainer);
- $("#schedule-grid").html(gridContainer);
-
- // Timeline background: vertical grid lines now start below the header row.
- const timelineBackground = $(`
-
- `);
- for (let m = 0; m <= TOTAL_WORKING_MINUTES; m += 10) {
- const leftPercent = (m / TOTAL_WORKING_MINUTES) * 100;
- const lineWidth = m % 60 === 0 ? 2 : 1;
- const lineColor = m % 60 === 0 ? "#aaa" : "#ddd";
- const lineDiv = $(`
-
- `);
- timelineBackground.append(lineDiv);
- }
- gridContainer.append(timelineBackground);
-
- // Filter technician rows but exclude the header row so that the search input stays visible.
- $("#technician-search")
- .off("keyup")
- .on(
- "keyup",
- debounce(function () {
- const value = $(this).val().toLowerCase();
- $(".technician-row")
- .not(".header-row")
- .filter(function () {
- $(this).toggle($(this).text().toLowerCase().includes(value));
- });
- }, 300)
- );
-
- appointments.forEach((appointment) => {
- appointment.service_technicians.forEach((technicianName) => {
- technicianName = technicianName.trim();
- const $techRow = $(`.technician-row[data-tech='${technicianName}']`);
- if ($techRow.length === 0) return;
- const timelineCell = $techRow.find(".timeline-cell");
- const startMins = timeStringToMinutes(appointment.start_time);
- const endMins = timeStringToMinutes(appointment.finish_time);
- const pos = calculatePosition(startMins, endMins);
- const appointment_status = appointment.status || "Dispatched";
- const event_color =
- appointment.color || status_colors[appointment_status] || "#007bff";
- const $ev = $(`
-
- ${transformServiceOrder(appointment.service_order)} (${
- appointment.start_time
- } - ${appointment.finish_time})
-
- `);
- $ev.css({
- background: event_color,
- left: pos.leftPercent + "%",
- width: pos.widthPercent + "%",
- });
- timelineCell.append($ev);
- attachResizeHandles($ev);
- });
- });
-}
-
-// Edit event dialog
-function edit_event(eventElement) {
- if (justResized) return;
- const appointmentId = $(eventElement).data("appointment");
- const service_order = $(eventElement).data("service-order");
- const start_time = $(eventElement).attr("data-start");
- const finish_time = $(eventElement).attr("data-end");
- const technician = $(eventElement).data("tech");
- const selectedDate = currentSelectedDate || frappe.datetime.get_today();
- const d = new frappe.ui.Dialog({
- title: "Edit Schedule",
- fields: [
- {
- fieldname: "appointment",
- fieldtype: "Link",
- options: "Service Appointment",
- label: "Appointment",
- default: appointmentId,
- read_only: 1,
- },
- {
- fieldname: "service_order",
- fieldtype: "Link",
- options: "Service Order",
- label: "Service Order",
- default: service_order,
- read_only: 1,
- },
- {
- fieldname: "technician",
- fieldtype: "Link",
- options: "Service Technician",
- label: "Technician",
- default: technician,
- read_only: 1,
- },
- { fieldtype: "Column Break" },
- {
- fieldname: "selected_date",
- fieldtype: "Date",
- label: "Selected Date",
- default: selectedDate,
- read_only: 1,
- },
- {
- fieldname: "start_time",
- fieldtype: "Time",
- label: "Start Time",
- default: start_time,
- reqd: 1,
- },
- {
- fieldname: "finish_time",
- fieldtype: "Time",
- label: "Finish Time",
- default: finish_time,
- reqd: 1,
- },
- ],
- primary_action_label: "Update",
- primary_action: (values) => {
- const newStart = timeStringToMinutes(values.start_time);
- const newEnd = timeStringToMinutes(values.finish_time);
- if (
- newStart >= newEnd ||
- newStart < START_TIME_MINUTES ||
- newEnd > END_TIME_MINUTES
- ) {
- frappe.msgprint(
- "Invalid time range. Select a valid start and finish time between 07:00 - 19:00."
- );
- return;
- }
- if (newEnd - newStart < 30) {
- frappe.msgprint("Time range must be at least 30 minutes.");
- return;
- }
- const pos = calculatePosition(newStart, newEnd);
- $(eventElement).css({
- left: pos.leftPercent + "%",
- width: pos.widthPercent + "%",
- });
- $(eventElement).attr("data-start", values.start_time);
- $(eventElement).attr("data-end", values.finish_time);
- $(eventElement).html(
- `${transformServiceOrder(values.service_order)} (${
- values.start_time
- } - ${values.finish_time})`
- );
-
- const scheduledStartDatetime = formatDatetime(
- values.selected_date,
- values.start_time
- );
- const scheduledFinishDatetime = formatDatetime(
- values.selected_date,
- values.finish_time
- );
- update_appointment(
- values.appointment,
- values.selected_date,
- values.service_order,
- scheduledStartDatetime,
- scheduledFinishDatetime,
- $(eventElement).data("tech")
- );
- d.hide();
- },
- });
- d.show();
-}
-
-// Drag & drop handler
-function drop(event, timelineCell) {
- event.preventDefault();
- const draggedHtml = event.dataTransfer.getData("text");
- const draggedElement = $(draggedHtml);
- const oldStart = timeStringToMinutes(draggedElement.attr("data-start"));
- const oldEnd = timeStringToMinutes(draggedElement.attr("data-end"));
- const duration = oldEnd - oldStart;
- const timelineOffset = $(timelineCell).offset();
- const dropX = event.pageX - timelineOffset.left;
- const timelineWidth = $(timelineCell).width();
- let minutesFromStart = (dropX / timelineWidth) * TOTAL_WORKING_MINUTES;
- minutesFromStart = roundToNearestTen(minutesFromStart);
- let newStartMins = START_TIME_MINUTES + minutesFromStart;
- let newEndMins = newStartMins + duration;
- if (newStartMins < START_TIME_MINUTES) {
- newStartMins = START_TIME_MINUTES;
- newEndMins = newStartMins + duration;
- }
- if (newEndMins > END_TIME_MINUTES) {
- newStartMins = END_TIME_MINUTES - duration;
- newEndMins = END_TIME_MINUTES;
- }
- const technician = $(timelineCell).closest(".technician-row").data("tech");
- if (
- is_overlapping_excluding(
- technician,
- newStartMins,
- newEndMins,
- draggedElement.data("appointment")
- )
- ) {
- frappe.msgprint(
- "Time overlap detected after drop! Please choose a different position."
- );
- return;
- }
- const serviceOrder = draggedElement.data("service-order");
- if (!serviceOrder) {
- frappe.msgprint("Service Order is required. Please create an event first.");
- return;
- }
- const newStartTime = minutesToTimeString(newStartMins);
- const newEndTime = minutesToTimeString(newEndMins);
- $(
- `.schedule-event[data-appointment='${draggedElement.data("appointment")}']`
- ).remove();
- const pos = calculatePosition(newStartMins, newEndMins);
- const event_color =
- draggedElement.data("color") ||
- status_colors[draggedElement.data("status")] ||
- "#007bff";
- const event_html = `
-
- ${transformServiceOrder(serviceOrder)} (${newStartTime} - ${newEndTime})
-
- `;
- const $newEvent = $(event_html);
- $newEvent.css({
- background: event_color,
- left: pos.leftPercent + "%",
- width: pos.widthPercent + "%",
- });
- $(timelineCell).append($newEvent);
- const selectedDate = currentSelectedDate || frappe.datetime.get_today();
- const scheduledStartDatetime = formatDatetime(selectedDate, newStartTime);
- const scheduledFinishDatetime = formatDatetime(selectedDate, newEndTime);
- update_appointment(
- draggedElement.data("appointment"),
- selectedDate,
- serviceOrder,
- scheduledStartDatetime,
- scheduledFinishDatetime,
- technician
- );
-}
-
-// --- Context Menu ---
-// For "Dispatched": show [Start Work, Invoice].
-// For "Scheduled": show [Reschedule, Invoice].
-
-// Desktop right-click context menu.
-$(document).on("contextmenu", ".schedule-event", function (e) {
- var $this = $(this);
- var status = $this.data("status");
- if (status !== "Dispatched" && status !== "Scheduled") {
- return;
- }
- e.preventDefault();
- $("#custom-context-menu").remove();
- var appointmentId = $this.data("appointment");
- var menuItems = "";
- if (status === "Dispatched") {
- menuItems = `
-
-
- `;
- } else if (status === "Scheduled") {
- menuItems = `
-
-
- `;
- }
- var menu = $(`
-
- `);
- menu.data("appointmentId", appointmentId);
- menu.data("eventElement", this);
- menu.css({ top: e.pageY + "px", left: e.pageX + "px" });
- $("body").append(menu);
-});
-
-$(document)
- .on("mouseenter", "#custom-context-menu .context-menu-item", function () {
- $(this).css("background", "#f5f5f5");
- })
- .on("mouseleave", "#custom-context-menu .context-menu-item", function () {
- $(this).css("background", "#fff");
- });
-
-$(document).on("click", ".context-menu-item", function (e) {
- e.stopPropagation();
- var action = $(this).data("action");
- var appointmentId = $("#custom-context-menu").data("appointmentId");
- var eventElement = $("#custom-context-menu").data("eventElement");
- $("#custom-context-menu").remove();
- if (action === "start_work") {
- startWork(appointmentId);
- } else if (action === "reschedule") {
- edit_event(eventElement);
- } else if (action === "invoice") {
- invoiceAppointment(appointmentId);
- }
-});
-
-$(document).on("click", function (e) {
- $("#custom-context-menu").remove();
-});
-
-// Mobile long-press context menu.
-var touchTimer;
-$(document)
- .on("touchstart", ".schedule-event", function (e) {
- var $this = $(this);
- touchTimer = setTimeout(function () {
- var status = $this.data("status");
- if (status !== "Dispatched" && status !== "Scheduled") {
- return;
- }
- $("#custom-context-menu").remove();
- var appointmentId = $this.data("appointment");
- var touch = e.originalEvent.touches[0];
- var menuItems = "";
- if (status === "Dispatched") {
- menuItems = `
-
-
- `;
- } else if (status === "Scheduled") {
- menuItems = `
-
-
- `;
- }
- var menu = $(`
-
- `);
- menu.data("appointmentId", appointmentId);
- menu.data("eventElement", $this.get(0));
- menu.css({ top: touch.pageY + "px", left: touch.pageX + "px" });
- $("body").append(menu);
- }, 800);
- })
- .on("touchend touchcancel", ".schedule-event", function (e) {
- clearTimeout(touchTimer);
- });
-
-// Backend actions for context menu
-function startWork(appointmentId) {
- frappe.call({
- method:
- "beveren_fsm.field_service_management.page.service_scheduling.service_scheduling.start_work",
- args: { appointment_id: appointmentId },
- callback: function (r) {
- if (!r.exc) {
- frappe.msgprint("Appointment started");
- refresh_schedule_grid(currentSelectedDate);
- }
- },
- });
-}
-
-function invoiceAppointment(appointmentId) {
- frappe.set_route("Form", "Service Appointment", appointmentId);
-}
-
-// Date table & refresh functions
-function render_date_table(selected_date) {
- const dates = generate_date_range(selected_date);
- let table_html = ``;
- dates.forEach((date) => {
- const isSelected = date === selected_date ? "selected-date" : "";
- const monthName = new Date(date)
- .toLocaleString("en-us", { month: "short" })
- .toUpperCase();
- table_html += ``;
- });
- table_html += `
`;
- $("#date-table").html(table_html);
-}
-
-function filter_by_date(date) {
- $(".date-header").removeClass("selected-date");
- $(`.date-header[data-date='${date}']`).addClass("selected-date");
- currentSelectedDate = date;
- load_schedule(date);
-}
-
-function format_date(date) {
- const dateObj = new Date(date);
- const dayName = dateObj
- .toLocaleString("en-us", { weekday: "short" })
- .toUpperCase();
- const dayNum = dateObj.getDate();
- return `${dayName}
${dayNum}
`;
-}
-
-function refresh_schedule_grid(selected_date) {
- $("#schedule-grid").html(`
-
- `);
- setTimeout(() => {
- load_schedule(selected_date);
- }, 100);
-}
-
-// Backend calls
-function create_appointment(
- selected_date,
- service_order,
- scheduled_start_datetime,
- scheduled_finish_datetime,
- technician,
- callback
-) {
- frappe.call({
- method:
- "beveren_fsm.field_service_management.page.service_scheduling.service_scheduling.create_service_appointment",
- args: {
- selected_date,
- service_order,
- scheduled_start_datetime,
- scheduled_finish_datetime,
- technician,
- },
- callback: (r) => {
- if (!r.exc) {
- if (callback) {
- callback(r.message);
- }
- refresh_schedule_grid(selected_date);
- } else {
- frappe.msgprint("Failed to update schedule.");
- }
- },
- });
-}
-
-function update_appointment(
- appointment_id,
- selected_date,
- service_order,
- start_time,
- end_time,
- technician
-) {
- frappe.call({
- method:
- "beveren_fsm.field_service_management.page.service_scheduling.service_scheduling.update_service_appointment",
- args: {
- appointment_id,
- selected_date,
- service_order,
- scheduled_start_datetime: start_time,
- scheduled_finish_datetime: end_time,
- technician,
- },
- callback: (r) => {
- if (!r.exc) {
- refresh_schedule_grid(selected_date);
- }
- },
- });
-}
-
-function load_schedule(selected_date) {
- currentSelectedDate = selected_date;
- frappe.call({
- method:
- "beveren_fsm.field_service_management.page.service_scheduling.service_scheduling.get_schedule_data",
- args: { selected_date },
- callback: (r) => {
- if (r.message) {
- render_date_table(selected_date);
- render_schedule_grid(
- r.message.technicians,
- selected_date,
- r.message.appointments
- );
- } else if (r.exc) {
- console.error("Error fetching schedule data", r.exc);
- frappe.msgprint("An error occurred while loading the schedule.");
- }
- },
- });
-}
diff --git a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.json b/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.json
deleted file mode 100644
index a3c81b9..0000000
--- a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "content": null,
- "creation": "2025-02-12 12:35:28.281192",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2025-02-12 12:35:28.281192",
- "modified_by": "Administrator",
- "module": "Field Service Management",
- "name": "service-scheduling",
- "owner": "Administrator",
- "page_name": "service-scheduling",
- "roles": [],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 0
-}
diff --git a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.py b/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.py
deleted file mode 100644
index 8924215..0000000
--- a/beveren_fsm/field_service_management/page/service_scheduling/service_scheduling.py
+++ /dev/null
@@ -1,164 +0,0 @@
-import frappe
-from frappe.utils import get_datetime, getdate
-
-
-@frappe.whitelist()
-def get_schedule_data(selected_date):
- # Fetch Technicians
- technicians = frappe.get_all("Service Technician", fields=["name", "full_name"])
-
- # Convert selected_date to a date object
- selected_date = getdate(selected_date)
-
- # Fetch Service Appointments for the selected date
- appointments = frappe.get_all(
- "Service Appointment",
- filters={
- "posting_date": selected_date,
- },
- fields=[
- "name",
- "posting_date",
- "service_order",
- "scheduled_start_datetime",
- "scheduled_finish_datetime",
- ],
- )
-
- # Get metadata for Service Appointment to fetch workflow states and their colors
- meta = frappe.get_meta("Service Appointment")
-
- appointments_with_technicians = []
- for appointment in appointments:
- # Get the full document to access child table and status.
- appointment_doc = frappe.get_doc("Service Appointment", appointment.name)
-
- # Fetch service technicians from child table
- service_technicians = [
- frappe.get_doc("Service Technician", tech.service_technician).name
- for tech in appointment_doc.service_technicians
- ]
-
- # Convert scheduled datetime fields to proper datetime objects
- start_time = get_datetime(appointment_doc.scheduled_start_datetime)
- finish_time = get_datetime(appointment_doc.scheduled_finish_datetime)
-
- # Get the color for the current state using meta.states
- state_color = None
- if meta.states:
- for s in meta.states:
- if s.title == appointment_doc.status:
- state_color = s.color
- break
-
- # Prepare the structured appointment data
- structured_appointment = {
- "name": appointment.name,
- "service_order": appointment.service_order,
- "start_time": start_time.strftime("%H:%M"),
- "finish_time": finish_time.strftime("%H:%M"),
- "service_technicians": service_technicians,
- "status": appointment_doc.status,
- "color": state_color,
- }
- appointments_with_technicians.append(structured_appointment)
-
- return {"technicians": technicians, "appointments": appointments_with_technicians}
-
-
-@frappe.whitelist()
-def create_service_appointment(
- selected_date, service_order, scheduled_start_datetime, scheduled_finish_datetime, technician
-):
- # This function only creates a new appointment if one does NOT already exist
- # for the same posting date, service order, scheduled times, and where the specified technician exists.
-
- # Convert the datetime strings to datetime objects.
- scheduled_start_datetime = get_datetime(scheduled_start_datetime)
- scheduled_finish_datetime = get_datetime(scheduled_finish_datetime)
-
- # Search for an existing Service Appointment using the main filters.
- appointment_list = frappe.get_all(
- "Service Appointment",
- filters={
- "posting_date": getdate(selected_date),
- "service_order": service_order,
- "scheduled_start_datetime": scheduled_start_datetime,
- "scheduled_finish_datetime": scheduled_finish_datetime,
- },
- limit=10,
- )
-
- found = None
- for app in appointment_list:
- app_doc = frappe.get_doc("Service Appointment", app.name)
- # Check if the technician is in the child table.
- for row in app_doc.get("service_technicians"):
- if row.service_technician == technician:
- found = app_doc
- break
- if found:
- break
-
- if found:
- appointment = found
- else:
- # Create a new Service Appointment.
- appointment = frappe.new_doc("Service Appointment")
- appointment.posting_date = getdate(selected_date)
- appointment.service_order = service_order
- appointment.scheduled_start_datetime = scheduled_start_datetime
- appointment.scheduled_finish_datetime = scheduled_finish_datetime
- appointment.append("service_technicians", {"service_technician": technician})
- appointment.save()
- appointment.submit()
-
- # Get metadata to fetch the workflow state color.
- meta = frappe.get_meta("Service Appointment")
- state_color = None
- if meta.states:
- for s in meta.states:
- if s.title == appointment.status:
- state_color = s.color
- break
-
- return {"name": appointment.name, "status": appointment.status, "color": state_color}
-
-
-@frappe.whitelist()
-def update_service_appointment(
- appointment_id,
- selected_date,
- service_order,
- scheduled_start_datetime,
- scheduled_finish_datetime,
- technician,
-):
- appointment = frappe.get_doc("Service Appointment", appointment_id)
-
- appointment.update(
- {
- "posting_date": getdate(selected_date),
- "service_order": service_order,
- "scheduled_start_datetime": get_datetime(scheduled_start_datetime),
- "scheduled_finish_datetime": get_datetime(scheduled_finish_datetime),
- }
- )
-
- # Clear existing technicians and add the new one.
- appointment.set("service_technicians", [])
- appointment.append("service_technicians", {"service_technician": technician})
-
- appointment.save()
- return appointment.name
-
-
-@frappe.whitelist()
-def start_work(appointment_id):
- appointment = frappe.get_doc("Service Appointment", appointment_id)
- if appointment.status == "Dispatched":
- appointment.status = "In Progress"
- appointment.save(ignore_permissions=True)
- return appointment.name
- else:
- frappe.throw("Appointment is not in Dispatched status. Cannot start work.")
diff --git a/beveren_fsm/hooks.py b/beveren_fsm/hooks.py
index 615e9ef..5fa0002 100644
--- a/beveren_fsm/hooks.py
+++ b/beveren_fsm/hooks.py
@@ -20,6 +20,7 @@
# "has_permission": "beveren_fsm.api.permission.has_app_permission"
# }
# ]
+
fixtures = [
# Export your custom "Service Type" doctype
"Service Type",
diff --git a/schedule/src/components/layout/sidebar-menu.tsx b/schedule/src/components/layout/sidebar-menu.tsx
index 3fc0128..a2b028b 100644
--- a/schedule/src/components/layout/sidebar-menu.tsx
+++ b/schedule/src/components/layout/sidebar-menu.tsx
@@ -1,29 +1,38 @@
"use client";
-import { Home, Users, Settings } from "lucide-react";
+import { Home, Users, Settings, ClipboardList } from "lucide-react";
import { cn } from "../../lib/utils";
import { useState } from "react";
interface MenuItem {
icon: React.ComponentType<{ className?: string }>;
label: string;
- active?: boolean;
+ key: "home" | "requests" | "technicians" | "settings";
onClick?: () => void;
}
interface SidebarMenuProps {
+ activeMenu: "home" | "requests" | "technicians" | "settings";
onTechniciansClick?: () => void;
onScheduleClick?: () => void;
+ onRequestsClick?: () => void;
onSettingsClick?: () => void;
}
-export function SidebarMenu({ onTechniciansClick, onScheduleClick, onSettingsClick }: SidebarMenuProps) {
+export function SidebarMenu({
+ activeMenu,
+ onTechniciansClick,
+ onScheduleClick,
+ onRequestsClick,
+ onSettingsClick,
+}: SidebarMenuProps) {
const [hoveredItem, setHoveredItem] = useState(null);
const menuItems: MenuItem[] = [
- { icon: Home, label: "Home", active: true, onClick: onScheduleClick },
- { icon: Users, label: "Technicians", onClick: onTechniciansClick },
- { icon: Settings, label: "Settings", onClick: onSettingsClick },
+ { icon: Home, label: "Home", key: "home", onClick: onScheduleClick },
+ { icon: ClipboardList, label: "Requests", key: "requests", onClick: onRequestsClick },
+ { icon: Users, label: "Technicians", key: "technicians", onClick: onTechniciansClick },
+ { icon: Settings, label: "Settings", key: "settings", onClick: onSettingsClick },
];
return (
@@ -42,7 +51,7 @@ export function SidebarMenu({ onTechniciansClick, onScheduleClick, onSettingsCli