mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +00:00
Add interactive status and assigned to dropdowns on work order list
- Status column shows colored badge with dropdown to change status - Assigned To column shows dropdown to reassign work orders - Both use portal-rendered menus and update rows locally - Fix grid cell vertical alignment to center content - Both changes logged in audit trail
This commit is contained in:
parent
168383b0ee
commit
29fe1173d2
4 changed files with 266 additions and 5 deletions
|
|
@ -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)}`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<button ref={btnRef} className={`woStatusBadge ${statusClass} woStatusClickable`} onClick={handleOpen} style={{ border: "none" }}>
|
||||
{value || "Open"} <span className="woStatusCaret">▾</span>
|
||||
</button>
|
||||
{open && ReactDOM.createPortal(
|
||||
<div ref={menuRef} className="assignDropdownMenu" style={{ top: menuPos.top, left: menuPos.left }}>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
className={`assignDropdownOption ${s === value ? "active" : ""}`}
|
||||
onClick={() => handleSelect(s)}
|
||||
>
|
||||
<span className={`woStatusDot ${s.toLowerCase().replace(/\s+/g, "-")}`} style={{ display: "inline-block", width: 8, height: 8, borderRadius: "50%", marginRight: 8 }} />
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
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 (
|
||||
<>
|
||||
<button ref={btnRef} className="assignDropdownBtn" onClick={handleOpen}>
|
||||
{value || "Unassigned"} <span className="woStatusCaret">▾</span>
|
||||
</button>
|
||||
{open && ReactDOM.createPortal(
|
||||
<div ref={menuRef} className="assignDropdownMenu" style={{ top: menuPos.top, left: menuPos.left }}>
|
||||
<button
|
||||
className={`assignDropdownOption ${!value ? "active" : ""}`}
|
||||
onClick={() => handleSelect("", "")}
|
||||
>
|
||||
Unassigned
|
||||
</button>
|
||||
{users.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
className={`assignDropdownOption ${value === u.name ? "active" : ""}`}
|
||||
onClick={() => handleSelect(u.id, u.name)}
|
||||
>
|
||||
{u.name}
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
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) => <strong><a href="#" onClick={(e) => { e.preventDefault(); onEdit && onEdit(row); }}>{val}</a></strong> },
|
||||
{ key: "title", label: "Title" },
|
||||
{ key: "location", label: "Location" },
|
||||
{ key: "priority", label: "Priority" },
|
||||
{ key: "status", label: "Status" },
|
||||
{ key: "status", label: "Status", render: (val, row) => (
|
||||
<StatusDropdown value={val} rowId={row.id} onUpdate={handleStatusUpdate} />
|
||||
)},
|
||||
{ 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) => (
|
||||
<AssignDropdown value={val || row.employeeName} rowId={row.id} onUpdate={handleAssignUpdate} />
|
||||
)},
|
||||
];
|
||||
|
||||
return (
|
||||
<SharedTable rows={rows} columns={columns} onEdit={onEdit} onDelete={onDelete} page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize} totalCount={totalCount} loading={loading} error={error} />
|
||||
<SharedTable rows={localRows} columns={columns} onEdit={onEdit} onDelete={onDelete} page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize} totalCount={totalCount} loading={loading} error={error} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ body {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gridHeader {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue