mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-01 19:03:12 +00:00
Add WO fields, collapsible sections, assign dropdown, settings page
- New Classification section: Problem, Trade, SubTrade dropdowns (SubTrade filters by Trade) - New Schedule fields: Scheduled, Completed dates - New Financial section: Vendor NTE, Customer NTE placeholder - All sections below Details default collapsed with toggle arrows - Assigned To on view page is now a clickable dropdown like status - TT moved to Details section - Dropdown options settings page at /settings/dropdowns with add/edit/delete - Settings link in sidebar and user dropdown
This commit is contained in:
parent
46bf66a35d
commit
feacb72337
10 changed files with 756 additions and 164 deletions
|
|
@ -46,6 +46,9 @@ import AssetFormPage from "./pages/assets/form/AssetFormPage";
|
|||
import CalendarPage from "./pages/calendar/list/CalendarPage";
|
||||
import EventFormPage from "./pages/calendar/form/EventFormPage";
|
||||
|
||||
// SETTINGS
|
||||
import DropdownOptionsPage from "./pages/settings/DropdownOptionsPage";
|
||||
|
||||
// Protected Route Component
|
||||
function ProtectedRoute({ children }) {
|
||||
return isAuthenticated() ? children : <Navigate to="/login" replace />;
|
||||
|
|
@ -121,6 +124,9 @@ export default function App() {
|
|||
<Route path="/calendar" element={<CalendarPage />} />
|
||||
<Route path="/calendar/new" element={<EventFormPage />} />
|
||||
<Route path="/calendar/edit/:id" element={<EventFormPage />} />
|
||||
|
||||
{/* SETTINGS */}
|
||||
<Route path="/settings/dropdowns" element={<DropdownOptionsPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
FaClipboardList,
|
||||
FaCubes,
|
||||
FaMapMarkerAlt,
|
||||
FaCog,
|
||||
} from "react-icons/fa";
|
||||
|
||||
const navItems = [
|
||||
|
|
@ -28,6 +29,7 @@ const navItems = [
|
|||
{ label: "PM Schedules", icon: <FaClipboardList />, path: "/pmschedules" },
|
||||
{ label: "Assets", icon: <FaCubes />, path: "/assets" },
|
||||
{ label: "Locations", icon: <FaMapMarkerAlt />, path: "/locations" },
|
||||
{ label: "Settings", icon: <FaCog />, path: "/settings/dropdowns" },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ function UserDropdown({ currentUser, onLogout, onNavigate }) {
|
|||
<button className="userDropdownItem" onClick={() => handleNav("/profile")}>
|
||||
<i className="fa fa-user"></i> My Profile
|
||||
</button>
|
||||
<button className="userDropdownItem" onClick={() => handleNav("/settings")}>
|
||||
<i className="fa fa-cog"></i> Account Settings
|
||||
<button className="userDropdownItem" onClick={() => handleNav("/settings/dropdowns")}>
|
||||
<i className="fa fa-cog"></i> Dropdown Options
|
||||
</button>
|
||||
<button className="userDropdownItem" onClick={() => handleNav("/change-password")}>
|
||||
<i className="fa fa-lock"></i> Change Password
|
||||
|
|
|
|||
182
src/pages/settings/DropdownOptionsPage.css
Normal file
182
src/pages/settings/DropdownOptionsPage.css
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
.dropdownSettingsPage {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dropdownSettingsHeader {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.dropdownSettingsHeader h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dropdownSettingsHeader p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dropdownSettingsLayout {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.dropdownSettingsSidebar {
|
||||
width: 180px;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #ddd;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dropdownSettingsTab {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dropdownSettingsTab:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.dropdownSettingsTab.active {
|
||||
background: #fff;
|
||||
color: #007bff;
|
||||
font-weight: 600;
|
||||
border-right: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.dropdownSettingsContent {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dropdownSettingsAddRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dropdownSettingsInput {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dropdownSettingsSelect {
|
||||
height: 36px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.dropdownSettingsAddBtn {
|
||||
height: 36px;
|
||||
padding: 6px 16px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdownSettingsAddBtn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dropdownSettingsAddBtn:hover:not(:disabled) {
|
||||
background: #0069d9;
|
||||
}
|
||||
|
||||
.dropdownSettingsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dropdownSettingsItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dropdownSettingsItem:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.dropdownSettingsItemValue {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdownSettingsParentTag {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dropdownSettingsItemActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dropdownSettingsBtn {
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dropdownSettingsBtn.edit:hover { background: #e0e0e0; }
|
||||
.dropdownSettingsBtn.save { background: #28a745; color: #fff; }
|
||||
.dropdownSettingsBtn.cancel { background: #6c757d; color: #fff; }
|
||||
.dropdownSettingsBtn.delete { color: #dc3545; }
|
||||
.dropdownSettingsBtn.delete:hover { background: #fff5f5; }
|
||||
|
||||
.dropdownSettingsEditInput {
|
||||
flex: 1;
|
||||
height: 30px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dropdownSettingsLoading,
|
||||
.dropdownSettingsEmpty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
192
src/pages/settings/DropdownOptionsPage.js
Normal file
192
src/pages/settings/DropdownOptionsPage.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { get, post, put, deleteItem } from "../../apiUtil";
|
||||
import { API_URL } from "../../constants";
|
||||
import { toast } from "react-toastify";
|
||||
import "./DropdownOptionsPage.css";
|
||||
|
||||
const CATEGORIES = [
|
||||
{ key: "Trade", label: "Trades", hasChildren: true },
|
||||
{ key: "SubTrade", label: "Sub-Trades", parent: "Trade" },
|
||||
{ key: "Problem", label: "Problems", hasChildren: false },
|
||||
];
|
||||
|
||||
export default function DropdownOptionsPage() {
|
||||
const [activeCategory, setActiveCategory] = useState("Trade");
|
||||
const [options, setOptions] = useState([]);
|
||||
const [parentOptions, setParentOptions] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [newValue, setNewValue] = useState("");
|
||||
const [newParent, setNewParent] = useState("");
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
|
||||
const categoryConfig = CATEGORIES.find((c) => c.key === activeCategory);
|
||||
|
||||
const loadOptions = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await get(`${API_URL}/DropdownOptions/ByCategory/${activeCategory}`);
|
||||
setOptions(data || []);
|
||||
} catch {
|
||||
toast.error("Failed to load options");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadParents = async () => {
|
||||
if (categoryConfig?.parent) {
|
||||
const data = await get(`${API_URL}/DropdownOptions/ByCategory/${categoryConfig.parent}`);
|
||||
setParentOptions(data || []);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadOptions();
|
||||
loadParents();
|
||||
setNewValue("");
|
||||
setNewParent("");
|
||||
setEditingId(null);
|
||||
}, [activeCategory]);
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!newValue.trim()) return;
|
||||
try {
|
||||
const body = {
|
||||
category: activeCategory,
|
||||
value: newValue.trim(),
|
||||
parentValue: categoryConfig?.parent ? newParent || null : null,
|
||||
sortOrder: options.length,
|
||||
isActive: true,
|
||||
};
|
||||
await post(`${API_URL}/DropdownOptions`, body);
|
||||
setNewValue("");
|
||||
setNewParent("");
|
||||
loadOptions();
|
||||
toast.success("Option added");
|
||||
} catch (err) {
|
||||
toast.error("Failed to add option");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
await deleteItem(`${API_URL}/DropdownOptions/${id}`);
|
||||
loadOptions();
|
||||
toast.success("Option deleted");
|
||||
} catch {
|
||||
toast.error("Failed to delete");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSave = async (item) => {
|
||||
try {
|
||||
await put(`${API_URL}/DropdownOptions/${item.id}`, {
|
||||
...item,
|
||||
value: editValue.trim(),
|
||||
});
|
||||
setEditingId(null);
|
||||
loadOptions();
|
||||
toast.success("Option updated");
|
||||
} catch {
|
||||
toast.error("Failed to update");
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleAdd();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="dropdownSettingsPage">
|
||||
<div className="dropdownSettingsHeader">
|
||||
<h2>Dropdown Options</h2>
|
||||
<p>Manage configurable dropdown values for work orders</p>
|
||||
</div>
|
||||
|
||||
<div className="dropdownSettingsLayout">
|
||||
<div className="dropdownSettingsSidebar">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat.key}
|
||||
className={`dropdownSettingsTab ${activeCategory === cat.key ? "active" : ""}`}
|
||||
onClick={() => setActiveCategory(cat.key)}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="dropdownSettingsContent">
|
||||
<div className="dropdownSettingsAddRow">
|
||||
{categoryConfig?.parent && (
|
||||
<select
|
||||
className="dropdownSettingsSelect"
|
||||
value={newParent}
|
||||
onChange={(e) => setNewParent(e.target.value)}
|
||||
>
|
||||
<option value="">Parent: All</option>
|
||||
{parentOptions.map((p) => (
|
||||
<option key={p.id} value={p.value}>{p.value}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<input
|
||||
className="dropdownSettingsInput"
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={`Add new ${categoryConfig?.label?.slice(0, -1) || "option"}...`}
|
||||
/>
|
||||
<button className="dropdownSettingsAddBtn" onClick={handleAdd} disabled={!newValue.trim()}>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="dropdownSettingsLoading">Loading...</div>
|
||||
) : options.length === 0 ? (
|
||||
<div className="dropdownSettingsEmpty">No options configured yet</div>
|
||||
) : (
|
||||
<div className="dropdownSettingsList">
|
||||
{options.map((item) => (
|
||||
<div key={item.id} className="dropdownSettingsItem">
|
||||
{editingId === item.id ? (
|
||||
<input
|
||||
className="dropdownSettingsEditInput"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleEditSave(item); if (e.key === "Escape") setEditingId(null); }}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div className="dropdownSettingsItemValue">
|
||||
<span>{item.value}</span>
|
||||
{item.parentValue && <span className="dropdownSettingsParentTag">{item.parentValue}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="dropdownSettingsItemActions">
|
||||
{editingId === item.id ? (
|
||||
<>
|
||||
<button className="dropdownSettingsBtn save" onClick={() => handleEditSave(item)}>Save</button>
|
||||
<button className="dropdownSettingsBtn cancel" onClick={() => setEditingId(null)}>Cancel</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="dropdownSettingsBtn edit" onClick={() => { setEditingId(item.id); setEditValue(item.value); }}>Edit</button>
|
||||
<button className="dropdownSettingsBtn delete" onClick={() => handleDelete(item.id)}>Delete</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,6 +32,13 @@ export const getWorkOrderById = async (id) => {
|
|||
status: wo.status ?? "Open",
|
||||
po: wo.po ?? "",
|
||||
tt: wo.tt ?? "",
|
||||
problem: wo.problem ?? "",
|
||||
trade: wo.trade ?? "",
|
||||
subTrade: wo.subTrade ?? "",
|
||||
vendorNTE: wo.vendorNTE != null ? String(wo.vendorNTE) : "",
|
||||
scheduledDate: wo.scheduledDate ? wo.scheduledDate.split("T")[0] : "",
|
||||
completedDate: wo.completedDate ? wo.completedDate.split("T")[0] : "",
|
||||
source: wo.source ?? "",
|
||||
comments: wo.comments ?? [],
|
||||
auditLog: wo.auditLog ?? [],
|
||||
};
|
||||
|
|
@ -64,6 +71,13 @@ export const editWorkOrder = async (workOrder) => {
|
|||
Priority: workOrder.priority || null,
|
||||
Status: workOrder.status,
|
||||
LocationId: workOrder.locationId ? parseInt(workOrder.locationId) : null,
|
||||
Problem: workOrder.problem || null,
|
||||
Trade: workOrder.trade || null,
|
||||
SubTrade: workOrder.subTrade || null,
|
||||
VendorNTE: workOrder.vendorNTE ? parseFloat(workOrder.vendorNTE) : null,
|
||||
ScheduledDate: workOrder.scheduledDate || null,
|
||||
CompletedDate: workOrder.completedDate || null,
|
||||
Source: workOrder.source || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating work order:", error);
|
||||
|
|
@ -89,6 +103,13 @@ export const fetchUsers = async () => {
|
|||
return (data || []).map((u) => ({ id: u.id ?? u.Id, name: u.name ?? u.Name ?? u.userName ?? "" }));
|
||||
};
|
||||
|
||||
export const fetchDropdownOptions = async (category, parent) => {
|
||||
const { get: apiGet } = await import("../../apiUtil");
|
||||
let url = `${API_URL}/DropdownOptions/ByCategory/${encodeURIComponent(category)}`;
|
||||
if (parent) url += `?parent=${encodeURIComponent(parent)}`;
|
||||
return (await apiGet(url)) || [];
|
||||
};
|
||||
|
||||
export const changeWorkOrderStatus = async (id, status) => {
|
||||
return post(`${API_URL}/WorkOrder/ChangeStatus?id=${id}&status=${encodeURIComponent(status)}`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -164,6 +164,10 @@
|
|||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.col-sm-3 {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.col-sm-4 {
|
||||
width: 33.33333333%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { getWorkOrderById, addWorkOrder, editWorkOrder } from "../api";
|
||||
import { getWorkOrderById, addWorkOrder, editWorkOrder, fetchDropdownOptions } from "../api";
|
||||
import { get } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
import "./WorkOrderFormPage.css";
|
||||
|
|
@ -20,21 +20,41 @@ export default function WorkOrderFormPage() {
|
|||
status: "Open",
|
||||
createdDate: new Date().toISOString().split("T")[0],
|
||||
dueDate: "",
|
||||
scheduledDate: "",
|
||||
completedDate: "",
|
||||
assignedTo: "",
|
||||
description: "",
|
||||
po: "",
|
||||
tt: "",
|
||||
problem: "",
|
||||
trade: "",
|
||||
subTrade: "",
|
||||
vendorNTE: "",
|
||||
source: "",
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [locations, setLocations] = useState([]);
|
||||
const [problems, setProblems] = useState([]);
|
||||
const [trades, setTrades] = useState([]);
|
||||
const [subTrades, setSubTrades] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
get(`${API_URL}/Contact/GetLocations`)
|
||||
.then((data) => setLocations(data || []))
|
||||
.catch(() => {});
|
||||
fetchDropdownOptions("Problem").then(setProblems).catch(() => {});
|
||||
fetchDropdownOptions("Trade").then(setTrades).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.trade) {
|
||||
fetchDropdownOptions("SubTrade", formData.trade).then(setSubTrades).catch(() => {});
|
||||
} else {
|
||||
setSubTrades([]);
|
||||
}
|
||||
}, [formData.trade]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && id) {
|
||||
const loadWorkOrder = async () => {
|
||||
|
|
@ -60,10 +80,11 @@ export default function WorkOrderFormPage() {
|
|||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
setFormData(prev => {
|
||||
const updated = { ...prev, [name]: value };
|
||||
if (name === "trade") updated.subTrade = "";
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async (e) => {
|
||||
|
|
@ -106,7 +127,6 @@ export default function WorkOrderFormPage() {
|
|||
|
||||
<div className="container">
|
||||
<form onSubmit={handleSave}>
|
||||
{/* Page Header */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h3 className="heading thin">
|
||||
|
|
@ -119,57 +139,29 @@ export default function WorkOrderFormPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Work Order Details Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Work Order Details</strong>
|
||||
</h5>
|
||||
<h5 className="heading text-uppercase"><strong>Work Order Details</strong></h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="workOrderNumber">Customer WO</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="workOrderNumber"
|
||||
name="workOrderNumber"
|
||||
value={formData.workOrderNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="Customer work order number"
|
||||
/>
|
||||
<input className="form-control" type="text" id="workOrderNumber" name="workOrderNumber" value={formData.workOrderNumber} onChange={handleChange} placeholder="Customer work order number" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="title">Title</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Work order title"
|
||||
required
|
||||
/>
|
||||
<input className="form-control" type="text" id="title" name="title" value={formData.title} onChange={handleChange} placeholder="Work order title" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="locationId">Location</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="locationId"
|
||||
name="locationId"
|
||||
value={formData.locationId}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<select className="form-control" id="locationId" name="locationId" value={formData.locationId} onChange={handleChange} required>
|
||||
<option value="">Select Location</option>
|
||||
{locations.map((loc) => (
|
||||
<option key={loc.id} value={loc.id}>{loc.name}</option>
|
||||
|
|
@ -179,73 +171,63 @@ export default function WorkOrderFormPage() {
|
|||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="assignedTo">Assigned To</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="assignedTo"
|
||||
name="assignedTo"
|
||||
value={formData.assignedTo}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">Select Employee</option>
|
||||
<option value="John Anderson">John Anderson</option>
|
||||
<option value="Sarah Martinez">Sarah Martinez</option>
|
||||
<option value="Mike Johnson">Mike Johnson</option>
|
||||
<label htmlFor="source">Source</label>
|
||||
<input className="form-control" type="text" id="source" name="source" value={formData.source} onChange={handleChange} placeholder="e.g., Amazon APM, Manual" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase"><strong>Classification</strong></h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="problem">Problem</label>
|
||||
<select className="form-control" id="problem" name="problem" value={formData.problem} onChange={handleChange}>
|
||||
<option value="">Select Problem</option>
|
||||
{problems.map((p) => (
|
||||
<option key={p.id} value={p.value}>{p.value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="trade">Trade</label>
|
||||
<select className="form-control" id="trade" name="trade" value={formData.trade} onChange={handleChange}>
|
||||
<option value="">Select Trade</option>
|
||||
{trades.map((t) => (
|
||||
<option key={t.id} value={t.value}>{t.value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="subTrade">Sub-Trade</label>
|
||||
<select className="form-control" id="subTrade" name="subTrade" value={formData.subTrade} onChange={handleChange} disabled={!formData.trade}>
|
||||
<option value="">{formData.trade ? "Select Sub-Trade" : "Select a Trade first"}</option>
|
||||
{subTrades.map((s) => (
|
||||
<option key={s.id} value={s.value}>{s.value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule & Priority Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Schedule & Priority</strong>
|
||||
</h5>
|
||||
<h5 className="heading text-uppercase"><strong>Schedule & Priority</strong></h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="createdDate">Created Date</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="date"
|
||||
id="createdDate"
|
||||
name="createdDate"
|
||||
value={formData.createdDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="dueDate">Due Date</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="date"
|
||||
id="dueDate"
|
||||
name="dueDate"
|
||||
value={formData.dueDate}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="priority">Priority</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="priority"
|
||||
name="priority"
|
||||
value={formData.priority}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<select className="form-control" id="priority" name="priority" value={formData.priority} onChange={handleChange} required>
|
||||
<option value="">Select Priority</option>
|
||||
<option value="Sev 1">Sev 1</option>
|
||||
<option value="Sev 2">Sev 2</option>
|
||||
|
|
@ -255,17 +237,10 @@ export default function WorkOrderFormPage() {
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="status">Status</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="status"
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<select className="form-control" id="status" name="status" value={formData.status} onChange={handleChange} required>
|
||||
<option value="Open">Open</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="On Hold">On Hold</option>
|
||||
|
|
@ -274,81 +249,86 @@ export default function WorkOrderFormPage() {
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="po">Customer PO</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="po"
|
||||
name="po"
|
||||
value={formData.po}
|
||||
onChange={handleChange}
|
||||
placeholder="Purchase order number"
|
||||
/>
|
||||
<label htmlFor="assignedTo">Assigned To</label>
|
||||
<input className="form-control" type="text" id="assignedTo" name="assignedTo" value={formData.assignedTo} onChange={handleChange} placeholder="Assigned to" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-3">
|
||||
<div className="form-group">
|
||||
<label htmlFor="tt">TT</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="tt"
|
||||
name="tt"
|
||||
value={formData.tt}
|
||||
onChange={handleChange}
|
||||
placeholder="Trouble ticket number"
|
||||
/>
|
||||
<label className="required-field" htmlFor="createdDate">Created</label>
|
||||
<input className="form-control" type="date" id="createdDate" name="createdDate" value={formData.createdDate} onChange={handleChange} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<div className="form-group">
|
||||
<label htmlFor="dueDate">Due Date</label>
|
||||
<input className="form-control" type="date" id="dueDate" name="dueDate" value={formData.dueDate} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<div className="form-group">
|
||||
<label htmlFor="scheduledDate">Scheduled</label>
|
||||
<input className="form-control" type="date" id="scheduledDate" name="scheduledDate" value={formData.scheduledDate} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-3">
|
||||
<div className="form-group">
|
||||
<label htmlFor="completedDate">Completed</label>
|
||||
<input className="form-control" type="date" id="completedDate" name="completedDate" value={formData.completedDate} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Description</strong>
|
||||
</h5>
|
||||
<h5 className="heading text-uppercase"><strong>Financial</strong></h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="vendorNTE">Vendor NTE</label>
|
||||
<input className="form-control" type="number" id="vendorNTE" name="vendorNTE" value={formData.vendorNTE} onChange={handleChange} placeholder="0.00" step="0.01" min="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="po">Customer PO</label>
|
||||
<input className="form-control" type="text" id="po" name="po" value={formData.po} onChange={handleChange} placeholder="Purchase order number" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label htmlFor="tt">TT</label>
|
||||
<input className="form-control" type="text" id="tt" name="tt" value={formData.tt} onChange={handleChange} placeholder="Trouble ticket number" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase"><strong>Description</strong></h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows="10"
|
||||
style={{ resize: "none" }}
|
||||
placeholder="Detailed description of the work to be performed"
|
||||
/>
|
||||
<textarea className="form-control" id="description" name="description" value={formData.description} onChange={handleChange} rows="10" style={{ resize: "none" }} placeholder="Detailed description of the work to be performed" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-default"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
<button type="button" className="btn btn-default" onClick={handleCancel} disabled={loading}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={loading}
|
||||
>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -120,6 +120,76 @@
|
|||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.woAssignBtn {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.woAssignBtn:hover {
|
||||
border-color: #ccc;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.woAssignMenu {
|
||||
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;
|
||||
}
|
||||
|
||||
.woAssignOption {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.woAssignOption:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.woAssignOption.active {
|
||||
font-weight: 600;
|
||||
background: #e6f0ff;
|
||||
}
|
||||
|
||||
.woSectionToggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.woSectionToggle:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.woSectionArrow {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
transition: transform 0.15s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.woSectionArrow.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.woFieldGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
|
@ -152,6 +222,11 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.woFieldPlaceholder {
|
||||
color: #ccc;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.woTimeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,82 @@ import React, { useState, useEffect, useRef } from "react";
|
|||
import ReactDOM from "react-dom";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { getWorkOrderById, addComment, changeWorkOrderStatus } from "../api";
|
||||
import { getWorkOrderById, addComment, changeWorkOrderStatus, fetchUsers, changeWorkOrderAssignment } from "../api";
|
||||
import "./WorkOrderViewPage.css";
|
||||
|
||||
function AssignDropdown({ value, onAssign }) {
|
||||
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 { 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);
|
||||
onAssign(userId, userName);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={btnRef} className="woAssignBtn" onClick={handleOpen}>
|
||||
{value || "Unassigned"} <span className="woStatusCaret">▾</span>
|
||||
</button>
|
||||
{open && ReactDOM.createPortal(
|
||||
<div ref={menuRef} className="woAssignMenu" style={{ top: menuPos.top, left: menuPos.left }}>
|
||||
<button className={`woAssignOption ${!value ? "active" : ""}`} onClick={() => handleSelect("", "")}>
|
||||
Unassigned
|
||||
</button>
|
||||
{users.map((u) => (
|
||||
<button key={u.id} className={`woAssignOption ${value === u.name ? "active" : ""}`} onClick={() => handleSelect(u.id, u.name)}>
|
||||
{u.name}
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapsibleSection({ title, defaultOpen = false, children }) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
return (
|
||||
<div className="woViewSection">
|
||||
<h4 className="woSectionToggle" onClick={() => setOpen(!open)}>
|
||||
<span className={`woSectionArrow ${open ? "open" : ""}`}>▸</span>
|
||||
{title}
|
||||
</h4>
|
||||
{open && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
const d = new Date(value.endsWith("Z") ? value : value + "Z");
|
||||
if (isNaN(d)) return value;
|
||||
|
|
@ -328,8 +401,49 @@ export default function WorkOrderViewPage() {
|
|||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Assigned To</span>
|
||||
<span className="woFieldValue">{wo.assignedTo || "—"}</span>
|
||||
<span className="woFieldValue">
|
||||
<AssignDropdown
|
||||
value={wo.assignedTo}
|
||||
onAssign={async (userId, userName) => {
|
||||
try {
|
||||
await changeWorkOrderAssignment(wo.id, userId);
|
||||
setWo((prev) => ({ ...prev, assignedTo: userName }));
|
||||
toast.success(`Assigned to ${userName || "Unassigned"}`);
|
||||
} catch { toast.error("Failed to update assignment"); }
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Source</span>
|
||||
<span className="woFieldValue">{wo.source || "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">TT</span>
|
||||
<span className="woFieldValue">{wo.tt || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleSection title="Classification">
|
||||
<div className="woFieldGrid">
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Problem</span>
|
||||
<span className="woFieldValue">{wo.problem || "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Trade</span>
|
||||
<span className="woFieldValue">{wo.trade || "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Sub-Trade</span>
|
||||
<span className="woFieldValue">{wo.subTrade || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title="Schedule">
|
||||
<div className="woFieldGrid">
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Created</span>
|
||||
<span className="woFieldValue">
|
||||
|
|
@ -341,20 +455,36 @@ export default function WorkOrderViewPage() {
|
|||
<span className="woFieldValue">{wo.dueDate || "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">PO</span>
|
||||
<span className="woFieldLabel">Scheduled</span>
|
||||
<span className="woFieldValue">{wo.scheduledDate || "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Completed</span>
|
||||
<span className="woFieldValue">{wo.completedDate || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<CollapsibleSection title="Financial">
|
||||
<div className="woFieldGrid">
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Vendor NTE</span>
|
||||
<span className="woFieldValue">{wo.vendorNTE ? `$${parseFloat(wo.vendorNTE).toFixed(2)}` : "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">Customer PO</span>
|
||||
<span className="woFieldValue">{wo.po || "—"}</span>
|
||||
</div>
|
||||
<div className="woField">
|
||||
<span className="woFieldLabel">TT</span>
|
||||
<span className="woFieldValue">{wo.tt || "—"}</span>
|
||||
<span className="woFieldLabel">Customer NTE</span>
|
||||
<span className="woFieldValue woFieldPlaceholder">Not connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<div className="woViewSection">
|
||||
<h4>Description</h4>
|
||||
<CollapsibleSection title="Description">
|
||||
<p className="woDescription">{wo.description || "No description provided."}</p>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<div className="woViewSection">
|
||||
<h4>Activity</h4>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue