diff --git a/src/pages/workorders/api.js b/src/pages/workorders/api.js
index ebdfbb8b..c069fb05 100644
--- a/src/pages/workorders/api.js
+++ b/src/pages/workorders/api.js
@@ -79,6 +79,16 @@ export const addComment = async (workOrderId, text, commentType = "internal") =>
});
};
+export const changeWorkOrderAssignment = async (id, assignTo) => {
+ return post(`${API_URL}/WorkOrder/ChangeAssignment?id=${id}&assignTo=${encodeURIComponent(assignTo || "")}`);
+};
+
+export const fetchUsers = async () => {
+ const { get: apiGet } = await import("../../apiUtil");
+ const data = await apiGet(`${API_URL}/User`);
+ return (data || []).map((u) => ({ id: u.id ?? u.Id, name: u.name ?? u.Name ?? u.userName ?? "" }));
+};
+
export const changeWorkOrderStatus = async (id, status) => {
return post(`${API_URL}/WorkOrder/ChangeStatus?id=${id}&status=${encodeURIComponent(status)}`);
};
diff --git a/src/pages/workorders/list/Table.js b/src/pages/workorders/list/Table.js
index 01c9c8c8..9f4319a1 100644
--- a/src/pages/workorders/list/Table.js
+++ b/src/pages/workorders/list/Table.js
@@ -1,13 +1,175 @@
-import React from "react";
+import React, { useState, useEffect, useRef } from "react";
+import ReactDOM from "react-dom";
import SharedTable from "../../../components/SharedTable";
+import { fetchUsers, changeWorkOrderAssignment, changeWorkOrderStatus } from "../api";
+import { toast } from "react-toastify";
+
+const STATUS_OPTIONS = ["Open", "In Progress", "On Hold", "Done", "Cancelled"];
+
+function StatusDropdown({ value, rowId, onUpdate }) {
+ const [open, setOpen] = useState(false);
+ const btnRef = useRef(null);
+ const menuRef = useRef(null);
+ const [menuPos, setMenuPos] = useState({ top: 0, left: 0 });
+ const statusClass = (value || "").toLowerCase().replace(/\s+/g, "-");
+
+ useEffect(() => {
+ const handleClick = (e) => {
+ if (btnRef.current?.contains(e.target)) return;
+ if (menuRef.current?.contains(e.target)) return;
+ setOpen(false);
+ };
+ document.addEventListener("mousedown", handleClick);
+ return () => document.removeEventListener("mousedown", handleClick);
+ }, []);
+
+ const handleOpen = () => {
+ if (btnRef.current) {
+ const rect = btnRef.current.getBoundingClientRect();
+ setMenuPos({ top: rect.bottom + 4, left: rect.left });
+ }
+ setOpen(!open);
+ };
+
+ const handleSelect = async (newStatus) => {
+ if (newStatus === value) { setOpen(false); return; }
+ setOpen(false);
+ try {
+ await changeWorkOrderStatus(rowId, newStatus);
+ onUpdate(rowId, newStatus);
+ toast.success(`Status changed to ${newStatus}`);
+ } catch (err) {
+ toast.error("Failed to update status");
+ }
+ };
+
+ return (
+ <>
+
+ {open && ReactDOM.createPortal(
+
+ {STATUS_OPTIONS.map((s) => (
+
+ ))}
+
,
+ document.body
+ )}
+ >
+ );
+}
+
+function AssignDropdown({ value, rowId, onUpdate }) {
+ const [open, setOpen] = useState(false);
+ const [users, setUsers] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+ const btnRef = useRef(null);
+ const menuRef = useRef(null);
+ const [menuPos, setMenuPos] = useState({ top: 0, left: 0 });
+
+ useEffect(() => {
+ const handleClick = (e) => {
+ if (btnRef.current?.contains(e.target)) return;
+ if (menuRef.current?.contains(e.target)) return;
+ setOpen(false);
+ };
+ document.addEventListener("mousedown", handleClick);
+ return () => document.removeEventListener("mousedown", handleClick);
+ }, []);
+
+ const handleOpen = async () => {
+ if (!loaded) {
+ try {
+ const data = await fetchUsers();
+ setUsers(data);
+ setLoaded(true);
+ } catch (err) {
+ toast.error("Failed to load users");
+ return;
+ }
+ }
+ if (btnRef.current) {
+ const rect = btnRef.current.getBoundingClientRect();
+ setMenuPos({ top: rect.bottom + 4, left: rect.left });
+ }
+ setOpen(!open);
+ };
+
+ const handleSelect = async (userId, userName) => {
+ setOpen(false);
+ try {
+ await changeWorkOrderAssignment(rowId, userId);
+ onUpdate(rowId, userName);
+ toast.success(`Assigned to ${userName || "Unassigned"}`);
+ } catch (err) {
+ toast.error("Failed to update assignment");
+ }
+ };
+
+ return (
+ <>
+
+ {open && ReactDOM.createPortal(
+
+
+ {users.map((u) => (
+
+ ))}
+
,
+ document.body
+ )}
+ >
+ );
+}
+
+export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages, onRowUpdate }) {
+ const [localRows, setLocalRows] = useState(rows);
+
+ useEffect(() => {
+ setLocalRows(rows);
+ }, [rows]);
+
+ const handleAssignUpdate = (rowId, newName) => {
+ setLocalRows((prev) =>
+ prev.map((r) => r.id === rowId ? { ...r, assignedTo: newName, employeeName: newName } : r)
+ );
+ };
+
+ const handleStatusUpdate = (rowId, newStatus) => {
+ setLocalRows((prev) =>
+ prev.map((r) => r.id === rowId ? { ...r, status: newStatus } : r)
+ );
+ };
-export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
const columns = [
{ key: "number", label: "WO #", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
{ key: "title", label: "Title" },
{ key: "location", label: "Location" },
{ key: "priority", label: "Priority" },
- { key: "status", label: "Status" },
+ { key: "status", label: "Status", render: (val, row) => (
+
+ )},
{ key: "createdDate", label: "Created", render: (val) => {
if (!val) return "";
const d = new Date(val.endsWith("Z") ? val : val + "Z");
@@ -17,10 +179,12 @@ export default function Table({ loading, error, rows, onEdit, onDelete, page, se
const tz = d.toLocaleTimeString(undefined, { timeZoneName: "short" }).split(" ").pop();
return `${date} ${time} ${tz}`;
}},
- { key: "assignedTo", label: "Assigned To" },
+ { key: "assignedTo", label: "Assigned To", render: (val, row) => (
+
+ )},
];
return (
-
+
);
}
diff --git a/src/pages/workorders/list/styles.css b/src/pages/workorders/list/styles.css
index 33dfaf54..54af4470 100644
--- a/src/pages/workorders/list/styles.css
+++ b/src/pages/workorders/list/styles.css
@@ -238,6 +238,91 @@
border-right: 1px solid #ddd;
}
+.assignDropdownBtn {
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-size: 13px;
+ color: #333;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.assignDropdownBtn:hover {
+ border-color: #ccc;
+ background: #f5f5f5;
+}
+
+.assignDropdownMenu {
+ position: fixed;
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-shadow: 0 6px 20px rgba(0,0,0,0.2);
+ z-index: 99999;
+ min-width: 180px;
+ max-height: 250px;
+ overflow-y: auto;
+ padding: 4px 0;
+}
+
+.assignDropdownOption {
+ display: block;
+ width: 100%;
+ padding: 8px 14px;
+ border: none;
+ background: #fff;
+ font-size: 13px;
+ color: #333;
+ cursor: pointer;
+ text-align: left;
+}
+
+.assignDropdownOption:hover {
+ background: #f0f0f0;
+}
+
+.assignDropdownOption.active {
+ font-weight: 600;
+ background: #e6f0ff;
+}
+
+.woStatusDot.open { background: #007bff; }
+.woStatusDot.in-progress { background: #ffc107; }
+.woStatusDot.on-hold { background: #fd7e14; }
+.woStatusDot.done { background: #28a745; }
+.woStatusDot.cancelled { background: #dc3545; }
+
+.woStatusBadge {
+ display: inline-block;
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 500;
+ color: #fff;
+ background: #6c757d;
+}
+
+.woStatusBadge.open { background: #007bff; }
+.woStatusBadge.in-progress { background: #ffc107; color: #333; }
+.woStatusBadge.on-hold { background: #fd7e14; }
+.woStatusBadge.done { background: #28a745; }
+.woStatusBadge.cancelled { background: #dc3545; }
+
+.woStatusClickable {
+ cursor: pointer;
+}
+
+.woStatusClickable:hover {
+ filter: brightness(0.9);
+}
+
+.woStatusCaret {
+ font-size: 10px;
+ margin-left: 4px;
+}
+
/* Table wrapper */
.tableContainer {
background: white;
diff --git a/src/site.css b/src/site.css
index 652b1572..7144924f 100644
--- a/src/site.css
+++ b/src/site.css
@@ -153,6 +153,8 @@ body {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+ display: flex;
+ align-items: center;
}
.gridHeader {