Add interactive status dropdown on work order view page

- Click the status badge to open a dropdown with all status options
- Status change calls ChangeStatus API and updates locally
- Color-coded status dots in the dropdown menu
- Closes on outside click
- Also logged in audit trail via backend ChangeStatus endpoint
This commit is contained in:
Adam Moussa 2026-04-16 18:27:00 -04:00
parent d3a4bcc984
commit 3b15a43879
3 changed files with 131 additions and 4 deletions

View file

@ -78,6 +78,10 @@ export const addComment = async (workOrderId, text, commentType = "internal") =>
});
};
export const changeWorkOrderStatus = async (id, status) => {
return post(`${API_URL}/WorkOrder/ChangeStatus?id=${id}&status=${encodeURIComponent(status)}`);
};
export const deleteWorkOrder = async (id) => {
try {
await workOrderApi.delete(id);

View file

@ -34,6 +34,75 @@
.woStatusBadge.done, .woStatusBadge.completed { background: #28a745; }
.woStatusBadge.cancelled { background: #dc3545; }
.woStatusDropdown {
position: relative;
display: inline-block;
}
.woStatusClickable {
cursor: pointer;
user-select: none;
}
.woStatusClickable:hover {
filter: brightness(0.9);
}
.woStatusCaret {
font-size: 10px;
margin-left: 4px;
}
.woStatusMenu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
background: #fff;
border: 1px solid #ddd;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
z-index: 100;
min-width: 160px;
overflow: hidden;
}
.woStatusOption {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 14px;
border: none;
background: none;
font-size: 13px;
color: #333;
cursor: pointer;
text-align: left;
}
.woStatusOption:hover {
background: #f5f5f5;
}
.woStatusOption.active {
font-weight: 600;
background: #f0f7ff;
}
.woStatusDot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.woStatusDot.open { background: #007bff; }
.woStatusDot.in-progress { background: #ffc107; }
.woStatusDot.on-hold { background: #fd7e14; }
.woStatusDot.done { background: #28a745; }
.woStatusDot.cancelled { background: #dc3545; }
.woViewActions {
display: flex;
gap: 8px;

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import { toast } from "react-toastify";
import { getWorkOrderById, addComment } from "../api";
import { getWorkOrderById, addComment, changeWorkOrderStatus } from "../api";
import "./WorkOrderViewPage.css";
function formatDateTime(value) {
@ -117,12 +117,68 @@ function CommentInput({ workOrderId, commentType, onCommentAdded }) {
);
}
const STATUS_OPTIONS = ["Open", "In Progress", "On Hold", "Done", "Cancelled"];
function StatusDropdown({ status, onStatusChange }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
const statusClass = (status || "").toLowerCase().replace(/\s+/g, "-");
useEffect(() => {
const handleClick = (e) => {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
return (
<div className="woStatusDropdown" ref={ref}>
<button
className={`woStatusBadge ${statusClass} woStatusClickable`}
onClick={() => setOpen(!open)}
>
{status} <span className="woStatusCaret">&#9662;</span>
</button>
{open && (
<div className="woStatusMenu">
{STATUS_OPTIONS.map((s) => (
<button
key={s}
className={`woStatusOption ${s === status ? "active" : ""}`}
onClick={() => { onStatusChange(s); setOpen(false); }}
>
<span className={`woStatusDot ${s.toLowerCase().replace(/\s+/g, "-")}`} />
{s}
</button>
))}
</div>
)}
</div>
);
}
export default function WorkOrderViewPage() {
const { id } = useParams();
const navigate = useNavigate();
const [wo, setWo] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState("customer");
const [statusUpdating, setStatusUpdating] = useState(false);
const handleStatusChange = async (newStatus) => {
if (newStatus === wo?.status) return;
setStatusUpdating(true);
try {
await changeWorkOrderStatus(id, newStatus);
setWo((prev) => ({ ...prev, status: newStatus }));
toast.success(`Status changed to ${newStatus}`);
} catch (err) {
toast.error("Failed to update status");
} finally {
setStatusUpdating(false);
}
};
useEffect(() => {
const load = async () => {
@ -159,8 +215,6 @@ export default function WorkOrderViewPage() {
if (loading) return <div className="woViewPage"><div className="loading">Loading...</div></div>;
if (!wo) return <div className="woViewPage"><div className="loading">Work order not found.</div></div>;
const statusClass = (wo.status || "").toLowerCase().replace(/\s+/g, "-");
const allComments = wo.comments || [];
const customerComments = allComments.filter((c) => c.commentType === "customer");
const vendorComments = allComments.filter((c) => c.commentType === "vendor");
@ -192,7 +246,7 @@ export default function WorkOrderViewPage() {
<div className="woViewHeader">
<div className="woViewTitle">
<h2>{wo.title}</h2>
<span className={`woStatusBadge ${statusClass}`}>{wo.status}</span>
<StatusDropdown status={wo.status} onStatusChange={handleStatusChange} />
</div>
<div className="woViewActions">
<button className="btn btn-primary" onClick={() => navigate(`/workorders/edit/${id}`)}>