diff --git a/src/App.js b/src/App.js
deleted file mode 100644
index 09ca4da0..00000000
--- a/src/App.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import React, { useState } from "react";
-import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
-
-import Sidebar from "./components/Sidebar";
-import Topbar from "./components/Topbar";
-import Dashboard from "./pages/Dashboard";
-
-// AUTH
-import LoginPage from "./pages/auth/LoginPage";
-import { isAuthenticated } from "./services/authService";
-
-// CONTACTS
-import ContactsList from "./pages/contacts/list/List";
-import ContactFormPage from "./pages/contacts/form/ContactFormPage";
-
-// ACCOUNTS
-import AccountList from "./pages/accounts/list/List";
-import AccountFormPage from "./pages/accounts/form/AccountFormPage";
-
-// EMPLOYEES
-import EmployeeList from "./pages/employees/list/List";
-import EmployeeFormPage from "./pages/employees/form/EmployeeFormPage";
-
-// LOCATIONS
-import LocationsList from "./pages/locations/list/List";
-import LocationFormPage from "./pages/locations/form/LocationFormPage";
-
-// WORK ORDERS
-import WorkOrdersList from "./pages/workorders/list/List";
-import WorkOrderViewPage from "./pages/workorders/view/WorkOrderViewPage";
-import WorkOrderFormPage from "./pages/workorders/form/WorkOrderFormPage";
-
-// FOLLOW UPS
-import FollowUpList from "./pages/followup/list/List";
-import FollowUpFormPage from "./pages/followup/form/FollowUpFormPage";
-
-// PM SCHEDULES
-import PMScheduleList from "./pages/PmSchedule/list/List";
-import PMScheduleFormPage from "./pages/PmSchedule/form/PmScheduleFormPage";
-
-// ASSETS
-import AssetsList from "./pages/assets/list/List";
-import AssetFormPage from "./pages/assets/form/AssetFormPage";
-
-// CALENDAR
-import CalendarPage from "./pages/calendar/list/CalendarPage";
-import EventFormPage from "./pages/calendar/form/EventFormPage";
-
-// VENDORS
-import VendorsList from "./pages/vendors/list/List";
-import VendorFormPage from "./pages/vendors/form/VendorFormPage";
-
-// UPLIFTS
-import UpliftQueuePage from "./pages/uplifts/UpliftQueuePage";
-
-// VENDOR POs
-import VendorPoList from "./pages/vendor-pos/list/List";
-import VendorPoViewPage from "./pages/vendor-pos/view/VendorPoViewPage";
-
-// SETTINGS
-import DropdownOptionsPage from "./pages/settings/DropdownOptionsPage";
-import TaskTemplatesPage from "./pages/settings/TaskTemplatesPage";
-
-// VENDOR PORTAL (public, token-authenticated, separate tree)
-import VendorPortalLayout from "./pages/vendor-portal/VendorPortalLayout";
-import VendorPortalDashboard from "./pages/vendor-portal/DashboardPage";
-import VendorPortalPos from "./pages/vendor-portal/PosPage";
-import VendorPortalDispatchDetail from "./pages/vendor-portal/DispatchDetailPage";
-
-// Protected Route Component
-function ProtectedRoute({ children }) {
- return isAuthenticated() ? children : ;
-}
-
-export default function App() {
- const [navCollapsed, setNavCollapsed] = useState(false);
-
- return (
-
-
- {/* Public Routes */}
- } />
-
- {/* Vendor Portal — public, token-authenticated via URL */}
- }>
- } />
- } />
- } />
- } />
-
-
- {/* Protected Routes */}
-
-
-
-
-
-
setNavCollapsed(v => !v)} />
-
-
-
- } />
- } />
-
- {/* CONTACTS */}
- } />
- } />
- } />
-
- {/* ACCOUNTS */}
- } />
- } />
- } />
-
- {/* EMPLOYEES */}
- } />
- } />
- } />
-
- {/* LOCATIONS */}
- } />
- } />
- } />
-
- {/* WORK ORDERS */}
- } />
- } />
- } />
- } />
-
- {/* FOLLOW UPS */}
- } />
- } />
- } />
-
- {/* PM SCHEDULES */}
- } />
- } />
- } />
-
- {/* ASSETS */}
- } />
- } />
- } />
-
- {/* VENDORS */}
- } />
- } />
- } />
-
- {/* CALENDAR */}
- } />
- } />
- } />
-
- {/* UPLIFTS */}
- } />
-
- {/* VENDOR POs */}
- } />
- } />
-
- {/* SETTINGS */}
- } />
- } />
-
-
-
-
-
- }
- />
-
-
- );
-}
diff --git a/src/App.test.js b/src/App.test.js
deleted file mode 100644
index 1f03afee..00000000
--- a/src/App.test.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/Site.Layout.js b/src/Site.Layout.js
deleted file mode 100644
index 3b0293d1..00000000
--- a/src/Site.Layout.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from "react";
-import { Container } from "@mui/material";
-import { ThemeProvider, createTheme } from "@mui/material/styles";
-import "./site.css";
-import TopNav from "./components/TopNav";
-import LeftNav from "./components/LeftNav";
-import Main from "./components/Main";
-
-const THEME = createTheme({
- typography: {
- fontFamily: `"Dosis", "Nunito", sans-serif`,
- fontSize: 14,
- fontWeightLight: 300,
- fontWeightRegular: 400,
- fontWeightMedium: 500,
- },
- palette: {
- header: {
- main: "#4D81B7",
- contrastText: "header background",
- },
- },
- flexBox: {
- display: "flex",
- },
-});
-
-const Layout = (props) => {
- const [open, setOpen] = React.useState(false);
-
- const handleDrawerOpen = () => {
- setOpen(true);
- };
-
- const handleDrawerClose = () => {
- setOpen(false);
- };
- return (
-
-
-
- {props.children}
-
-)};
-export default Layout;
diff --git a/src/apiUtil.js b/src/apiUtil.js
deleted file mode 100644
index f2604308..00000000
--- a/src/apiUtil.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import { API_ERROR_MESSAGE } from "./constants";
-import { getAccessToken } from "./tokenUtility";
-
-const getHeaders = () => ({
- Accept: "application/json",
- "Content-Type": "application/json",
- pragma: "no-cache",
- "cache-control": "no-cache",
- Authorization: getAccessToken(),
-});
-
-const getFormHeaders = () => ({
- accept: "application/json",
- pragma: "no-cache",
- "cache-control": "no-cache",
- Authorization: getAccessToken(),
-});
-
-export const get = (url) =>
- fetch(url, {
- headers: getHeaders(),
- method: "GET",
- }).then(handleHttpResponse);
-
-export const post = (url, payload) =>
- fetch(url, {
- headers: getHeaders(),
- method: "POST",
- body: JSON.stringify(payload),
- }).then(handleHttpResponse);
-
-export const postFormBody = (url, payload) => {
- const formData = new FormData();
- for (const [key, value] of Object.entries(payload)) {
- formData.append(key, value);
- }
- return fetch(url, {
- method: "POST",
- headers: getFormHeaders(),
- body: formData,
- }).then(handleHttpResponse);
-};
-
-export const put = (url, payload) =>
- fetch(url, {
- headers: getHeaders(),
- method: "PUT",
- body: JSON.stringify(payload),
- }).then(handleHttpResponse);
-
-export const putFormBody = (url, payload) => {
- const formData = new FormData();
- for (const [key, value] of Object.entries(payload)) {
- formData.append(key, value);
- }
- return fetch(url, {
- method: "PUT",
- headers: getFormHeaders(),
- body: formData,
- }).then(handleHttpResponse);
-};
-
-export const deleteItem = (url, payload) =>
- fetch(url, {
- headers: getHeaders(),
- method: "DELETE",
- body: JSON.stringify(payload),
- }).then(handleHttpResponse);
-
-const handleHttpResponse = (response) => {
- if (response.ok) return response.json();
- if (response.status === 401) {
- throw new Error("You are not authorized to access this page.");
- }
- throw new Error(API_ERROR_MESSAGE);
-};
diff --git a/src/app/slices/authSlice.js b/src/app/slices/authSlice.js
deleted file mode 100644
index 357c87d2..00000000
--- a/src/app/slices/authSlice.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-import { STORAGE_KEY_TOKEN } from '../../constants';
-
-// Helper functions
-const getUserFromStorage = () => {
- try {
- const result = window.localStorage.getItem(STORAGE_KEY_TOKEN);
- return result ? JSON.parse(result) : null;
- } catch (error) {
- console.error('Error reading user from storage:', error);
- return null;
- }
-};
-
-const isTokenValid = (token) => {
- if (!token) return false;
- try {
- const payload = JSON.parse(atob(token.split('.')[1]));
- return new Date(payload.exp * 1000) > new Date();
- } catch (e) {
- return false;
- }
-};
-
-const initialState = {
- user: getUserFromStorage(),
- isAuthenticated: (() => {
- const user = getUserFromStorage();
- return user?.token ? isTokenValid(user.token) : false;
- })(),
- loading: false,
- error: null,
-};
-
-const authSlice = createSlice({
- name: 'auth',
- initialState,
- reducers: {
- setUser: (state, action) => {
- state.user = action.payload;
- state.isAuthenticated = action.payload?.token ? isTokenValid(action.payload.token) : false;
- state.error = null;
-
- // Persist to localStorage
- if (action.payload?.token) {
- window.localStorage.setItem(STORAGE_KEY_TOKEN, JSON.stringify(action.payload));
- } else {
- window.localStorage.removeItem(STORAGE_KEY_TOKEN);
- }
- },
- logout: (state) => {
- state.user = null;
- state.isAuthenticated = false;
- state.error = null;
- window.localStorage.removeItem(STORAGE_KEY_TOKEN);
- },
- setLoading: (state, action) => {
- state.loading = action.payload;
- },
- setError: (state, action) => {
- state.error = action.payload;
- state.loading = false;
- },
- clearError: (state) => {
- state.error = null;
- },
- },
-});
-
-export const { setUser, logout, setLoading, setError, clearError } = authSlice.actions;
-
-// Selectors
-export const selectUser = (state) => state.auth.user;
-export const selectIsAuthenticated = (state) => state.auth.isAuthenticated;
-export const selectAuthLoading = (state) => state.auth.loading;
-export const selectAuthError = (state) => state.auth.error;
-export const selectToken = (state) => state.auth.user?.token;
-
-export default authSlice.reducer;
diff --git a/src/app/slices/uiSlice.js b/src/app/slices/uiSlice.js
deleted file mode 100644
index 990208e9..00000000
--- a/src/app/slices/uiSlice.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-import { THEME_LIGHT } from '../../constants';
-
-const initialState = {
- sidebarOpen: true,
- theme: THEME_LIGHT,
- notifications: [],
-};
-
-const uiSlice = createSlice({
- name: 'ui',
- initialState,
- reducers: {
- toggleSidebar: (state) => {
- state.sidebarOpen = !state.sidebarOpen;
- },
- setSidebarOpen: (state, action) => {
- state.sidebarOpen = action.payload;
- },
- setTheme: (state, action) => {
- state.theme = action.payload;
- },
- addNotification: (state, action) => {
- state.notifications.push({
- id: Date.now(),
- ...action.payload,
- });
- },
- removeNotification: (state, action) => {
- state.notifications = state.notifications.filter(
- (notification) => notification.id !== action.payload
- );
- },
- clearNotifications: (state) => {
- state.notifications = [];
- },
- },
-});
-
-export const {
- toggleSidebar,
- setSidebarOpen,
- setTheme,
- addNotification,
- removeNotification,
- clearNotifications,
-} = uiSlice.actions;
-
-// Selectors
-export const selectSidebarOpen = (state) => state.ui.sidebarOpen;
-export const selectTheme = (state) => state.ui.theme;
-export const selectNotifications = (state) => state.ui.notifications;
-
-export default uiSlice.reducer;
diff --git a/src/app/store.js b/src/app/store.js
deleted file mode 100644
index 0845ea6d..00000000
--- a/src/app/store.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { configureStore } from '@reduxjs/toolkit';
-import authReducer from './slices/authSlice';
-import uiReducer from './slices/uiSlice';
-
-export const store = configureStore({
- reducer: {
- auth: authReducer,
- ui: uiReducer,
- },
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({
- serializableCheck: {
- // Ignore these action types
- ignoredActions: ['auth/setUser'],
- },
- }),
-});
-
-export default store;
diff --git a/src/components/ActionBar.css b/src/components/ActionBar.css
deleted file mode 100644
index b373120f..00000000
--- a/src/components/ActionBar.css
+++ /dev/null
@@ -1,122 +0,0 @@
-/* Action Bar - Top navigation bar above search controls */
-.actionBar {
- background-color: #f5f5f5;
- border-bottom: 1px solid #ddd;
- padding: 0;
- margin-bottom: 0;
- position: relative;
- z-index: 10;
-}
-
-.actionBarNav {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- align-items: center;
- gap: 0;
-}
-
-.actionBarNav > li {
- position: relative;
- display: inline-block;
-}
-
-.actionBarLink {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 10px 15px;
- color: #333;
- text-decoration: none;
- transition: background-color 0.2s;
- white-space: nowrap;
-}
-
-.actionBarLink:hover {
- background-color: #e0e0e0;
- color: #000;
- text-decoration: none;
-}
-
-.actionBarLink i {
- font-size: 14px;
-}
-
-.actionBarText {
- font-size: 13px;
-}
-
-/* Dropdown */
-.actionBarDropdown {
- position: relative;
- z-index: 100;
-}
-
-.actionBarDropdownMenu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1050;
- display: block;
- min-width: 180px;
- padding: 5px 0;
- margin: 0;
- list-style: none;
- background-color: #fff;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-}
-
-.actionBarDropdownMenu li {
- display: block;
-}
-
-.actionBarDropdownMenu li a {
- display: block;
- padding: 8px 20px;
- color: #333;
- text-decoration: none;
- font-size: 13px;
- white-space: nowrap;
-}
-
-.actionBarDropdownMenu li a:hover {
- background-color: #f5f5f5;
- color: #000;
-}
-
-.actionBarDropdownMenu li.divider {
- height: 1px;
- margin: 5px 0;
- overflow: hidden;
- background-color: #e5e5e5;
-}
-
-.countLabel {
- color: #999;
- font-size: 12px;
-}
-
-.pull-right {
- float: right;
- margin-left: 10px;
-}
-
-/* Hide text on small screens */
-@media (max-width: 768px) {
- .actionBarText {
- display: none;
- }
-}
-
-/* Close dropdowns when clicking outside */
-.actionBarDropdown.open .actionBarDropdownMenu {
- display: block;
-}
-
-.actionBarDropdown:not(.open) .actionBarDropdownMenu {
- display: none;
-}
-
diff --git a/src/components/ActionBar.js b/src/components/ActionBar.js
deleted file mode 100644
index 92a27d3e..00000000
--- a/src/components/ActionBar.js
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, { useState } from "react";
-import "./ActionBar.css";
-
-export default function ActionBar({
- moduleName = "Items",
- moduleIcon = "fa-list",
- onNew,
- onBulkEdit,
- onImportExport,
- onSearch,
- selectedCount = 0
-}) {
- const [workspaceOpen, setWorkspaceOpen] = useState(false);
- const [moduleOpen, setModuleOpen] = useState(false);
- const [searchOpen, setSearchOpen] = useState(false);
- const [selectedWorkspace, setSelectedWorkspace] = useState("All");
-
- const handleWorkspaceSelect = (workspace) => {
- setSelectedWorkspace(workspace);
- setWorkspaceOpen(false);
- };
-
- const toggleWorkspace = (e) => {
- e.preventDefault();
- e.stopPropagation();
- setWorkspaceOpen(!workspaceOpen);
- setModuleOpen(false);
- setSearchOpen(false);
- };
-
- const toggleModule = (e) => {
- e.preventDefault();
- e.stopPropagation();
- setModuleOpen(!moduleOpen);
- setWorkspaceOpen(false);
- setSearchOpen(false);
- };
-
- const toggleSearch = (e) => {
- e.preventDefault();
- e.stopPropagation();
- setSearchOpen(!searchOpen);
- setWorkspaceOpen(false);
- setModuleOpen(false);
- };
-
- // Close dropdowns when clicking outside
- React.useEffect(() => {
- const handleClickOutside = () => {
- setWorkspaceOpen(false);
- setModuleOpen(false);
- setSearchOpen(false);
- };
-
- if (workspaceOpen || moduleOpen || searchOpen) {
- document.addEventListener("click", handleClickOutside);
- return () => document.removeEventListener("click", handleClickOutside);
- }
- }, [workspaceOpen, moduleOpen, searchOpen]);
-
- return (
-
-
- {/* Settings/Filter Button */}
- -
-
-
-
-
-
- {/* Workspace Selector */}
- -
-
-
- {selectedWorkspace}
-
-
- {workspaceOpen && (
-
- )}
-
-
- {/* Module Actions (New, Edit, Import/Export) */}
- -
-
-
- {moduleName}
-
-
- {moduleOpen && (
-
- )}
-
-
- {/* Search & Report */}
- -
-
-
- Search
-
-
- {searchOpen && (
-
- )}
-
-
-
- );
-}
-
diff --git a/src/components/DeleteModal.js b/src/components/DeleteModal.js
deleted file mode 100644
index 8e141e33..00000000
--- a/src/components/DeleteModal.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import React from "react";
-import { toast } from "react-toastify";
-
-export default function DeleteModal({
- isOpen,
- onClose,
- onConfirm,
- title = "Delete Confirmation",
- message = "Are you sure you want to delete this item?",
- loading = false
-}) {
- if (!isOpen) return null;
-
- return (
-
-
-
-
{title}
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-const styles = {
- overlay: {
- position: "fixed",
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- backgroundColor: "rgba(0, 0, 0, 0.5)",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- zIndex: 1000,
- },
- modal: {
- backgroundColor: "#fff",
- borderRadius: "8px",
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
- maxWidth: "400px",
- width: "90%",
- overflow: "hidden",
- },
- header: {
- backgroundColor: "#f8f9fa",
- padding: "16px 20px",
- borderBottom: "1px solid #ddd",
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- },
- title: {
- margin: 0,
- fontSize: "18px",
- fontWeight: 600,
- color: "#333",
- },
- closeBtn: {
- background: "none",
- border: "none",
- fontSize: "24px",
- cursor: "pointer",
- color: "#666",
- padding: "0",
- width: "32px",
- height: "32px",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- },
- body: {
- padding: "20px",
- },
- message: {
- margin: 0,
- fontSize: "14px",
- color: "#666",
- lineHeight: "1.5",
- },
- actions: {
- display: "flex",
- gap: "10px",
- padding: "16px 20px",
- borderTop: "1px solid #ddd",
- backgroundColor: "#f8f9fa",
- },
- cancelBtn: {
- flex: 1,
- padding: "8px 16px",
- fontSize: "14px",
- fontWeight: 500,
- border: "1px solid #ddd",
- borderRadius: "4px",
- backgroundColor: "#fff",
- color: "#333",
- cursor: "pointer",
- transition: "all 0.2s",
- },
- deleteBtn: {
- flex: 1,
- padding: "8px 16px",
- fontSize: "14px",
- fontWeight: 500,
- border: "none",
- borderRadius: "4px",
- backgroundColor: "#dc3545",
- color: "#fff",
- cursor: "pointer",
- transition: "all 0.2s",
- },
-};
diff --git a/src/components/SharedTable.js b/src/components/SharedTable.js
deleted file mode 100644
index 594949f8..00000000
--- a/src/components/SharedTable.js
+++ /dev/null
@@ -1,284 +0,0 @@
-import React, { useState, useCallback, useRef, useEffect } from "react";
-import ReactDOM from "react-dom";
-
-function ColumnConfigPanel({ columns, columnOrder, hiddenColumns, onReorder, onToggle, onClose }) {
- const btnRef = useRef(null);
- const panelRef = useRef(null);
- const [open, setOpen] = useState(false);
- const [menuPos, setMenuPos] = useState({ top: 0, right: 0 });
- const [dragIdx, setDragIdx] = useState(null);
-
- useEffect(() => {
- const handleClick = (e) => {
- if (btnRef.current?.contains(e.target)) return;
- if (panelRef.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, right: window.innerWidth - rect.right });
- }
- setOpen(!open);
- };
-
- const handleDragStart = (idx) => setDragIdx(idx);
- const handleDragOver = (e) => e.preventDefault();
- const handleDrop = (targetIdx) => {
- if (dragIdx === null || dragIdx === targetIdx) return;
- const newOrder = [...columnOrder];
- const [moved] = newOrder.splice(dragIdx, 1);
- newOrder.splice(targetIdx, 0, moved);
- onReorder(newOrder);
- setDragIdx(null);
- };
-
- const ordered = columnOrder.map((key) => columns.find((c) => c.key === key)).filter(Boolean);
-
- return (
- <>
-
- {open && ReactDOM.createPortal(
-
-
Columns
-
- {ordered.map((col, idx) => (
-
handleDragStart(idx)}
- onDragOver={handleDragOver}
- onDrop={() => handleDrop(idx)}
- >
- ⠿
-
-
- ))}
-
-
,
- document.body
- )}
- >
- );
-}
-
-const SharedTable = ({
- rows = [],
- columns = [],
- onEdit,
- onDelete,
- page,
- setPage,
- pageSize,
- setPageSize,
- totalCount,
- loading,
- error,
- sortBy,
- setSortBy,
- sortDir,
- setSortDir
-}) => {
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
- const [colWidths, setColWidths] = useState(null);
- const [columnOrder, setColumnOrder] = useState(() => columns.map((c) => c.key));
- const [hiddenColumns, setHiddenColumns] = useState([]);
- const gridRef = useRef(null);
-
- useEffect(() => {
- setColumnOrder((prev) => {
- const newKeys = columns.map((c) => c.key);
- const existing = prev.filter((k) => newKeys.includes(k));
- const added = newKeys.filter((k) => !prev.includes(k));
- return [...existing, ...added];
- });
- }, [columns]);
-
- const visibleColumns = columnOrder
- .map((key) => columns.find((c) => c.key === key))
- .filter((col) => col && !hiddenColumns.includes(col.key));
-
- const handleToggleColumn = (key) => {
- setHiddenColumns((prev) =>
- prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key]
- );
- setColWidths(null);
- };
-
- const snapshotWidths = useCallback(() => {
- if (!gridRef.current) return null;
- const headerCells = gridRef.current.querySelectorAll("[data-col]");
- const widths = {};
- headerCells.forEach((cell) => {
- widths[cell.dataset.col] = cell.getBoundingClientRect().width;
- });
- return widths;
- }, []);
-
- const handleMouseDown = useCallback((e, colKey) => {
- e.preventDefault();
- const startX = e.clientX;
-
- const currentWidths = snapshotWidths();
- if (!currentWidths) return;
- setColWidths(currentWidths);
-
- const colIndex = visibleColumns.findIndex((c) => c.key === colKey);
- const nextColKey = colIndex < visibleColumns.length - 1 ? visibleColumns[colIndex + 1].key : null;
- const startWidth = currentWidths[colKey] || 100;
- const nextStartWidth = nextColKey ? (currentWidths[nextColKey] || 100) : 0;
-
- const onMove = (moveEvent) => {
- let diff = moveEvent.clientX - startX;
- const minCol = 50;
- if (startWidth + diff < minCol) diff = minCol - startWidth;
- if (nextColKey && nextStartWidth - diff < minCol) diff = nextStartWidth - minCol;
- setColWidths((prev) => {
- const updated = { ...prev, [colKey]: startWidth + diff };
- if (nextColKey) updated[nextColKey] = nextStartWidth - diff;
- return updated;
- });
- };
-
- const onUp = () => {
- document.removeEventListener("mousemove", onMove);
- document.removeEventListener("mouseup", onUp);
- document.body.style.cursor = "";
- document.body.style.userSelect = "";
- };
-
- document.body.style.cursor = "col-resize";
- document.body.style.userSelect = "none";
- document.addEventListener("mousemove", onMove);
- document.addEventListener("mouseup", onUp);
- }, [snapshotWidths, visibleColumns]);
-
- const handleSort = (key) => {
- if (!setSortBy || !setSortDir) return;
- if (sortBy === key) {
- setSortDir(sortDir === "asc" ? "desc" : "asc");
- } else {
- setSortBy(key);
- setSortDir("asc");
- }
- if (setPage) setPage(1);
- };
-
- if (loading) return Loading...
;
- if (error) return {error}
;
-
- const gridTemplate = colWidths
- ? ["40px", ...visibleColumns.map((col) => `${colWidths[col.key] || 100}px`), "80px"].join(" ")
- : ["40px", ...visibleColumns.map(() => "1fr"), "80px"].join(" ");
-
- const paginationBar = (
-
-
-
Show
-
- {[12, 24, 48, 96].map((size) => (
-
- ))}
-
-
per page
-
-
-
- {Math.min((page - 1) * pageSize + 1, totalCount)}–{Math.min(page * pageSize, totalCount)} of {totalCount}
-
-
-
-
- {page}
-
-
-
-
{ setColumnOrder(order); setColWidths(null); }}
- onToggle={handleToggleColumn}
- />
-
-
- );
-
- return (
-
- {paginationBar}
-
-
-
-
-
- {visibleColumns.map((col) => (
-
handleSort(col.key)}
- >
- {col.label}
- {sortBy === col.key && {sortDir === "asc" ? " ▲" : " ▼"}}
- { e.stopPropagation(); handleMouseDown(e, col.key); }}
- />
-
- ))}
-
Actions
-
- {rows.map((row, idx) => (
-
-
-
-
- {visibleColumns.map((col) => (
-
- {col.render ? col.render(row[col.key], row) : row[col.key]}
-
- ))}
-
-
- {onEdit && (
-
- )}
- {onDelete && (
-
- )}
-
-
-
- ))}
-
-
-
- );
-};
-
-export default SharedTable;
diff --git a/src/components/Sidebar.js b/src/components/Sidebar.js
deleted file mode 100644
index 0225b93a..00000000
--- a/src/components/Sidebar.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import React from "react";
-import { NavLink } from "react-router-dom";
-import {
- FaTachometerAlt,
- FaCalendarAlt,
- FaFileAlt,
- FaUserTie,
- FaMoneyCheckAlt,
- FaAddressBook,
- FaChartBar,
- FaTools,
- FaBell,
- FaClipboardList,
- FaCubes,
- FaMapMarkerAlt,
- FaCog,
- FaTruck,
- FaDollarSign,
- FaFileInvoiceDollar,
-} from "react-icons/fa";
-
-const navGroups = [
- {
- title: "Overview",
- items: [
- { label: "Dashboard", icon: , path: "/" },
- { label: "Calendar", icon: , path: "/calendar" },
- { label: "Reports", icon: , path: "/reports" },
- ],
- },
- {
- title: "Operations",
- items: [
- { label: "Work Orders", icon: , path: "/workorders" },
- { label: "Uplifts", icon: , path: "/uplifts" },
- { label: "Follow Ups", icon: , path: "/followups" },
- { label: "PM Schedules", icon: , path: "/pmschedules" },
- ],
- },
- {
- title: "Vendors",
- items: [
- { label: "Vendors", icon: , path: "/vendors" },
- { label: "Vendor POs", icon: , path: "/vendor-pos" },
- ],
- },
- {
- title: "Customers & Sites",
- items: [
- { label: "Accounts", icon: , path: "/accounts" },
- { label: "Contacts", icon: , path: "/contacts" },
- { label: "Locations", icon: , path: "/locations" },
- { label: "Assets", icon: , path: "/assets" },
- ],
- },
- {
- title: "Internal",
- items: [
- { label: "Employees", icon: , path: "/employees" },
- { label: "Documents", icon: , path: "/documents" },
- ],
- },
- {
- title: "System",
- items: [
- { label: "Settings", icon: , path: "/settings/dropdowns" },
- ],
- },
-];
-
-export default function Sidebar() {
- return (
-
- );
-}
diff --git a/src/components/SignatureCapture.css b/src/components/SignatureCapture.css
deleted file mode 100644
index 15f281f2..00000000
--- a/src/components/SignatureCapture.css
+++ /dev/null
@@ -1,125 +0,0 @@
-.sigOverlay {
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.4);
- z-index: 100000;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.sigModal {
- background: #fff;
- border-radius: 8px;
- width: 520px;
- box-shadow: 0 12px 40px rgba(0,0,0,0.2);
-}
-
-.sigHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 14px 20px;
- border-bottom: 1px solid #eee;
-}
-
-.sigHeader h3 {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
-}
-
-.sigBody {
- padding: 20px;
-}
-
-.sigModeTabs {
- display: flex;
- gap: 0;
- margin-bottom: 16px;
- border-bottom: 2px solid #e5e5e5;
-}
-
-.sigNameField {
- margin-bottom: 12px;
-}
-
-.sigNameField label {
- display: block;
- font-size: 12px;
- font-weight: 600;
- color: #666;
- margin-bottom: 4px;
-}
-
-.sigModeTab {
- padding: 6px 16px;
- border: none;
- background: none;
- font-size: 13px;
- font-weight: 600;
- color: #999;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
-}
-
-.sigModeTab.active {
- color: #007bff;
- border-bottom-color: #007bff;
-}
-
-.sigCanvasWrap {
- position: relative;
-}
-
-.sigCanvas {
- border: 1px solid #ddd;
- border-radius: 4px;
- cursor: crosshair;
- display: block;
- width: 100%;
- touch-action: none;
-}
-
-.sigClearBtn {
- position: absolute;
- top: 8px;
- right: 8px;
- padding: 2px 10px;
- font-size: 11px;
- border: 1px solid #ddd;
- background: #fff;
- border-radius: 3px;
- cursor: pointer;
- color: #666;
-}
-
-.sigTypeWrap {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.sigTypeInput {
- font-size: 16px;
-}
-
-.sigTypePreview {
- padding: 20px;
- text-align: center;
- font-family: "Brush Script MT", "Segoe Script", cursive;
- font-size: 36px;
- color: #333;
- border: 1px solid #eee;
- border-radius: 4px;
- background: #fafafa;
-}
-
-.sigFooter {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding: 14px 20px;
- border-top: 1px solid #eee;
-}
diff --git a/src/components/SignatureCapture.js b/src/components/SignatureCapture.js
deleted file mode 100644
index dc9e5ac0..00000000
--- a/src/components/SignatureCapture.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import React, { useState, useRef, useEffect } from "react";
-import ReactDOM from "react-dom";
-
-export default function SignatureCapture({ onSave, onClose, title }) {
- const [mode, setMode] = useState("draw");
- const [typedName, setTypedName] = useState("");
- const [signerName, setSignerName] = useState("");
- const canvasRef = useRef(null);
- const isDrawing = useRef(false);
-
- useEffect(() => {
- if (mode === "draw" && canvasRef.current) {
- const canvas = canvasRef.current;
- const ctx = canvas.getContext("2d");
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.strokeStyle = "#333";
- ctx.lineWidth = 2;
- ctx.lineCap = "round";
- }
- }, [mode]);
-
- const getPos = (e) => {
- const rect = canvasRef.current.getBoundingClientRect();
- const touch = e.touches ? e.touches[0] : e;
- return { x: touch.clientX - rect.left, y: touch.clientY - rect.top };
- };
-
- const startDraw = (e) => {
- e.preventDefault();
- isDrawing.current = true;
- const ctx = canvasRef.current.getContext("2d");
- const pos = getPos(e);
- ctx.beginPath();
- ctx.moveTo(pos.x, pos.y);
- };
-
- const draw = (e) => {
- e.preventDefault();
- if (!isDrawing.current) return;
- const ctx = canvasRef.current.getContext("2d");
- const pos = getPos(e);
- ctx.lineTo(pos.x, pos.y);
- ctx.stroke();
- };
-
- const stopDraw = () => { isDrawing.current = false; };
-
- const clearCanvas = () => {
- const ctx = canvasRef.current.getContext("2d");
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
- };
-
- const handleSave = () => {
- if (!signerName.trim()) return;
- if (mode === "draw") {
- const dataUrl = canvasRef.current.toDataURL("image/png");
- onSave(dataUrl, "drawn", signerName.trim());
- } else {
- onSave(typedName, "typed", signerName.trim());
- }
- };
-
- return ReactDOM.createPortal(
-
-
e.stopPropagation()}>
-
-
{title || "Capture Signature"}
-
-
-
-
-
- setSignerName(e.target.value)}
- placeholder="Enter signer's full name"
- />
-
-
-
-
-
- {mode === "draw" ? (
-
-
-
-
- ) : (
-
-
setTypedName(e.target.value)}
- placeholder="Type your name"
- />
- {typedName &&
{typedName}
}
-
- )}
-
-
-
-
-
-
-
,
- document.body
- );
-}
diff --git a/src/components/Topbar.js b/src/components/Topbar.js
deleted file mode 100644
index 7fa3fbaf..00000000
--- a/src/components/Topbar.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import React, { useState, useEffect, useRef } from "react";
-import ReactDOM from "react-dom";
-import { useNavigate } from "react-router-dom";
-import { useAuth } from "../hooks/useAuth";
-
-function NotificationDropdown() {
- const [open, setOpen] = useState(false);
- const btnRef = useRef(null);
- const menuRef = useRef(null);
- const [menuPos, setMenuPos] = useState({ top: 0, right: 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 = () => {
- if (btnRef.current) {
- const rect = btnRef.current.getBoundingClientRect();
- setMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right });
- }
- setOpen(!open);
- };
-
- return (
- <>
-
- {open && ReactDOM.createPortal(
-
-
Notifications
-
-
- No new notifications
-
-
,
- document.body
- )}
- >
- );
-}
-
-function UserDropdown({ currentUser, onLogout, onNavigate }) {
- const [open, setOpen] = useState(false);
- const btnRef = useRef(null);
- const menuRef = useRef(null);
- const [menuPos, setMenuPos] = useState({ top: 0, right: 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 = () => {
- if (btnRef.current) {
- const rect = btnRef.current.getBoundingClientRect();
- setMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right });
- }
- setOpen(!open);
- };
-
- const handleItemClick = (action) => {
- setOpen(false);
- action();
- };
-
- return (
- <>
-
- {open && ReactDOM.createPortal(
-
-
-
-
-
{currentUser?.name || "User"}
-
{currentUser?.email || ""}
-
-
-
-
-
-
,
- document.body
- )}
- >
- );
-}
-
-export default function Topbar({ onToggleNav }) {
- const navigate = useNavigate();
- const { user, logout: performLogout } = useAuth();
-
- const handleLogout = () => {
- performLogout(); // This now uses Redux and automatically navigates
- };
-
- return (
-
- );
-}
diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js
deleted file mode 100644
index fed9bf21..00000000
--- a/src/constants/actionTypes.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// Redux Action Types
-
-// Auth Actions
-export const AUTH_SET_USER = 'auth/setUser';
-export const AUTH_LOGOUT = 'auth/logout';
-export const AUTH_SET_LOADING = 'auth/setLoading';
-export const AUTH_SET_ERROR = 'auth/setError';
-export const AUTH_CLEAR_ERROR = 'auth/clearError';
-
-// UI Actions
-export const UI_TOGGLE_SIDEBAR = 'ui/toggleSidebar';
-export const UI_SET_SIDEBAR_OPEN = 'ui/setSidebarOpen';
-export const UI_SET_THEME = 'ui/setTheme';
-export const UI_ADD_NOTIFICATION = 'ui/addNotification';
-export const UI_REMOVE_NOTIFICATION = 'ui/removeNotification';
-export const UI_CLEAR_NOTIFICATIONS = 'ui/clearNotifications';
diff --git a/src/constants/index.js b/src/constants/index.js
deleted file mode 100644
index b44ac009..00000000
--- a/src/constants/index.js
+++ /dev/null
@@ -1,87 +0,0 @@
-// Main constants file - exports all constants
-
-// API Constants
-export const ADD = "add";
-export const EDIT = "edit";
-
-export const API_ERROR_MESSAGE = "Sorry, something went wrong. Please try again later.";
-export const API_SUCCESS_MESSAGE = "Your changes have been successfully saved.";
-export const API_DELETE_SUCCESS_MESSAGE = "Item deleted successfully";
-
-export const API_URL = process.env.REACT_APP_API_URL || "/api";
-
-// Cache Times (in milliseconds)
-export const CACHE_TIME_SHORT = 2 * 60 * 1000; // 2 minutes
-export const CACHE_TIME_MEDIUM = 5 * 60 * 1000; // 5 minutes
-export const CACHE_TIME_LONG = 10 * 60 * 1000; // 10 minutes
-export const CACHE_TIME_VERY_LONG = 30 * 60 * 1000; // 30 minutes
-
-// Stale Times (in milliseconds)
-export const STALE_TIME_SHORT = 1 * 60 * 1000; // 1 minute
-export const STALE_TIME_MEDIUM = 5 * 60 * 1000; // 5 minutes
-export const STALE_TIME_LONG = 10 * 60 * 1000; // 10 minutes
-export const STALE_TIME_VERY_LONG = 30 * 60 * 1000; // 30 minutes
-
-// Pagination
-export const DEFAULT_PAGE_SIZE = 12;
-export const DEFAULT_PAGE = 1;
-
-// Debounce Times
-export const DEBOUNCE_SEARCH = 600; // 600ms for search inputs
-export const DEBOUNCE_RESIZE = 300; // 300ms for window resize
-export const DEBOUNCE_INPUT = 400; // 400ms for general inputs
-
-// Local Storage Keys
-export const STORAGE_KEY_TOKEN = 'token';
-export const STORAGE_KEY_THEME = 'theme';
-export const STORAGE_KEY_SIDEBAR = 'sidebarOpen';
-
-// Themes
-export const THEME_LIGHT = 'light';
-export const THEME_DARK = 'dark';
-
-// View Modes
-export const VIEW_MODE_LIST = 'list';
-export const VIEW_MODE_CARD = 'card';
-export const VIEW_MODE_GRID = 'grid';
-
-// Status Types
-export const STATUS_OPEN = 'Open';
-export const STATUS_CLOSED = 'Closed';
-export const STATUS_PENDING = 'Pending';
-export const STATUS_COMPLETED = 'Completed';
-export const STATUS_CANCELLED = 'Cancelled';
-export const STATUS_IN_PROGRESS = 'In Progress';
-
-// Priority Levels
-export const PRIORITY_LOW = 'Low';
-export const PRIORITY_MEDIUM = 'Medium';
-export const PRIORITY_HIGH = 'High';
-export const PRIORITY_URGENT = 'Urgent';
-
-// Form Modes
-export const FORM_MODE_CREATE = 'create';
-export const FORM_MODE_EDIT = 'edit';
-export const FORM_MODE_VIEW = 'view';
-
-// Helper function for form text
-export const form_text_builder = (mode) => {
- let PRIMARY_TEXT = "";
- let SECONDARY_TEXT = "";
- let BUTTON_TEXT = "";
-
- if (mode === ADD) {
- PRIMARY_TEXT = "Add an Item";
- SECONDARY_TEXT = "Add your new item below";
- BUTTON_TEXT = "Add Item";
- } else {
- PRIMARY_TEXT = "Edit an Item";
- SECONDARY_TEXT = "Edit your item below";
- BUTTON_TEXT = "Save Item";
- }
- return { PRIMARY_TEXT, SECONDARY_TEXT, BUTTON_TEXT };
-};
-
-// Re-export all action types and query keys
-export * from './actionTypes';
-export * from './queryKeys';
diff --git a/src/constants/queryKeys.js b/src/constants/queryKeys.js
deleted file mode 100644
index 9619efb2..00000000
--- a/src/constants/queryKeys.js
+++ /dev/null
@@ -1,82 +0,0 @@
-// React Query Keys - Centralized query key management
-
-// Work Orders
-export const WORK_ORDERS_LIST = 'workOrdersList';
-export const WORK_ORDERS_DETAIL = 'workOrdersDetail';
-export const WORK_ORDERS_BY_LOCATION = 'workOrdersByLocation';
-export const WORK_ORDERS_BY_ACCOUNT = 'workOrdersByAccount';
-
-// PM Schedules
-export const PM_SCHEDULES_LIST = 'pmSchedulesList';
-export const PM_SCHEDULES_DETAIL = 'pmSchedulesDetail';
-export const PM_SCHEDULES_BY_ASSET = 'pmSchedulesByAsset';
-export const PM_SCHEDULES_BY_LOCATION = 'pmSchedulesByLocation';
-
-// Follow Ups
-export const FOLLOW_UPS_LIST = 'followUpsList';
-export const FOLLOW_UPS_DETAIL = 'followUpsDetail';
-export const FOLLOW_UPS_BY_STATUS = 'followUpsByStatus';
-export const FOLLOW_UPS_BY_WORK_ORDER = 'followUpsByWorkOrder';
-
-// Employees
-export const EMPLOYEES_LIST = 'employeesList';
-export const EMPLOYEES_DETAIL = 'employeesDetail';
-export const EMPLOYEES_DROPDOWN = 'employeesDropdown';
-export const EMPLOYEES_BY_DEPARTMENT = 'employeesByDepartment';
-
-// Contacts
-export const CONTACTS_LIST = 'contactsList';
-export const CONTACTS_DETAIL = 'contactsDetail';
-export const CONTACTS_DROPDOWN = 'contactsDropdown';
-export const CONTACTS_BY_LOCATION = 'contactsByLocation';
-export const CONTACTS_BY_ACCOUNT = 'contactsByAccount';
-
-// Locations
-export const LOCATIONS_LIST = 'locationsList';
-export const LOCATIONS_DETAIL = 'locationsDetail';
-export const LOCATIONS_DROPDOWN = 'locationsDropdown';
-export const LOCATIONS_BY_ACCOUNT = 'locationsByAccount';
-
-// Accounts
-export const ACCOUNTS_LIST = 'accountsList';
-export const ACCOUNTS_DETAIL = 'accountsDetail';
-export const ACCOUNTS_DROPDOWN = 'accountsDropdown';
-
-// Assets
-export const ASSETS_LIST = 'assetsList';
-export const ASSETS_DETAIL = 'assetsDetail';
-export const ASSETS_BY_LOCATION = 'assetsByLocation';
-export const ASSETS_BY_ACCOUNT = 'assetsByAccount';
-
-// Vendors
-export const VENDORS_LIST = 'vendorsList';
-export const VENDORS_DETAIL = 'vendorsDetail';
-export const VENDORS_DROPDOWN = 'vendorsDropdown';
-export const VENDORS_BY_TRADE = 'vendorsByTrade';
-
-// Dashboard
-export const DASHBOARD_STATS = 'dashboardStats';
-export const DASHBOARD_RECENT_WORK_ORDERS = 'dashboardRecentWorkOrders';
-export const DASHBOARD_OVERDUE_WORK_ORDERS = 'dashboardOverdueWorkOrders';
-
-// Dropdowns
-export const DROPDOWN_OPTIONS_BY_CATEGORY = 'dropdownOptionsByCategory';
-export const DROPDOWN_PARENTS = 'dropdownParents';
-
-// Dispatches
-export const DISPATCHES_LIST = 'dispatchesList';
-export const DISPATCHES_DETAIL = 'dispatchesDetail';
-export const DISPATCHES_BY_VENDOR = 'dispatchesByVendor';
-
-// Task Templates
-export const TASK_TEMPLATES_LIST = 'taskTemplatesList';
-export const TASK_TEMPLATES_DETAIL = 'taskTemplatesDetail';
-
-// Uplifts
-export const UPLIFTS_LIST = 'upliftsList';
-export const UPLIFTS_BY_DISPATCH = 'upliftsByDispatch';
-
-// Calendar Events
-export const CALENDAR_EVENTS_LIST = 'calendarEventsList';
-export const CALENDAR_EVENTS_DETAIL = 'calendarEventsDetail';
-export const CALENDAR_EVENTS_BY_DATE_RANGE = 'calendarEventsByDateRange';
diff --git a/src/hooks/api/useEmployees.js b/src/hooks/api/useEmployees.js
deleted file mode 100644
index 86f8942a..00000000
--- a/src/hooks/api/useEmployees.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { employeesApi } from '../../lib/api/services';
-import { toast } from 'react-toastify';
-import {
- EMPLOYEES_LIST,
- EMPLOYEES_DETAIL,
- EMPLOYEES_DROPDOWN,
- API_ERROR_MESSAGE,
- STALE_TIME_LONG
-} from '../../constants';
-
-export function useEmployees(params = {}) {
- return useQuery({
- queryKey: [EMPLOYEES_LIST, params],
- queryFn: () => employeesApi.getList(params),
- keepPreviousData: true,
- });
-}
-
-export function useEmployee(id, options = {}) {
- return useQuery({
- queryKey: [EMPLOYEES_DETAIL, id],
- queryFn: () => employeesApi.getById(id),
- enabled: !!id,
- ...options,
- });
-}
-
-export function useEmployeesDropdown() {
- return useQuery({
- queryKey: [EMPLOYEES_DROPDOWN],
- queryFn: () => employeesApi.getDropdown(),
- staleTime: STALE_TIME_LONG,
- });
-}
-
-export function useCreateEmployee() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (data) => employeesApi.create(data),
- onSuccess: () => {
- queryClient.invalidateQueries([EMPLOYEES_LIST]);
- queryClient.invalidateQueries([EMPLOYEES_DROPDOWN]);
- toast.success('Employee created successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
-
-export function useUpdateEmployee() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (data) => employeesApi.update(data),
- onSuccess: () => {
- queryClient.invalidateQueries([EMPLOYEES_LIST]);
- queryClient.invalidateQueries([EMPLOYEES_DETAIL]);
- queryClient.invalidateQueries([EMPLOYEES_DROPDOWN]);
- toast.success('Employee updated successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
-
-export function useDeleteEmployee() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (id) => employeesApi.delete(id),
- onSuccess: () => {
- queryClient.invalidateQueries([EMPLOYEES_LIST]);
- queryClient.invalidateQueries([EMPLOYEES_DETAIL]);
- queryClient.invalidateQueries([EMPLOYEES_DROPDOWN]);
- toast.success('Employee deleted successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
diff --git a/src/hooks/api/usePMSchedules.js b/src/hooks/api/usePMSchedules.js
deleted file mode 100644
index 98423bdc..00000000
--- a/src/hooks/api/usePMSchedules.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { pmSchedulesApi } from '../../lib/api/services';
-import { toast } from 'react-toastify';
-import {
- PM_SCHEDULES_LIST,
- PM_SCHEDULES_DETAIL,
- API_SUCCESS_MESSAGE,
- API_ERROR_MESSAGE,
-} from '../../constants';
-
-/**
- * Hook to fetch PM schedules list
- */
-export function usePMSchedules(params = {}) {
- return useQuery({
- queryKey: [PM_SCHEDULES_LIST, params],
- queryFn: () => pmSchedulesApi.getList(params),
- keepPreviousData: true,
- });
-}
-
-/**
- * Hook to fetch a single PM schedule by ID
- */
-export function usePMSchedule(id, options = {}) {
- return useQuery({
- queryKey: [PM_SCHEDULES_DETAIL, id],
- queryFn: () => pmSchedulesApi.getById(id),
- enabled: !!id,
- ...options,
- });
-}
-
-/**
- * Hook to save (create/update) a PM schedule
- */
-export function useSavePMSchedule() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (data) => pmSchedulesApi.save(data),
- onSuccess: () => {
- queryClient.invalidateQueries([PM_SCHEDULES_LIST]);
- queryClient.invalidateQueries([PM_SCHEDULES_DETAIL]);
- toast.success('PM schedule saved successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
-
-/**
- * Hook to delete a PM schedule
- */
-export function useDeletePMSchedule() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (id) => pmSchedulesApi.delete(id),
- onSuccess: () => {
- queryClient.invalidateQueries([PM_SCHEDULES_LIST]);
- toast.success('PM schedule deleted successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
diff --git a/src/hooks/api/useWorkOrders.js b/src/hooks/api/useWorkOrders.js
deleted file mode 100644
index 49f3d8b0..00000000
--- a/src/hooks/api/useWorkOrders.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { workOrdersApi } from '../../lib/api/services';
-import { toast } from 'react-toastify';
-import {
- WORK_ORDERS_LIST,
- WORK_ORDERS_DETAIL,
- API_SUCCESS_MESSAGE,
- API_ERROR_MESSAGE
-} from '../../constants';
-
-/**
- * Hook to fetch work orders list with pagination and filters
- */
-export function useWorkOrders(params = {}) {
- return useQuery({
- queryKey: [WORK_ORDERS_LIST, params],
- queryFn: () => workOrdersApi.getList(params),
- keepPreviousData: true,
- });
-}
-
-/**
- * Hook to fetch a single work order by ID
- */
-export function useWorkOrder(id, options = {}) {
- return useQuery({
- queryKey: [WORK_ORDERS_DETAIL, id],
- queryFn: () => workOrdersApi.getById(id),
- enabled: !!id,
- ...options,
- });
-}
-
-/**
- * Hook to create a new work order
- */
-export function useCreateWorkOrder() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (data) => workOrdersApi.create(data),
- onSuccess: () => {
- queryClient.invalidateQueries([WORK_ORDERS_LIST]);
- queryClient.invalidateQueries([WORK_ORDERS_DETAIL]);
- toast.success('Work order created successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
-
-/**
- * Hook to update a work order
- */
-export function useUpdateWorkOrder() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (data) => workOrdersApi.update(data),
- onSuccess: (data, variables) => {
- queryClient.invalidateQueries([WORK_ORDERS_LIST]);
- if (variables.id) {
- queryClient.invalidateQueries([WORK_ORDERS_DETAIL, variables.id]);
- }
- toast.success('Work order updated successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
-
-/**
- * Hook to delete a work order
- */
-export function useDeleteWorkOrder() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (id) => workOrdersApi.delete(id),
- onSuccess: () => {
- queryClient.invalidateQueries([WORK_ORDERS_LIST]);
- queryClient.invalidateQueries([WORK_ORDERS_DETAIL]);
- toast.success('Work order deleted successfully!');
- },
- onError: (error) => {
- toast.error(error.message || API_ERROR_MESSAGE);
- },
- });
-}
diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js
deleted file mode 100644
index 26a6a35e..00000000
--- a/src/hooks/useAuth.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useSelector, useDispatch } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
-import { useMutation } from '@tanstack/react-query';
-import {
- selectUser,
- selectIsAuthenticated,
- selectAuthLoading,
- selectAuthError,
- selectToken,
- setUser,
- logout as logoutAction,
- setLoading,
- setError,
-} from '../app/slices/authSlice';
-import { authApi } from '../lib/api/services';
-import { toast } from 'react-toastify';
-
-/**
- * Custom hook for authentication
- * Provides auth state and methods
- */
-export function useAuth() {
- const dispatch = useDispatch();
- const navigate = useNavigate();
-
- const user = useSelector(selectUser);
- const isAuthenticated = useSelector(selectIsAuthenticated);
- const loading = useSelector(selectAuthLoading);
- const error = useSelector(selectAuthError);
- const token = useSelector(selectToken);
-
- const loginMutation = useMutation({
- mutationFn: (credentials) => authApi.login(credentials),
- onMutate: () => {
- dispatch(setLoading(true));
- },
- onSuccess: (data) => {
- dispatch(setUser(data));
- toast.success('Login successful!');
- navigate('/');
- },
- onError: (error) => {
- dispatch(setError(error.message || 'Login failed'));
- toast.error(error.message || 'Login failed');
- },
- });
-
- const logout = () => {
- dispatch(logoutAction());
- toast.info('You have been logged out');
- navigate('/login');
- };
-
- const changePasswordMutation = useMutation({
- mutationFn: (data) => authApi.changePassword(data),
- onSuccess: () => {
- toast.success('Password changed successfully!');
- },
- onError: (error) => {
- toast.error(error.message || 'Failed to change password');
- },
- });
-
- return {
- user,
- isAuthenticated,
- loading: loading || loginMutation.isLoading,
- error,
- token,
- login: loginMutation.mutate,
- logout,
- changePassword: changePasswordMutation.mutate,
- isChangingPassword: changePasswordMutation.isLoading,
- };
-}
diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.js
deleted file mode 100644
index 72b2f037..00000000
--- a/src/hooks/useDebounce.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useState, useEffect } from 'react';
-
-/**
- * Debounce a value
- * @param {*} value - The value to debounce
- * @param {number} delay - Delay in milliseconds
- */
-export function useDebounce(value, delay = 500) {
- const [debouncedValue, setDebouncedValue] = useState(value);
-
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
-
- return () => {
- clearTimeout(handler);
- };
- }, [value, delay]);
-
- return debouncedValue;
-}
diff --git a/src/hooks/useModal.js b/src/hooks/useModal.js
deleted file mode 100644
index fe916a8b..00000000
--- a/src/hooks/useModal.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useState, useCallback } from 'react';
-
-/**
- * Reusable hook for modal state management
- */
-export function useModal(initialState = false) {
- const [isOpen, setIsOpen] = useState(initialState);
- const [data, setData] = useState(null);
-
- const open = useCallback((modalData = null) => {
- setData(modalData);
- setIsOpen(true);
- }, []);
-
- const close = useCallback(() => {
- setIsOpen(false);
- // Delay clearing data to allow for exit animations
- setTimeout(() => setData(null), 300);
- }, []);
-
- const toggle = useCallback(() => {
- setIsOpen((prev) => !prev);
- }, []);
-
- return {
- isOpen,
- data,
- open,
- close,
- toggle,
- setData,
- };
-}
diff --git a/src/hooks/usePaginatedList.js b/src/hooks/usePaginatedList.js
deleted file mode 100644
index 0ac4f18f..00000000
--- a/src/hooks/usePaginatedList.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-
-export default function usePaginatedList(fetchFn, extraParams = {}) {
- const [rows, setRows] = useState([]);
- const [search, setSearch] = useState("");
- const [debouncedSearch, setDebouncedSearch] = useState("");
- const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(12);
- const [totalCount, setTotalCount] = useState(0);
- const [loading, setLoading] = useState(true);
- const [err, setErr] = useState("");
- const [sortBy, setSortBy] = useState("lastUpdated");
- const [sortDir, setSortDir] = useState("desc");
- const debounceRef = useRef(null);
-
- const extraKey = JSON.stringify(extraParams);
-
- useEffect(() => {
- if (debounceRef.current) clearTimeout(debounceRef.current);
- debounceRef.current = setTimeout(() => {
- setDebouncedSearch(search);
- setPage(1);
- }, 600);
- return () => clearTimeout(debounceRef.current);
- }, [search]);
-
- const reload = useCallback(() => {
- setLoading(true);
- setErr("");
- fetchFn({ search: debouncedSearch, page, pageSize, sortBy, sortDir, ...extraParams })
- .then(({ rows, totalCount }) => {
- setRows(rows);
- setTotalCount(totalCount);
- })
- .catch((e) => setErr(e.message || "Failed to load"))
- .finally(() => setLoading(false));
- }, [fetchFn, debouncedSearch, page, pageSize, sortBy, sortDir, extraKey]);
-
- useEffect(() => { reload(); }, [reload]);
-
- return useMemo(() => ({
- rows, search, setSearch, page, setPage, pageSize, setPageSize,
- totalCount, loading, err, reload, sortBy, setSortBy, sortDir, setSortDir
- }), [rows, search, page, pageSize, totalCount, loading, err, reload, sortBy, sortDir]);
-}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 945f4f27..00000000
--- a/src/index.css
+++ /dev/null
@@ -1,569 +0,0 @@
-/* =========================================================
- ROOT + RESET
- ========================================================= */
-:root {
- --sidebar-w: 220px;
- --sidebar-collapsed-w: 56px;
- --topbar-h: 64px;
-
- --blue-main: #0c4f6f;
- --blue-dark: #083a52;
- --border: #d9dde0;
- --bg: #f4f6f7;
-}
-
-* {
- box-sizing: border-box;
-}
-
-html, body, #root {
- height: 100%;
-}
-
-body {
- margin: 0;
- font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- font-size: 13px;
- background: var(--bg);
- color: #333;
-}
-
-/* =========================================================
- APP LAYOUT
- ========================================================= */
-.app {
- display: flex;
- min-height: 100vh;
- width: 100%;
-}
-
-/* =========================================================
- SIDEBAR
- ========================================================= */
-.sidebar {
- width: var(--sidebar-w);
- height: 100vh;
- background: #fbfbfb;
- border-right: 1px solid var(--border);
- padding: 20px;
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- transition: width .25s ease;
-}
-
-/* Collapsed width */
-.app.nav-collapsed .sidebar {
- width: var(--sidebar-collapsed-w);
-}
-/* Sidebar logo */
-.sidebarLogo {
- max-width: 140px;
- height: auto;
- display: block;
-}
-
-/* Center logo when collapsed */
-.app.nav-collapsed .sidebarLogo {
- max-width: 32px;
- margin: 0 auto;
-}
-
-/* =========================================================
- LOGO
- ========================================================= */
-.logoRow {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 28px;
-}
-
-.logoMark {
- font-size: 26px;
- font-weight: bold;
- color: #e74c3c;
-}
-
-.logoText {
- font-size: 17px;
- line-height: 1.2;
-}
-
-.logoText span {
- font-size: 13px;
- color: #666;
-}
-
-/* =========================================================
- NAVIGATION
- ========================================================= */
-.navTitle {
- font-size: 12px;
- font-weight: bold;
- letter-spacing: 1.5px;
- color: #888;
- margin-bottom: 14px;
-}
-
-.navGroup {
- list-style: none;
-}
-
-.navGroupList {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.navGroupTitle {
- font-size: 11px;
- font-weight: 600;
- letter-spacing: 1.2px;
- text-transform: uppercase;
- color: #9aa0a6;
- margin-top: 18px;
- margin-bottom: 6px;
- padding: 0 14px;
-}
-
-.navGroupTitleFirst {
- margin-top: 0;
-}
-
-.navList {
- list-style: none;
- padding: 0;
- margin: 0;
- flex-grow: 1;
-}
-
-/* Reset anchor defaults */
-.navList a,
-.navList a:visited,
-.navList a:hover,
-.navList a:active {
- text-decoration: none;
- color: #333;
-}
-
-/* Nav item */
-.navItem {
- display: flex;
- align-items: center;
- padding: 10px 14px;
- border-radius: 6px;
- margin-bottom: 6px;
- font-weight: normal;
- transition: background-color .15s ease;
-}
-
-/* Hover */
-.navItem:hover {
- background: #eef2f4;
-}
-
-/* Active (expanded only) */
-.navItem.active {
- background: #e6f1f7;
- font-weight: 600;
- border-left: 3px solid #0b80b5;
-}
-
-/* Icon */
-.glyph {
- font-size: 18px;
- width: 22px;
- display: inline-flex;
- justify-content: center;
- margin-right: 12px;
- color: #8c949a;
-}
-
-/* Label */
-.navLabel {
- white-space: nowrap;
-}
-
-/* =========================================================
- COLLAPSED SIDEBAR (LEGACY BEHAVIOR)
- ========================================================= */
-
-/* Hide text only */
-.app.nav-collapsed .navLabel,
-.app.nav-collapsed .logoText,
-.app.nav-collapsed .navTitle,
-.app.nav-collapsed .navGroupTitle {
- display: none;
-}
-
-.app.nav-collapsed .navGroup + .navGroup {
- border-top: 1px solid #e5e7eb;
- margin-top: 8px;
- padding-top: 8px;
-}
-
-/* Center icons */
-.app.nav-collapsed .navItem {
- justify-content: center;
- padding-left: 0;
- padding-right: 0;
-}
-
-.app.nav-collapsed .glyph {
- margin-right: 0;
-}
-
-/* 🔥 REMOVE ACTIVE STYLING WHEN COLLAPSED (CRITICAL FIX) */
-.app.nav-collapsed .navItem.active {
- background: transparent;
- border-left: none;
- font-weight: normal;
-}
-
-/* Hover only (legacy) */
-.app.nav-collapsed .navItem:hover {
- background: #eef2f4;
-}
-
-/* =========================================================
- MAIN AREA (NEVER MOVES)
- ========================================================= */
-.mainArea {
- flex: 1 1 auto;
- min-width: 0;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-/* =========================================================
- TOPBAR
- ========================================================= */
-.topbar {
- height: var(--topbar-h);
- background: linear-gradient(var(--blue-main), var(--blue-dark));
- border-bottom: 1px solid #062c3e;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 16px;
- color: #fff;
-}
-
-.topLeft {
- display: flex;
- align-items: center;
-}
-
-.hamburger {
- background: none;
- border: none;
- color: #fff;
- font-size: 18px;
- cursor: pointer;
- margin-right: 12px;
-}
-
-.topRight {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.topbarIconBtn {
- background: none;
- border: none;
- color: rgba(255,255,255,0.7);
- font-size: 16px;
- cursor: pointer;
- padding: 6px 8px;
- border-radius: 4px;
-}
-
-.topbarIconBtn:hover {
- color: #fff;
- background: rgba(255,255,255,0.1);
-}
-
-.topbarLogout {
- color: rgba(255,255,255,0.5);
-}
-
-.topbarLogout:hover {
- color: #ff6b6b;
- background: rgba(255,100,100,0.1);
-}
-
-.notifDropdown {
- position: fixed;
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
- z-index: 99999;
- width: 300px;
- overflow: hidden;
-}
-
-.notifHeader {
- padding: 12px 16px;
- font-size: 14px;
- font-weight: 600;
- color: #333;
- border-bottom: 1px solid #eee;
-}
-
-.notifEmpty {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
- padding: 30px 16px;
- color: #999;
- font-size: 13px;
-}
-
-.notifEmptyIcon {
- font-size: 24px;
- color: #ccc;
-}
-
-.topbarUserBtn {
- display: flex;
- align-items: center;
- gap: 8px;
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px 8px;
- border-radius: 4px;
-}
-
-.topbarUserBtn:hover {
- background: rgba(255,255,255,0.1);
-}
-
-.topbarUserCaret {
- color: rgba(255,255,255,0.5);
- font-size: 12px;
- margin-left: 2px;
-}
-
-.userDropdown {
- position: fixed;
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
- z-index: 99999;
- width: 260px;
- overflow: hidden;
-}
-
-.userDropdownHeader {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px;
-}
-
-.userDropdownAvatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: #1fa8de;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 14px;
- color: #fff;
- flex-shrink: 0;
-}
-
-.userDropdownName {
- font-size: 14px;
- font-weight: 600;
- color: #333;
-}
-
-.userDropdownEmail {
- font-size: 12px;
- color: #999;
-}
-
-.userDropdownDivider {
- height: 1px;
- background: #eee;
-}
-
-.userDropdownItem {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
- padding: 10px 16px;
- border: none;
- background: #fff;
- font-size: 13px;
- color: #333;
- cursor: pointer;
- text-align: left;
-}
-
-.userDropdownItem:hover {
- background: #f5f5f5;
-}
-
-.userDropdownItem i {
- width: 16px;
- text-align: center;
- color: #999;
-}
-
-.userDropdownLogout {
- color: #dc3545;
-}
-
-.userDropdownLogout:hover {
- background: #fff5f5;
-}
-
-.userDropdownLogout i {
- color: #dc3545;
-}
-
-.topbarDivider {
- width: 1px;
- height: 24px;
- background: rgba(255,255,255,0.15);
- margin: 0 4px;
-}
-
-.topbarUser {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.avatar {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- background: #1fa8de;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 12px;
- color: #fff;
- flex-shrink: 0;
-}
-
-.topbarUserInfo {
- display: flex;
- flex-direction: column;
- line-height: 1.2;
-}
-
-.topbarUserName {
- font-size: 13px;
- font-weight: 600;
- color: #fff;
-}
-
-.topbarUserRole {
- font-size: 11px;
- color: rgba(255,255,255,0.5);
-}
-
-/* =========================================================
- CONTENT
- ========================================================= */
-.contentWrapper {
- padding: 10px;
-}
-
-/* =========================================================
- TABLE (CONTACTS / EMPLOYEES)
- ========================================================= */
-.contactTable {
- width: 100%;
- border-collapse: collapse;
-}
-
-.contactTable thead th {
- padding: 10px 12px;
- background: linear-gradient(#a9bdca, #8fa6b4);
- color: #fff;
- font-weight: 600;
- border-bottom: 1px solid #7d96a6;
-}
-
-.contactTable tbody td {
- padding: 10px 12px;
- border-bottom: 1px solid #eee;
-}
-
-.contactTable tbody tr:nth-child(odd) {
- background: #fff;
-}
-
-.contactTable tbody tr:nth-child(even) {
- background: #fbfbfb;
-}
-
-.controlsTop {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 12px;
-}
-
-.leftControls {
- display: flex;
- align-items: center;
-}
-
-.filter {
- display: flex;
- align-items: center;
-}
-
-.filter select {
- height: 34px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- padding: 6px 8px;
- background: #fff;
-}
-
-.filter input {
- height: 34px;
- width: 260px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: none;
- padding: 6px 10px;
-}
-
-.searchBtn {
- height: 34px;
- padding: 0 14px;
- border: 1px solid #0073aa;
- background-color: #0073aa;
- color: #fff;
- border-radius: 0 4px 4px 0;
- cursor: pointer;
-}
-
-.searchBtn:hover {
- background-color: #005f8d;
-}
-
-.rightControls {
- display: flex;
- align-items: center;
-}
diff --git a/src/index.html b/src/index.html
deleted file mode 100644
index 98499e9e..00000000
--- a/src/index.html
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index be44fd46..00000000
--- a/src/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import { BrowserRouter } from "react-router";
-import { Provider } from 'react-redux';
-import { QueryClientProvider } from '@tanstack/react-query';
-import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
-import { store } from './app/store';
-import { queryClient } from './lib/queryClient';
-import App from "./App";
-import "./index.css";
-import "./site.css";
-
-const root = ReactDOM.createRoot(document.getElementById("root"));
-root.render(
-
-
-
- {process.env.NODE_ENV === 'development' && }
-
-
-);
diff --git a/src/lib/api/client.js b/src/lib/api/client.js
deleted file mode 100644
index 68bd5f55..00000000
--- a/src/lib/api/client.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import axios from 'axios';
-import { API_URL } from '../../constants';
-
-// Create axios instance
-const apiClient = axios.create({
- baseURL: API_URL,
- headers: {
- 'Content-Type': 'application/json',
- 'Accept': 'application/json',
- },
-});
-
-// Request interceptor - Add auth token
-apiClient.interceptors.request.use(
- (config) => {
- const tokenData = window.localStorage.getItem('token');
- if (tokenData) {
- try {
- const user = JSON.parse(tokenData);
- if (user?.token) {
- config.headers.Authorization = `Bearer ${user.token}`;
- }
- } catch (error) {
- console.error('Error parsing token:', error);
- }
- }
- return config;
- },
- (error) => {
- return Promise.reject(error);
- }
-);
-
-// Response interceptor - Handle errors globally
-apiClient.interceptors.response.use(
- (response) => {
- // Handle different response structures from the API
- // Some endpoints return { data: [...] }, some return plain arrays
- return response;
- },
- (error) => {
- if (error.response) {
- // The request was made and the server responded with a status code
- // that falls out of the range of 2xx
- const { status, data } = error.response;
-
- if (status === 401) {
- // Unauthorized - redirect to login
- window.localStorage.removeItem('token');
- window.location.href = '/login';
- return Promise.reject(new Error('You are not authorized to access this page.'));
- }
-
- if (status === 403) {
- return Promise.reject(new Error('You do not have permission to perform this action.'));
- }
-
- if (status === 404) {
- return Promise.reject(new Error('The requested resource was not found.'));
- }
-
- if (status === 500) {
- return Promise.reject(new Error('A server error occurred. Please try again later.'));
- }
-
- // Extract error message from response
- const message = data?.message || data?.Message || data?.error || 'An error occurred';
- return Promise.reject(new Error(message));
- } else if (error.request) {
- // The request was made but no response was received
- return Promise.reject(new Error('No response from server. Please check your connection.'));
- } else {
- // Something happened in setting up the request that triggered an Error
- return Promise.reject(error);
- }
- }
-);
-
-export default apiClient;
diff --git a/src/lib/api/services.js b/src/lib/api/services.js
deleted file mode 100644
index 12097781..00000000
--- a/src/lib/api/services.js
+++ /dev/null
@@ -1,268 +0,0 @@
-import apiClient from './client';
-
-/**
- * Generic API service factory
- * Creates CRUD operations for any entity
- */
-const createApiService = (endpoint) => ({
- getAll: (params) => apiClient.get(endpoint, { params }).then((res) => res.data),
-
- getById: (id) => apiClient.get(`${endpoint}/${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post(endpoint, data).then((res) => res.data),
-
- update: (id, data) => apiClient.put(`${endpoint}/${id}`, data).then((res) => res.data),
-
- delete: (id) => apiClient.delete(`${endpoint}/${id}`).then((res) => res.data),
-});
-
-// Work Orders API
-export const workOrdersApi = {
- getList: (params) =>
- apiClient
- .get('/WorkOrder/GetWorkOrderList', { params })
- .then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- page: res.data?.pageNumber || res.data?.PageNumber || params?.page || 1,
- pageSize: res.data?.pageSize || res.data?.PageSize || params?.pageSize || 12,
- };
- }),
-
- getById: (id) => apiClient.get(`/WorkOrder/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/WorkOrder/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/WorkOrder/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.get(`/WorkOrder/DeleteWorkOrder?id=${id}`).then((res) => res.data),
-};
-
-// PM Schedules API
-export const pmSchedulesApi = {
- getList: (params) =>
- apiClient
- .get('/PMSchedule/GetList', { params })
- .then((res) => {
- // Handle both array responses and paginated responses
- const data = res.data;
- const items = Array.isArray(data) ? data : data?.value || data?.data || data?.Data || [];
-
- return {
- items,
- totalCount: data?.Count || data?.totalCount || items.length,
- page: params?.page || 1,
- pageSize: params?.pageSize || 12,
- };
- }),
-
- getById: (id) => apiClient.get(`/PMSchedule/GetById?id=${id}`).then((res) => res.data),
-
- save: (data) => apiClient.post('/PMSchedule/Save', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/PMSchedule/Delete?id=${id}`).then((res) => res.data),
-};
-
-// Follow Ups API
-export const followUpsApi = {
- getList: (params) =>
- apiClient
- .get('/FollowUp/GetFollowUpList', { params })
- .then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/FollowUp/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/FollowUp/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/FollowUp/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/FollowUp/Delete?id=${id}`).then((res) => res.data),
-
- complete: (id) => apiClient.post(`/FollowUp/Complete?id=${id}`).then((res) => res.data),
-
- cancel: (id) => apiClient.post(`/FollowUp/Cancel?id=${id}`).then((res) => res.data),
-};
-
-// Employees API
-export const employeesApi = {
- getList: (params) =>
- apiClient
- .get('/Employee/GetEmployeeList', { params })
- .then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/Employee/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/Employee/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/Employee/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/Employee/Delete?id=${id}`).then((res) => res.data),
-
- getDropdown: () => apiClient.get('/Employee/Dropdown').then((res) => res.data),
-};
-
-// Contacts API
-export const contactsApi = {
- getList: (params) =>
- apiClient
- .get('/Contact/GetContactList', { params })
- .then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/Contact/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/Contact/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/Contact/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/Contact/Delete?id=${id}`).then((res) => res.data),
-
- getDropdown: (locationId) =>
- apiClient.get(`/Contact/Dropdown?locationId=${locationId}`).then((res) => res.data),
-};
-
-// Locations API
-export const locationsApi = {
- getList: (params) =>
- apiClient.get('/Location/GetLocationList', { params }).then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/Location/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/Location/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/Location/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/Location/Delete?id=${id}`).then((res) => res.data),
-
- getDropdown: (accountId) =>
- apiClient.get(`/Location/Dropdown?accountId=${accountId || ''}`).then((res) => res.data),
-};
-
-// Accounts API
-export const accountsApi = {
- getList: (params) =>
- apiClient.get('/Account/GetAccountList', { params }).then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/Account/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/Account/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/Account/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/Account/Delete?id=${id}`).then((res) => res.data),
-
- getDropdown: () => apiClient.get('/Account/Dropdown').then((res) => res.data),
-};
-
-// Assets API
-export const assetsApi = {
- getList: (params) =>
- apiClient.get('/Asset/GetAssetList', { params }).then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/Asset/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/Asset/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/Asset/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/Asset/Delete?id=${id}`).then((res) => res.data),
-};
-
-// Vendors API
-export const vendorsApi = {
- getList: (params) =>
- apiClient.get('/Vendor/GetVendorList', { params }).then((res) => {
- const data = res.data?.data || res.data?.Data || res.data || [];
- return {
- items: Array.isArray(data) ? data : [],
- totalCount: res.data?.totalCount || res.data?.TotalCount || data.length,
- };
- }),
-
- getById: (id) => apiClient.get(`/Vendor/GetById?id=${id}`).then((res) => res.data),
-
- create: (data) => apiClient.post('/Vendor/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/Vendor/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.post(`/Vendor/Delete?id=${id}`).then((res) => res.data),
-
- getDropdown: (params) =>
- apiClient.get('/Vendor/Dropdown', { params }).then((res) => res.data),
-};
-
-// Dashboard API
-export const dashboardApi = {
- getStats: () => apiClient.get('/Dashboard/Stats').then((res) => res.data),
-};
-
-// Dropdown Options API
-export const dropdownApi = {
- getByCategory: (category, parent) => {
- const params = parent ? { parent } : {};
- return apiClient
- .get(`/DropdownOptions/ByCategory/${encodeURIComponent(category)}`, { params })
- .then((res) => res.data);
- },
-
- create: (data) => apiClient.post('/DropdownOptions/Create', data).then((res) => res.data),
-
- update: (data) => apiClient.post('/DropdownOptions/Update', data).then((res) => res.data),
-
- delete: (id) => apiClient.delete(`/DropdownOptions/${id}`).then((res) => res.data),
-};
-
-// Authentication API
-export const authApi = {
- login: (credentials) =>
- apiClient.post('/Authentication/Login', credentials).then((res) => res.data),
-
- changePassword: (data) =>
- apiClient.post('/Authentication/ChangePassword', data).then((res) => res.data),
-
- forgotPassword: (email) =>
- apiClient.post(`/Authentication/ForgetPassword?Email=${encodeURIComponent(email)}`).then((res) => res.data),
-
- verifyResetCode: (code) =>
- apiClient.post(`/Authentication/VerificationCode?code=${encodeURIComponent(code)}`).then((res) => res.data),
-
- resetPassword: (data) =>
- apiClient.post('/Authentication/ResetPassword', data).then((res) => res.data),
-};
diff --git a/src/lib/queryClient.js b/src/lib/queryClient.js
deleted file mode 100644
index ec1a63c6..00000000
--- a/src/lib/queryClient.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { QueryClient } from '@tanstack/react-query';
-import { toast } from 'react-toastify';
-
-export const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 5 * 60 * 1000, // 5 minutes
- cacheTime: 10 * 60 * 1000, // 10 minutes
- retry: 1,
- refetchOnWindowFocus: false,
- onError: (error) => {
- console.error('Query error:', error);
- toast.error(error.message || 'An error occurred while fetching data');
- },
- },
- mutations: {
- onError: (error) => {
- console.error('Mutation error:', error);
- toast.error(error.message || 'An error occurred while updating data');
- },
- },
- },
-});
-
-export default queryClient;
diff --git a/src/pages/Dashboard.css b/src/pages/Dashboard.css
deleted file mode 100644
index aeb19af4..00000000
--- a/src/pages/Dashboard.css
+++ /dev/null
@@ -1,122 +0,0 @@
-/* Dashboard Styles */
-.dashboardPage {
- padding: 20px;
-}
-
-.pageTitle {
- font-size: 24px;
- font-weight: 600;
- margin-bottom: 20px;
- color: #333;
-}
-
-.kpiContainer {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- gap: 15px;
- margin-bottom: 30px;
-}
-
-.kpiCard {
- padding: 20px;
- border-radius: 4px;
- color: white;
- display: flex;
- align-items: center;
- gap: 15px;
- min-height: 100px;
- cursor: pointer;
- transition: filter 0.15s;
-}
-
-.kpiCard:hover {
- filter: brightness(0.9);
-}
-
-.bg-success { background-color: #28a745; }
-.bg-primary { background-color: #007bff; }
-.bg-info { background-color: #17a2b8; }
-.bg-warning { background-color: #ffc107; color: #333; }
-.bg-danger { background-color: #dc3545; }
-
-.kpiIcon {
- font-size: 32px;
- min-width: 50px;
-}
-
-.kpiContent h5 {
- margin: 0 0 10px 0;
- font-size: 16px;
- font-weight: 600;
-}
-
-.kpiContent p {
- margin: 0;
- font-size: 14px;
- opacity: 0.9;
-}
-
-.kpiStat {
- font-size: 24px;
- font-weight: bold;
-}
-
-.card {
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
- margin-bottom: 20px;
-}
-
-.mt-4 { margin-top: 30px; }
-
-.cardHeader {
- padding: 20px;
- border-bottom: 1px solid #ddd;
-}
-
-.cardHeader h3 {
- margin: 0 0 5px 0;
- font-size: 18px;
- color: #333;
-}
-
-.cardHeader p {
- margin: 0;
- font-size: 13px;
- color: #666;
-}
-
-.tableWrapper {
- padding: 20px;
-}
-
-.dashTable {
- width: 100%;
- border-collapse: collapse;
- font-size: 14px;
-}
-
-.dashTable th {
- background: #f5f5f5;
- border: 1px solid #ddd;
- padding: 12px;
- text-align: left;
- font-weight: 600;
- color: #333;
-}
-
-.dashTable td {
- border: 1px solid #ddd;
- padding: 10px 12px;
- color: #333;
-}
-
-.dashTable tbody tr:hover {
- background: #f9f9f9;
-}
-
-.text-right {
- text-align: right;
-}
diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js
deleted file mode 100644
index 1cd26e0e..00000000
--- a/src/pages/Dashboard.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { get } from "../apiUtil";
-import { API_URL } from "../constants";
-import "./Dashboard.css";
-
-const Dashboard = () => {
- const navigate = useNavigate();
- const [stats, setStats] = useState(null);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- get(`${API_URL}/Dashboard/Stats`)
- .then((data) => setStats(data))
- .catch(() => {})
- .finally(() => setLoading(false));
- }, []);
-
- const kpis = [
- {
- title: "Create a Work Order",
- icon: "fa-plus",
- color: "success",
- desc: "Report a problem you need fixed",
- onClick: () => navigate("/workorders/new"),
- },
- {
- title: "Open Work Orders",
- icon: "fa-folder-open",
- color: "primary",
- stat: loading ? "..." : stats?.open ?? 0,
- onClick: () => navigate("/workorders"),
- },
- {
- title: "Not Dispatched",
- icon: "fa-clock-o",
- color: "info",
- stat: loading ? "..." : stats?.notDispatched ?? 0,
- onClick: () => navigate("/workorders"),
- },
- {
- title: "Completed Orders",
- icon: "fa-check-circle",
- color: "warning",
- stat: loading ? "..." : stats?.completed ?? 0,
- onClick: () => navigate("/workorders"),
- },
- ];
-
- return (
-
- Dashboard
-
-
- {kpis.map((kpi, idx) => (
-
-
-
-
-
-
{kpi.title}
- {kpi.stat !== undefined &&
{kpi.stat?.toLocaleString?.() ?? kpi.stat}
}
- {kpi.desc &&
{kpi.desc}
}
-
-
- ))}
-
-
-
-
-
Priority Analysis
-
Age of open Work Orders by Priority
-
-
-
-
-
- | Priority |
- Total |
- 0-14 Days |
- 15-29 Days |
- 30-59 Days |
- 60+ Days |
-
-
-
- {[5, 4, 3, 2, 1].map((p) => (
-
- | {p} |
- — |
- — |
- — |
- — |
- — |
-
- ))}
-
-
-
-
-
-
-
-
Invoice Analysis
-
-
-
-
-
- | Status |
- Count |
- Amount |
-
-
-
- {["Draft", "Challenged", "Pending Approval", "Approved", "Paid"].map((s) => (
-
- | {s} |
- 0 |
- $0.00 |
-
- ))}
-
-
-
-
-
- );
-};
-
-export default Dashboard;
diff --git a/src/pages/PmSchedule/api.js b/src/pages/PmSchedule/api.js
deleted file mode 100644
index a79e59f9..00000000
--- a/src/pages/PmSchedule/api.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { get, post } from "../../apiUtil";
-import { API_URL } from "../../constants";
-
-export const getPMSchedules = async () => {
- const response = await get(`${API_URL}/PMSchedule/GetList`);
- return Array.isArray(response) ? response : (response?.value || response?.data || response?.Data || []);
-};
-
-export const getPMScheduleById = async (id) => {
- return get(`${API_URL}/PMSchedule/GetById?id=${id}`);
-};
-
-export const addPMSchedule = async (pmSchedule) => {
- const payload = {
- Id: 0,
- Name: pmSchedule.assetName || pmSchedule.name,
- Frequency: pmSchedule.frequency,
- StartDate: pmSchedule.nextServiceDate || pmSchedule.startDate,
- IsActive: pmSchedule.status === "Active" || pmSchedule.isActive !== false,
- AssetId: pmSchedule.assetId || null,
- LocationId: pmSchedule.locationId || null
- };
- return post(`${API_URL}/PMSchedule/Save`, payload);
-};
-
-export const editPMSchedule = async (pmSchedule) => {
- const payload = {
- Id: pmSchedule.id,
- Name: pmSchedule.assetName || pmSchedule.name,
- Frequency: pmSchedule.frequency,
- StartDate: pmSchedule.nextServiceDate || pmSchedule.startDate,
- IsActive: pmSchedule.status === "Active" || pmSchedule.isActive !== false,
- AssetId: pmSchedule.assetId || null,
- LocationId: pmSchedule.locationId || null
- };
- return post(`${API_URL}/PMSchedule/Save`, payload);
-};
-
-export const deletePMSchedule = async (id) => {
- return post(`${API_URL}/PMSchedule/Delete?id=${id}`);
-};
diff --git a/src/pages/PmSchedule/edit/api.js b/src/pages/PmSchedule/edit/api.js
deleted file mode 100644
index 7da1e39c..00000000
--- a/src/pages/PmSchedule/edit/api.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const editEmployee = async (payload) => {
- return post(`${API_URL}/Employee/UpdateEmployee`, payload);
-};
diff --git a/src/pages/PmSchedule/form/PmScheduleFormPage.css b/src/pages/PmSchedule/form/PmScheduleFormPage.css
deleted file mode 100644
index 36081949..00000000
--- a/src/pages/PmSchedule/form/PmScheduleFormPage.css
+++ /dev/null
@@ -1,289 +0,0 @@
-/* PM Schedule Form Page - Matching Employee Form Style */
-.pmScheduleFormPage {
- background: #f5f5f5;
- min-height: 100vh;
- padding: 0;
-}
-
-.panel {
- margin-bottom: 20px;
- background-color: #fff;
- border: 1px solid transparent;
- border-radius: 4px;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.panel-default {
- border-color: #ddd;
-}
-
-.panel-connected {
- border-radius: 0;
- border: none;
- box-shadow: none;
-}
-
-.panel-body {
- padding: 15px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 15px;
- margin-bottom: 20px;
- list-style: none;
- background-color: #f5f5f5;
- border-radius: 4px;
-}
-
-.breadcrumb-alt {
- background-color: white;
- border-bottom: 1px solid #e5e5e5;
- padding: 10px 20px;
- margin: 0 0 20px 0;
- border-radius: 0;
-}
-
-.breadcrumb > li {
- display: inline-block;
-}
-
-.breadcrumb > li + li:before {
- content: ">\00a0";
- padding: 0 5px;
- color: #ccc;
-}
-
-.breadcrumb > li a {
- color: #0088cc;
- text-decoration: none;
-}
-
-.breadcrumb > li a:hover {
- text-decoration: underline;
-}
-
-.breadcrumb > .active {
- color: #00a0d1;
- font-weight: bold;
- cursor: default;
-}
-
-/* Container */
-.container {
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-
-/* Form Header */
-.header {
- margin-bottom: 15px;
-}
-
-.heading {
- margin: 0 0 10px 0;
- font-weight: 300;
- line-height: 1.1;
- color: #333;
-}
-
-.heading.thin {
- font-weight: 300;
- font-size: 24px;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.heading.text-uppercase {
- text-transform: uppercase;
- font-size: 14px;
- margin-bottom: 15px;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle;
-}
-
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.fa-stack-2x {
- font-size: 2em;
-}
-
-.fa-stack-1x {
- font-size: 1em;
- line-height: inherit;
-}
-
-.fa-inverse {
- color: #fff;
-}
-
-/* Form Groups */
-.form-group {
- margin-bottom: 15px;
- position: relative;
-}
-
-/* Row and Columns */
-.row {
- margin-right: -15px;
- margin-left: -15px;
- display: flex;
- flex-wrap: wrap;
-}
-
-.row::after {
- content: "";
- display: table;
- clear: both;
-}
-
-[class*="col-"] {
- position: relative;
- min-height: 1px;
- padding-right: 15px;
- padding-left: 15px;
-}
-
-.col-sm-4 {
- width: 33.33333333%;
-}
-
-.col-sm-6 {
- width: 50%;
-}
-
-@media (min-width: 992px) {
- .col-md-4 {
- width: 33.33333333%;
- }
-}
-
-/* Labels */
-label {
- display: inline-block;
- max-width: 100%;
- margin-bottom: 5px;
- font-weight: 600;
- font-size: 13px;
- color: #333;
-}
-
-.required-field::after {
- content: " *";
- color: #d9534f;
-}
-
-/* Form Controls */
-.form-control {
- display: block;
- width: 100%;
- height: 34px;
- padding: 6px 12px;
- font-size: 14px;
- line-height: 1.42857143;
- color: #555;
- background-color: #fff;
- background-image: none;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-}
-
-select.form-control {
- height: 34px;
- padding: 6px 12px;
-}
-
-textarea.form-control {
- height: auto;
- min-height: 100px;
-}
-
-/* Form Actions */
-.form-actions {
- padding: 19px 20px 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- border-top: 1px solid #e5e5e5;
- text-align: right;
-}
-
-/* Buttons */
-.btn {
- display: inline-block;
- padding: 6px 12px;
- margin-bottom: 0;
- font-size: 14px;
- font-weight: 400;
- line-height: 1.42857143;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- cursor: pointer;
- border: 1px solid transparent;
- border-radius: 4px;
- user-select: none;
-}
-
-.btn-default {
- color: #333;
- background-color: #fff;
- border-color: #ccc;
-}
-
-.btn-default:hover {
- background-color: #e6e6e6;
- border-color: #adadad;
-}
-
-.btn-primary {
- color: #fff;
- background-color: #0088cc;
- border-color: #0088cc;
-}
-
-.btn-primary:hover {
- background-color: #006699;
- border-color: #005580;
-}
-
-.btn:disabled {
- cursor: not-allowed;
- opacity: 0.65;
-}
-
-/* Responsive */
-@media (max-width: 767px) {
- [class*="col-sm-"] {
- width: 100%;
- }
-}
-
diff --git a/src/pages/PmSchedule/form/PmScheduleFormPage.js b/src/pages/PmSchedule/form/PmScheduleFormPage.js
deleted file mode 100644
index d3be7e51..00000000
--- a/src/pages/PmSchedule/form/PmScheduleFormPage.js
+++ /dev/null
@@ -1,303 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { getPMScheduleById, addPMSchedule, editPMSchedule } from "../api";
-import "./PmScheduleFormPage.css";
-
-export default function PmScheduleFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- asset: "",
- assetName: "",
- frequency: "Quarterly",
- lastServiceDate: "",
- nextServiceDate: "",
- status: "Active",
- assignedTo: "",
- notes: "",
- });
-
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (isEdit && id) {
- const loadPMSchedule = async () => {
- try {
- const pm = await getPMScheduleById(id);
- if (pm) {
- setFormData(prev => {
- const merged = { ...prev };
- for (const key of Object.keys(merged)) {
- if (pm[key] != null) merged[key] = pm[key];
- }
- return merged;
- });
- }
- } catch (err) {
- console.error("Failed to load PM schedule", err);
- toast.error("Failed to load PM schedule");
- }
- };
- loadPMSchedule();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData(prev => ({
- ...prev,
- [name]: value,
- }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- if (isEdit) {
- await editPMSchedule(formData);
- toast.success("PM Schedule updated successfully!");
- } else {
- await addPMSchedule(formData);
- toast.success("PM Schedule created successfully!");
- }
- navigate("/pmschedules");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save PM Schedule");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate("/pmschedules");
- };
-
- return (
-
- );
-}
diff --git a/src/pages/PmSchedule/list/Delete.js b/src/pages/PmSchedule/list/Delete.js
deleted file mode 100644
index 394ae1a9..00000000
--- a/src/pages/PmSchedule/list/Delete.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { deleteContact } from "./api";
-
-export default function DeleteModal({ open, item, onClose, onSuccess }) {
- if(!open) return null;
- const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
- return (
-
-
-
-
-
-
Are you sure you want to delete this contact?
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/PmSchedule/list/Form.js b/src/pages/PmSchedule/list/Form.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/PmSchedule/list/List.js b/src/pages/PmSchedule/list/List.js
deleted file mode 100644
index 1f9c0f08..00000000
--- a/src/pages/PmSchedule/list/List.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { useState } from "react";
-import { useNavigate } from "react-router";
-import { usePMSchedules, useDeletePMSchedule } from "../../../hooks/api/usePMSchedules";
-import { useDebounce } from "../../../hooks/useDebounce";
-import { useModal } from "../../../hooks/useModal";
-import { DEBOUNCE_SEARCH, DEFAULT_PAGE_SIZE, VIEW_MODE_LIST } from "../../../constants";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import ActionBar from "../../../components/ActionBar";
-import "./styles.css";
-
-export default function PmScheduleListPage() {
- const navigate = useNavigate();
-
- // State management
- const [search, setSearch] = useState("");
- const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
- const [viewMode, setViewMode] = useState(VIEW_MODE_LIST);
-
- // Debounce search to avoid excessive API calls
- const debouncedSearch = useDebounce(search, DEBOUNCE_SEARCH);
-
- // React Query hook - handles loading, caching, refetching automatically
- const { data, isLoading, error, refetch } = usePMSchedules({
- search: debouncedSearch,
- page,
- pageSize,
- });
-
- // Delete mutation - handles success/error toasts automatically
- const deleteMutation = useDeletePMSchedule();
-
- // Modal state management using custom hook
- const deleteModal = useModal();
-
- // Extract data with safe defaults
- const rows = data?.items || [];
- const totalCount = data?.totalCount || 0;
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- // Event handlers
- const handleDeleteClick = (item) => {
- deleteModal.open(item);
- };
-
- const handleDeleteConfirm = async () => {
- await deleteMutation.mutateAsync(deleteModal.data.id);
- deleteModal.close();
- refetch();
- };
-
- return (
- <>
-
navigate("/pmschedules/new")}
- onBulkEdit={() => console.log("Bulk edit")}
- selectedCount={0}
- />
-
-
-
- PM Schedules
-
-
-
-
-
-
-
-
-
- Search for PM Schedules
-
- Use the settings menu to adjust how to filter the list, and which columns to display.
-
-
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Enter search criteria here..."
- />
-
-
-
-
-
-
-
-
-
-
-
- {page}
-
-
-
-
-
-
- navigate(`/pmschedules/edit/${item.id}`)}
- onDelete={handleDeleteClick}
- page={page}
- setPage={setPage}
- pageSize={pageSize}
- setPageSize={setPageSize}
- totalCount={totalCount}
- totalPages={totalPages}
- />
-
-
-
- >
- );
-}
diff --git a/src/pages/PmSchedule/list/Table.js b/src/pages/PmSchedule/list/Table.js
deleted file mode 100644
index a98782f6..00000000
--- a/src/pages/PmSchedule/list/Table.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
- const columns = [
- { key: "asset", label: "Asset", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
- { key: "frequency", label: "Frequency" },
- { key: "lastDate", label: "Last Date" },
- { key: "nextDate", label: "Next Date" },
- { key: "status", label: "Status" },
- { key: "assignedTo", label: "Assigned To" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/PmSchedule/list/api.js b/src/pages/PmSchedule/list/api.js
deleted file mode 100644
index 996b317a..00000000
--- a/src/pages/PmSchedule/list/api.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// pages/pmschedules/list/api.js
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchPmSchedulesPaged = async ({
- search = "",
- page = 1,
- pageSize = 12,
-}) => {
- const res = await get(
- `${API_URL}/PMSchedule/GetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`
- );
-
- // API returns array directly or {value: [...], Count: n}
- const dataArray = Array.isArray(res) ? res : (res?.value || res?.data || res?.Data || []);
-
- const rows = dataArray.map(x => ({
- id: x.id ?? x.Id,
- name: x.name ?? x.Name ?? "",
- asset: x.asset ?? x.Asset ?? x.assetName ?? x.AssetName ?? "",
- frequency: x.frequency ?? x.Frequency ?? "",
- startDate: x.startDate ?? x.StartDate ?? "",
- lastDate: x.lastDate ?? x.LastDate ?? "",
- nextDate: x.nextDate ?? x.NextDate ?? "",
- status: x.isActive ? "Active" : "Inactive",
- assignedTo: x.assignedTo ?? x.AssignedTo ?? "",
- }));
-
- const totalCount = res?.Count ?? res?.count ?? res?.totalCount ?? res?.TotalCount ?? rows.length;
-
- return {
- rows,
- page: res?.pageNumber ?? res?.PageNumber ?? page,
- pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
- totalCount: totalCount,
- totalPages: Math.max(1, Math.ceil(totalCount / pageSize)),
- };
-};
diff --git a/src/pages/PmSchedule/list/styles.css b/src/pages/PmSchedule/list/styles.css
deleted file mode 100644
index f561a2f8..00000000
--- a/src/pages/PmSchedule/list/styles.css
+++ /dev/null
@@ -1,231 +0,0 @@
-/* Modal styles for PM Schedules module */
-.modalRoot{position:fixed;inset:0;z-index:1000;}
-.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
-.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/PmSchedule/new/api.js b/src/pages/PmSchedule/new/api.js
deleted file mode 100644
index 81505470..00000000
--- a/src/pages/PmSchedule/new/api.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { get, post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const getPmScheduleById = (id) =>
- get(`${API_URL}/PmSchedule/GetById?id=${id}`);
-
-export const addPmSchedule = (payload) =>
- post(`${API_URL}/PmSchedule/Create`, payload);
-
-export const editPmSchedule = (payload) =>
- post(`${API_URL}/PmSchedule/Edit`, payload);
diff --git a/src/pages/accounts/api.js b/src/pages/accounts/api.js
deleted file mode 100644
index 1598926c..00000000
--- a/src/pages/accounts/api.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { accountApi } from "../../services/api";
-
-export const getAccounts = async (params = {}) => {
- try {
- const response = await accountApi.getAll(params);
- const accounts = response.data || response;
- return Array.isArray(accounts) ? accounts : [];
- } catch (error) {
- console.error("Error fetching accounts:", error);
- throw error;
- }
-};
-
-export const getAccountById = async (id) => {
- try {
- return await accountApi.getById(id);
- } catch (error) {
- console.error("Error fetching account:", error);
- throw error;
- }
-};
-
-export const addAccount = async (account) => {
- try {
- return await accountApi.create(account);
- } catch (error) {
- console.error("Error creating account:", error);
- throw error;
- }
-};
-
-export const editAccount = async (account) => {
- try {
- return await accountApi.update(account.id, account);
- } catch (error) {
- console.error("Error updating account:", error);
- throw error;
- }
-};
-
-export const deleteAccount = async (id) => {
- try {
- await accountApi.delete(id);
- return true;
- } catch (error) {
- console.error("Error deleting account:", error);
- throw error;
- }
-};
diff --git a/src/pages/accounts/edit/api.js b/src/pages/accounts/edit/api.js
deleted file mode 100644
index 7e5ce3d2..00000000
--- a/src/pages/accounts/edit/api.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const editAccount = (data) =>
- post(`${API_URL}/Account/EditAccount`, data);
diff --git a/src/pages/accounts/form/AccountFormPage.js b/src/pages/accounts/form/AccountFormPage.js
deleted file mode 100644
index aa1b9b45..00000000
--- a/src/pages/accounts/form/AccountFormPage.js
+++ /dev/null
@@ -1,239 +0,0 @@
-// src/pages/accounts/form/AccountFormPage.js
-import React, { useState, useEffect } from "react";
-import { useNavigate, useParams } from "react-router";
-import { toast } from "react-toastify";
-import "../../../styles/FormPage.css";
-import { getAccountById, addAccount, editAccount } from "../api";
-
-const AccountFormPage = () => {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [loading, setLoading] = useState(false);
- const [account, setAccount] = useState({});
-
- useEffect(() => {
- if (isEdit && id) {
- const loadAccount = async () => {
- try {
- const data = await getAccountById(id);
- if (data) {
- setAccount(data);
- }
- } catch (err) {
- console.error("Failed to load account", err);
- toast.error("Failed to load account");
- }
- };
- loadAccount();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value, type, checked } = e.target;
- setAccount((prev) => ({
- ...prev,
- [name]: type === "checkbox" ? checked : value,
- }));
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
-
- if (!account.AccountName) {
- toast.error("Account Name is required.");
- return;
- }
-
- try {
- setLoading(true);
-
- const payload = { ...account };
-
- if (isEdit) {
- await editAccount(payload);
- toast.success("Account updated successfully!");
- } else {
- await addAccount(payload);
- toast.success("Account created successfully!");
- }
-
- navigate("/accounts");
- } catch (err) {
- console.error(err);
- toast.error("Something went wrong while saving the account.");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
{isEdit ? "Edit Account" : "Create an Account"}
-
-
-
-
-
- );
-};
-
-export default AccountFormPage;
diff --git a/src/pages/accounts/list/Delete.js b/src/pages/accounts/list/Delete.js
deleted file mode 100644
index 394ae1a9..00000000
--- a/src/pages/accounts/list/Delete.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { deleteContact } from "./api";
-
-export default function DeleteModal({ open, item, onClose, onSuccess }) {
- if(!open) return null;
- const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
- return (
-
-
-
-
-
-
Are you sure you want to delete this contact?
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/accounts/list/Form.js b/src/pages/accounts/list/Form.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/accounts/list/List.js b/src/pages/accounts/list/List.js
deleted file mode 100644
index 3fa973ba..00000000
--- a/src/pages/accounts/list/List.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchAccountsPaged } from "./api";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import ActionBar from "../../../components/ActionBar";
-import { deleteAccount } from "../api";
-import "./styles.css";
-
-export default function AccountList() {
- const navigate = useNavigate();
- const { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload } = usePaginatedList(fetchAccountsPaged);
- const [deleteModal, setDeleteModal] = useState({ isOpen: false, item: null, loading: false });
- const [viewMode, setViewMode] = useState("list");
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- const handleDeleteClick = (item) => {
- setDeleteModal({ isOpen: true, item, loading: false });
- };
-
- const handleDeleteConfirm = async () => {
- setDeleteModal(prev => ({ ...prev, loading: true }));
- try {
- await deleteAccount(deleteModal.item.id);
- toast.success(`Account deleted successfully!`);
- setDeleteModal({ isOpen: false, item: null, loading: false });
- reload();
- } catch (err) {
- console.error(err);
- toast.error("Failed to delete account");
- setDeleteModal(prev => ({ ...prev, loading: false }));
- }
- };
-
- return (
- <>
-
navigate("/accounts/new")}
- onBulkEdit={() => console.log("Bulk edit")}
- selectedCount={0}
- />
-
-
-
- Accounts
-
-
-
-
-
-
-
-
-
- Search for Accounts
-
- Use the settings menu to adjust how to filter the list, and which columns to display.
-
-
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Enter search criteria here..."
- />
-
-
-
-
-
-
-
-
-
-
-
- {page}
-
-
-
-
-
-
- navigate(`/accounts/edit/${item.id}`)}
- onDelete={handleDeleteClick}
- page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize}
- totalCount={totalCount} totalPages={totalPages} />
-
- setDeleteModal({ isOpen: false, item: null, loading: false })}
- onConfirm={handleDeleteConfirm}
- title="Delete Account"
- message={`Are you sure you want to delete "${deleteModal.item?.name}"? This action cannot be undone.`}
- loading={deleteModal.loading}
- />
-
- >
- );
-}
diff --git a/src/pages/accounts/list/Table.js b/src/pages/accounts/list/Table.js
deleted file mode 100644
index 987d4f54..00000000
--- a/src/pages/accounts/list/Table.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-export default function Table({ loading, error, rows, onEdit, onDelete,
- page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
-
- const columns = [
- { key: "name", label: "Name", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
- { key: "email", label: "Email" },
- { key: "phone", label: "Phone" },
- { key: "owner", label: "Owner" },
- { key: "status", label: "Status" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/accounts/list/api.js b/src/pages/accounts/list/api.js
deleted file mode 100644
index b5be4008..00000000
--- a/src/pages/accounts/list/api.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchAccountsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
- const res = await get(`${API_URL}/Account/GetAccountList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id ?? x.Id,
- name: x.name ?? x.Name ?? x.AccountName ?? "",
- number: x.number ?? x.AccountNumber ?? "",
- industry: x.industry ?? x.Industry ?? "",
- website: x.website ?? x.Website ?? "",
- primaryContact: x.primaryContact ?? x.PrimaryContact ?? "",
- phoneNumber: x.phoneNumber ?? x.Phone ?? x.PhoneNumber ?? "",
- owner: x.owner ?? x.Owner ?? "",
- }));
- return {
- rows,
- page: res?.pageNumber ?? 1,
- pageSize: res?.pageSize ?? pageSize,
- totalCount: res?.totalCount ?? rows.length,
- totalPages: res?.totalPages ?? 1,
- };
-};
-
-export const deleteAccount = (id) =>
- get(`${API_URL}/Account/DeleteAccount?id=${id}`);
diff --git a/src/pages/accounts/list/styles.css b/src/pages/accounts/list/styles.css
deleted file mode 100644
index 9a850ae8..00000000
--- a/src/pages/accounts/list/styles.css
+++ /dev/null
@@ -1,231 +0,0 @@
-/* Modal styles for Accounts module */
-.modalRoot{position:fixed;inset:0;z-index:1000;}
-.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
-.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/accounts/new/api.js b/src/pages/accounts/new/api.js
deleted file mode 100644
index 80a4ade5..00000000
--- a/src/pages/accounts/new/api.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const addAccount = (data) =>
- post(`${API_URL}/Account/AddAccount`, data);
diff --git a/src/pages/assets/form/AssetFormPage.js b/src/pages/assets/form/AssetFormPage.js
deleted file mode 100644
index 9a8498f8..00000000
--- a/src/pages/assets/form/AssetFormPage.js
+++ /dev/null
@@ -1,290 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-import {
- Box,
- Button,
- TextField,
- Typography,
- Paper,
- Grid,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
- FormControlLabel,
- Switch,
- CircularProgress,
- Alert,
-} from "@mui/material";
-import ArrowBackIcon from "@mui/icons-material/ArrowBack";
-import SaveIcon from "@mui/icons-material/Save";
-import { get, post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-const AssetFormPage = () => {
- const navigate = useNavigate();
- const { id } = useParams();
- const isEdit = Boolean(id);
-
- const [formData, setFormData] = useState({
- assetCode: "",
- name: "",
- description: "",
- accountId: "",
- locationId: "",
- isActive: true,
- });
-
- const [accounts, setAccounts] = useState([]);
- const [locations, setLocations] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [loadingData, setLoadingData] = useState(false);
-
- // Load accounts and locations for dropdowns
- useEffect(() => {
- const loadDropdownData = async () => {
- try {
- // Fetch accounts
- const accountsRes = await get(`${API_URL}/Account/GetAccountList?pageSize=1000`);
- const accountsData = accountsRes?.data || accountsRes?.Data || [];
- setAccounts(accountsData);
-
- // Fetch locations - using full API path
- const locationsRes = await get(`/Common/GetSitesName`);
- // Handle both array response and wrapped response
- const locationsData = Array.isArray(locationsRes) ? locationsRes : (locationsRes?.data || locationsRes?.Data || []);
- setLocations(locationsData);
- } catch (err) {
- console.error("Failed to load dropdown data:", err);
- setError("Failed to load dropdown options");
- }
- };
-
- loadDropdownData();
- }, []);
-
- // Load asset data if editing
- useEffect(() => {
- if (isEdit && id) {
- const loadAsset = async () => {
- setLoadingData(true);
- try {
- const asset = await get(`${API_URL}/Asset/GetAssetById?id=${id}`);
- if (asset) {
- // Handle both camelCase and PascalCase from backend
- setFormData({
- assetCode: asset.assetCode || asset.AssetCode || "",
- name: asset.name || asset.Name || "",
- description: asset.description || asset.Description || "",
- accountId: asset.accountId || asset.AccountId || "",
- locationId: asset.locationId || asset.LocationId || "",
- isActive: asset.isActive ?? asset.IsActive ?? true,
- });
- }
- } catch (err) {
- console.error("Failed to load asset:", err);
- setError("Failed to load asset data");
- } finally {
- setLoadingData(false);
- }
- };
-
- loadAsset();
- }
- }, [isEdit, id]);
-
- const handleChange = (e) => {
- const { name, value, checked } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]: name === "isActive" ? checked : value,
- }));
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setLoading(true);
- setError(null);
-
- try {
- const endpoint = isEdit ? "EditAsset" : "AddAsset";
-
- // Prepare payload with proper data types
- const payload = {
- assetCode: formData.assetCode,
- name: formData.name,
- description: formData.description || null,
- accountId: formData.accountId ? parseInt(formData.accountId) : null,
- locationId: formData.locationId ? parseInt(formData.locationId) : null,
- isActive: formData.isActive,
- };
-
- // Add id for edit
- if (isEdit) {
- payload.id = parseInt(id);
- }
-
- await post(`${API_URL}/Asset/${endpoint}`, payload);
- navigate("/assets");
- } catch (err) {
- console.error("Failed to save asset:", err);
- setError(err.message || "Failed to save asset");
- } finally {
- setLoading(false);
- }
- };
-
- if (loadingData) {
- return (
-
-
-
- );
- }
-
- return (
-
- }
- onClick={() => navigate("/assets")}
- sx={{ mb: 2 }}
- >
- Back to Assets
-
-
-
-
- {isEdit ? "Edit Asset" : "Create New Asset"}
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Account
-
-
-
-
-
-
- Location
-
-
-
-
-
-
- }
- label="Active"
- />
-
-
-
-
-
- }
- disabled={loading}
- >
- {loading ? "Saving..." : isEdit ? "Update Asset" : "Create Asset"}
-
-
-
-
-
-
-
- );
-};
-
-export default AssetFormPage;
-
diff --git a/src/pages/assets/list/Delete.js b/src/pages/assets/list/Delete.js
deleted file mode 100644
index 9e4da221..00000000
--- a/src/pages/assets/list/Delete.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from "react";
-import {
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- Button,
- Typography,
-} from "@mui/material";
-
-const DeleteDialog = ({ open, onClose, onConfirm, itemName }) => {
- return (
-
- );
-};
-
-export default DeleteDialog;
-
diff --git a/src/pages/assets/list/List.js b/src/pages/assets/list/List.js
deleted file mode 100644
index 145794c3..00000000
--- a/src/pages/assets/list/List.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import {
- Box,
- Button,
- TextField,
- Typography,
- CircularProgress,
- Alert,
- Pagination,
- InputAdornment,
-} from "@mui/material";
-import AddIcon from "@mui/icons-material/Add";
-import SearchIcon from "@mui/icons-material/Search";
-import AssetsTable from "./Table";
-import DeleteDialog from "./Delete";
-import { useItems } from "./useItems";
-import { deleteAsset } from "./api";
-
-const AssetsList = () => {
- const navigate = useNavigate();
- const {
- items,
- loading,
- error,
- page,
- totalPages,
- search,
- handleSearch,
- handlePageChange,
- reload,
- } = useItems();
-
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
- const [itemToDelete, setItemToDelete] = useState(null);
- const [searchInput, setSearchInput] = useState(search);
-
- const handleDeleteClick = (id) => {
- const item = items.find((i) => i.id === id);
- setItemToDelete(item);
- setDeleteDialogOpen(true);
- };
-
- const handleDeleteConfirm = async () => {
- if (!itemToDelete) return;
-
- try {
- await deleteAsset(itemToDelete.id);
- setDeleteDialogOpen(false);
- setItemToDelete(null);
- reload();
- } catch (err) {
- console.error("Failed to delete asset:", err);
- alert("Failed to delete asset. Please try again.");
- }
- };
-
- const handleSearchSubmit = (e) => {
- e.preventDefault();
- handleSearch(searchInput);
- };
-
- return (
-
-
-
- Assets
-
- }
- onClick={() => navigate("/assets/new")}
- >
- New Asset
-
-
-
-
- setSearchInput(e.target.value)}
- InputProps={{
- startAdornment: (
-
-
-
- ),
- }}
- />
-
-
- {error && (
-
- {error}
-
- )}
-
- {loading ? (
-
-
-
- ) : (
- <>
-
-
- {totalPages > 1 && (
-
- handlePageChange(value)}
- color="primary"
- />
-
- )}
- >
- )}
-
- setDeleteDialogOpen(false)}
- onConfirm={handleDeleteConfirm}
- itemName={itemToDelete?.name || ""}
- />
-
- );
-};
-
-export default AssetsList;
-
diff --git a/src/pages/assets/list/Table.js b/src/pages/assets/list/Table.js
deleted file mode 100644
index fb88ab05..00000000
--- a/src/pages/assets/list/Table.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from "react";
-import { useNavigate } from "react-router-dom";
-import {
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- IconButton,
- Chip,
-} from "@mui/material";
-import EditIcon from "@mui/icons-material/Edit";
-import DeleteIcon from "@mui/icons-material/Delete";
-
-const AssetsTable = ({ items, onDelete }) => {
- const navigate = useNavigate();
-
- const handleEdit = (id) => {
- navigate(`/assets/edit/${id}`);
- };
-
- return (
-
-
-
-
- Asset Code
- Name
- Description
- Account
- Location
- Status
- Actions
-
-
-
- {items.length === 0 ? (
-
-
- No assets found
-
-
- ) : (
- items.map((item) => (
-
- {item.assetCode}
- {item.name}
- {item.description || "-"}
- {item.accountName || "-"}
- {item.locationName || "-"}
-
-
-
-
- handleEdit(item.id)}
- title="Edit"
- >
-
-
- onDelete(item.id)}
- title="Delete"
- >
-
-
-
-
- ))
- )}
-
-
-
- );
-};
-
-export default AssetsTable;
-
diff --git a/src/pages/assets/list/api.js b/src/pages/assets/list/api.js
deleted file mode 100644
index 38d200d3..00000000
--- a/src/pages/assets/list/api.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { get, post } from "../../../apiUtil";
-import { API_URL } from '../../../constants';
-
-
-export const fetchAssetsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
- const res = await get(`${API_URL}/Asset/GetAssetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id || x.Id,
- assetCode: x.assetCode || x.AssetCode || "",
- name: x.name || x.Name || "",
- description: x.description || x.Description || "",
- isActive: x.isActive ?? x.IsActive ?? true,
- accountName: x.accountName || x.AccountName || "",
- locationName: x.locationName || x.LocationName || "",
- }));
- return {
- rows,
- page: res?.pageNumber || res?.PageNumber || 1,
- pageSize: res?.pageSize || res?.PageSize || pageSize,
- totalCount: res?.totalCount || res?.TotalCount || rows.length,
- totalPages: res?.totalPages || res?.TotalPages || 1,
- };
-};
-
-
-export const deleteAsset = (id) =>
- get(`${API_URL}/Asset/DeleteAsset?id=${id}`);
-
diff --git a/src/pages/assets/list/useItems.js b/src/pages/assets/list/useItems.js
deleted file mode 100644
index dbfe01cf..00000000
--- a/src/pages/assets/list/useItems.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useState, useEffect, useCallback } from "react";
-import { fetchAssetsPaged } from "./api";
-
-export const useItems = () => {
- const [items, setItems] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(12);
- const [totalCount, setTotalCount] = useState(0);
- const [totalPages, setTotalPages] = useState(1);
- const [search, setSearch] = useState("");
-
- const loadItems = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const data = await fetchAssetsPaged({ search, page, pageSize });
- setItems(data.rows);
- setTotalCount(data.totalCount);
- setTotalPages(data.totalPages);
- } catch (err) {
- setError(err.message || "Failed to load assets");
- setItems([]);
- } finally {
- setLoading(false);
- }
- }, [search, page, pageSize]);
-
- useEffect(() => {
- loadItems();
- }, [loadItems]);
-
- const handleSearch = (newSearch) => {
- setSearch(newSearch);
- setPage(1);
- };
-
- const handlePageChange = (newPage) => {
- setPage(newPage);
- };
-
- const handlePageSizeChange = (newPageSize) => {
- setPageSize(newPageSize);
- setPage(1);
- };
-
- return {
- items,
- loading,
- error,
- page,
- pageSize,
- totalCount,
- totalPages,
- search,
- handleSearch,
- handlePageChange,
- handlePageSizeChange,
- reload: loadItems,
- };
-};
-
diff --git a/src/pages/auth/LoginPage.css b/src/pages/auth/LoginPage.css
deleted file mode 100644
index e20ac8da..00000000
--- a/src/pages/auth/LoginPage.css
+++ /dev/null
@@ -1,190 +0,0 @@
-/* Login Page Styles - Matching Legacy Officetrax Design */
-
-.loginPage {
- min-height: 100vh;
- /* background-image set via inline style in JSX */
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
- background-color: #333;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- padding: 0;
- margin: 0;
-}
-
-.login-wrapper {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- margin-top: 7%;
- padding: 0 15px;
-}
-
-.login-container {
- width: 100%;
- max-width: 400px;
-}
-
-.login-panel {
- background-color: white;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
- border-radius: 0;
- overflow: hidden;
-}
-
-.login-branding {
- height: 140px;
- background-color: #FFFFFF;
- margin: 0;
- border-bottom: solid 1px #b8b5b5;
- display: flex;
- align-items: center;
- justify-content: center;
- /* background-image set via inline style in JSX */
- background-repeat: no-repeat;
- background-position: center;
- background-size: auto 80%;
- padding: 20px;
-}
-
-.login-header {
- background-color: #003A5D;
- border-bottom: solid 1px #b8b5b5;
- height: 40px;
- padding: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.login-header h5 {
- margin: 0;
- padding: 0;
- color: white;
- font-size: 14px;
- font-weight: bold;
- line-height: 1;
- text-align: center;
-}
-
-.login-body {
- background-color: white;
- padding: 30px 30px 20px;
-}
-
-.login-form .form-group {
- margin-bottom: 30px;
-}
-
-.login-form .form-group:last-of-type {
- margin-bottom: 0;
-}
-
-.login-form .form-control {
- width: 100%;
- height: 40px;
- font-size: 14px;
- border: 1px solid #ccc;
- border-radius: 4px;
- padding: 6px 12px;
- box-sizing: border-box;
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
-}
-
-.login-form .form-control:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
-}
-
-.login-form .form-control::placeholder {
- color: #999;
-}
-
-.button-content {
- margin-top: 30px;
- text-align: center;
-}
-
-.login-button {
- background: #003A5D;
- color: white;
- width: 250px;
- height: 40px;
- font-size: 14px;
- font-weight: normal;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.2s;
- display: inline-block;
-}
-
-.login-button:hover {
- background: #00527d;
-}
-
-.login-button:disabled {
- background: #6c757d;
- cursor: not-allowed;
- opacity: 0.65;
-}
-
-.forgot-password {
- text-align: center;
- margin-top: 15px;
- margin-bottom: 0;
-}
-
-.forgot-password a {
- color: black;
- font-weight: bold;
- text-decoration: none;
- font-size: 13px;
-}
-
-.forgot-password a:hover {
- text-decoration: underline;
-}
-
-.error-message {
- color: #dc3545;
- font-size: 13px;
- margin-top: 10px;
- margin-bottom: 10px;
- text-align: center;
-}
-
-.login-footer {
- text-align: center;
- margin-top: 20px;
- padding: 0 15px;
-}
-
-.login-footer a {
- color: white;
- text-decoration: none;
- margin: 0 3px;
- font-size: 13px;
-}
-
-.login-footer a:hover {
- text-decoration: underline;
-}
-
-.copyright {
- text-align: center;
- color: white;
- font-size: 12px;
- margin-top: 15px;
- padding-bottom: 20px;
-}
-
-.copyright em {
- font-style: italic;
-}
-
diff --git a/src/pages/auth/LoginPage.js b/src/pages/auth/LoginPage.js
deleted file mode 100644
index 4f7496e9..00000000
--- a/src/pages/auth/LoginPage.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { useAuth } from "../../hooks/useAuth";
-import styles from "./LoginPage.module.css";
-
-export default function LoginPage() {
- const navigate = useNavigate();
- const { login: performLogin, loading, error: authError } = useAuth();
-
- const [formData, setFormData] = useState({
- username: "",
- password: "",
- });
- const [error, setError] = useState("");
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]: value,
- }));
- setError(""); // Clear error when user types
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- setError("");
-
- try {
- // Use Redux auth hook - automatically handles success/error and navigation
- performLogin(formData);
- } catch (err) {
- console.error("Login error:", err);
- setError("You have entered an invalid username or password");
- }
- };
-
- return (
-
-
-
- {/* Copyright */}
-
- ©2026 Core 7 Technologies Inc. All rights reserved.
-
-
- );
-}
-
diff --git a/src/pages/auth/LoginPage.module.css b/src/pages/auth/LoginPage.module.css
deleted file mode 100644
index dc60251d..00000000
--- a/src/pages/auth/LoginPage.module.css
+++ /dev/null
@@ -1,188 +0,0 @@
-/* Login Page Styles - Matching Legacy Officetrax Design */
-/* CSS Module - All classes are automatically scoped */
-
-.page {
- min-height: 100vh;
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
- background-color: #333;
- display: flex;
- flex-direction: column;
- justify-content: flex-start;
- padding: 0;
- margin: 0;
-}
-
-.wrapper {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- margin-top: 7%;
- padding: 0 15px;
-}
-
-.container {
- width: 100%;
- max-width: 400px;
-}
-
-.panel {
- background-color: white;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
- border-radius: 0;
- overflow: hidden;
-}
-
-.branding {
- height: 140px;
- background-color: #FFFFFF;
- margin: 0;
- border-bottom: solid 1px #b8b5b5;
- display: flex;
- align-items: center;
- justify-content: center;
- background-repeat: no-repeat;
- background-position: center;
- background-size: auto 80%;
- padding: 20px;
-}
-
-.header {
- background-color: #003A5D;
- border-bottom: solid 1px #b8b5b5;
- height: 40px;
- padding: 0;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.header h5 {
- margin: 0;
- padding: 0;
- color: white;
- font-size: 14px;
- font-weight: bold;
- line-height: 1;
- text-align: center;
-}
-
-.body {
- background-color: white;
- padding: 30px 30px 20px;
-}
-
-.form .formGroup {
- margin-bottom: 30px;
-}
-
-.form .formGroup:last-of-type {
- margin-bottom: 0;
-}
-
-.formControl {
- width: 100%;
- height: 40px;
- font-size: 14px;
- border: 1px solid #ccc;
- border-radius: 4px;
- padding: 6px 12px;
- box-sizing: border-box;
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
-}
-
-.formControl:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
-}
-
-.formControl::placeholder {
- color: #999;
-}
-
-.buttonContent {
- margin-top: 30px;
- text-align: center;
-}
-
-.button {
- background: #003A5D;
- color: white;
- width: 250px;
- height: 40px;
- font-size: 14px;
- font-weight: normal;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.2s;
- display: inline-block;
-}
-
-.button:hover {
- background: #00527d;
-}
-
-.button:disabled {
- background: #6c757d;
- cursor: not-allowed;
- opacity: 0.65;
-}
-
-.forgotPassword {
- text-align: center;
- margin-top: 15px;
- margin-bottom: 0;
-}
-
-.forgotPassword a {
- color: black;
- font-weight: bold;
- text-decoration: none;
- font-size: 13px;
-}
-
-.forgotPassword a:hover {
- text-decoration: underline;
-}
-
-.errorMessage {
- color: #dc3545;
- font-size: 13px;
- margin-top: 10px;
- margin-bottom: 10px;
- text-align: center;
-}
-
-.footer {
- text-align: center;
- margin-top: 20px;
- padding: 0 15px;
-}
-
-.footer a {
- color: white;
- text-decoration: none;
- margin: 0 3px;
- font-size: 13px;
-}
-
-.footer a:hover {
- text-decoration: underline;
-}
-
-.copyright {
- text-align: center;
- color: white;
- font-size: 12px;
- margin-top: 15px;
- padding-bottom: 20px;
-}
-
-.copyright em {
- font-style: italic;
-}
diff --git a/src/pages/calendar/api.js b/src/pages/calendar/api.js
deleted file mode 100644
index 15f741a1..00000000
--- a/src/pages/calendar/api.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { get, post } from "../../apiUtil";
-import { getAccessToken } from "../../tokenUtility";
-import { API_URL } from "../../constants";
-
-// Fetch events for calendar view (with date range)
-export async function fetchEventsForCalendar(start, end) {
- const params = new URLSearchParams();
- if (start) params.append("start", start.toISOString());
- if (end) params.append("end", end.toISOString());
-
- const response = await get(`${API_URL}/Calendar/GetEventsForCalendar?${params.toString()}`);
- return response?.data || response?.Data || [];
-}
-
-// Fetch events with pagination
-export async function fetchEventsPaged({ search = "", page = 1, pageSize = 12 }) {
- const params = new URLSearchParams();
- if (search) params.append("search", search);
- params.append("page", page.toString());
- params.append("pageSize", pageSize.toString());
-
- const response = await get(`${API_URL}/Calendar/GetEventList?${params.toString()}`);
-
- const data = response?.data || response?.Data || {};
- return {
- rows: data.rows || [],
- totalCount: data.totalCount || 0,
- };
-}
-
-// Get single event by ID
-export async function getEventById(id) {
- const response = await get(`${API_URL}/Calendar/GetEventById/${id}`);
- const event = response?.data || response?.Data;
-
- // Handle both camelCase and PascalCase
- if (event) {
- return {
- id: event.id || event.Id,
- title: event.title || event.Title || "",
- description: event.description || event.Description || "",
- location: event.location || event.Location || "",
- recordID: event.recordID || event.RecordID || null,
- recordName: event.recordName || event.RecordName || "",
- recordTargetType: event.recordTargetType || event.RecordTargetType || "",
- reminderMinutes: event.reminderMinutes || event.ReminderMinutes || 15,
- eventColor: event.eventColor || event.EventColor || "primary",
- startDate: event.startDate || event.StartDate || "",
- startTime: event.startTime || event.StartTime || "",
- endDate: event.endDate || event.EndDate || "",
- endTime: event.endTime || event.EndTime || "",
- allDay: event.allDay || event.AllDay || false,
- };
- }
- return null;
-}
-
-// Create new event
-export async function addEvent(eventData) {
- const response = await post(`${API_URL}/Calendar/AddEvent`, {
- Title: eventData.title,
- Description: eventData.description,
- Location: eventData.location,
- RecordID: eventData.recordID,
- RecordName: eventData.recordName,
- RecordTargetType: eventData.recordTargetType,
- ReminderMinutes: eventData.reminderMinutes,
- EventColor: eventData.eventColor,
- StartDate: eventData.startDate,
- StartTime: eventData.startTime,
- EndDate: eventData.endDate,
- EndTime: eventData.endTime,
- AllDay: eventData.allDay,
- });
- return response;
-}
-
-// Update existing event
-export async function editEvent(eventData) {
- const response = await post(`${API_URL}/Calendar/EditEvent`, {
- Id: eventData.id,
- Title: eventData.title,
- Description: eventData.description,
- Location: eventData.location,
- RecordID: eventData.recordID,
- RecordName: eventData.recordName,
- RecordTargetType: eventData.recordTargetType,
- ReminderMinutes: eventData.reminderMinutes,
- EventColor: eventData.eventColor,
- StartDate: eventData.startDate,
- StartTime: eventData.startTime,
- EndDate: eventData.endDate,
- EndTime: eventData.endTime,
- AllDay: eventData.allDay,
- });
- return response;
-}
-
-// Delete event
-export async function deleteEvent(id) {
- const response = await fetch(`${API_URL}/Calendar/DeleteEvent/${id}`, {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- Authorization: getAccessToken(),
- },
- });
-
- if (!response.ok) {
- throw new Error("Failed to delete event");
- }
-
- return response.json();
-}
-
diff --git a/src/pages/calendar/components/CalendarActionBar.css b/src/pages/calendar/components/CalendarActionBar.css
deleted file mode 100644
index 18b39add..00000000
--- a/src/pages/calendar/components/CalendarActionBar.css
+++ /dev/null
@@ -1,142 +0,0 @@
-.calendarActionBar {
- background: #f5f5f5;
- border-bottom: 1px solid #ddd;
- padding: 0;
- display: flex;
- justify-content: space-between;
- align-items: stretch;
- flex-wrap: wrap;
- margin-bottom: 0;
-}
-
-.calActionBarNav {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- align-items: stretch;
-}
-
-.calActionBarNavRight {
- margin-left: auto;
-}
-
-.calActionBarNav li {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.calActionBarLink {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 10px 15px;
- color: #333;
- text-decoration: none;
- font-size: 14px;
- transition: background-color 0.2s;
- white-space: nowrap;
- border: none;
- background: transparent;
-}
-
-.calActionBarLink:hover {
- background-color: #e8e8e8;
- color: #333;
-}
-
-.calActionBarLink i {
- font-size: 13px;
- color: #666;
-}
-
-.calActionBarText {
- font-weight: 400;
-}
-
-/* Dropdown Styles */
-.calActionBarDropdown {
- position: relative;
-}
-
-.calActionBarDropdown.open .calActionBarLink {
- background-color: #e0e0e0;
-}
-
-.calActionBarDropdownMenu {
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- min-width: 180px;
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 0 0 4px 4px;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
- list-style: none;
- margin: 0;
- padding: 5px 0;
-}
-
-.calActionBarDropdownMenu li {
- border: none;
- display: block;
-}
-
-.calActionBarDropdownMenu li a {
- display: block;
- padding: 8px 16px;
- color: #333;
- text-decoration: none;
- font-size: 14px;
- transition: background-color 0.2s;
-}
-
-.calActionBarDropdownMenu li a:hover {
- background-color: #f5f5f5;
- color: #333;
-}
-
-.calActionBarDropdownMenu li.divider {
- height: 1px;
- background-color: #e5e5e5;
- margin: 5px 0;
- padding: 0;
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .calendarActionBar {
- flex-direction: column;
- }
-
- .calActionBarNav {
- flex-wrap: wrap;
- }
-
- .calActionBarNavRight {
- margin-left: 0;
- }
-
- .calActionBarLink {
- padding: 10px 12px;
- font-size: 13px;
- }
-
- .calActionBarText {
- display: none;
- }
-
- .calActionBarLink i {
- margin-right: 0;
- }
-}
-
-@media (min-width: 769px) {
- /* Show text on larger screens */
- .calActionBarText {
- display: inline;
- }
-}
-
diff --git a/src/pages/calendar/components/CalendarActionBar.js b/src/pages/calendar/components/CalendarActionBar.js
deleted file mode 100644
index 26e1469c..00000000
--- a/src/pages/calendar/components/CalendarActionBar.js
+++ /dev/null
@@ -1,185 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import "./CalendarActionBar.css";
-
-export default function CalendarActionBar({
- onWorkspaceChange,
- onEventTypeChange,
- onTodayClick,
- onPrevClick,
- onNextClick,
- onViewChange,
-}) {
- const navigate = useNavigate();
- const [workspaceOpen, setWorkspaceOpen] = useState(false);
- const [eventTypeOpen, setEventTypeOpen] = useState(false);
- const [selectedWorkspace, setSelectedWorkspace] = useState("All");
- const [selectedEventType, setSelectedEventType] = useState("Events");
-
- const toggleWorkspace = (e) => {
- e.preventDefault();
- e.stopPropagation();
- setWorkspaceOpen(!workspaceOpen);
- setEventTypeOpen(false);
- };
-
- const toggleEventType = (e) => {
- e.preventDefault();
- e.stopPropagation();
- setEventTypeOpen(!eventTypeOpen);
- setWorkspaceOpen(false);
- };
-
- const handleWorkspaceSelect = (workspace, type) => {
- setSelectedWorkspace(workspace);
- setWorkspaceOpen(false);
- onWorkspaceChange && onWorkspaceChange(type);
- };
-
- const handleEventTypeSelect = (eventType, filter) => {
- setSelectedEventType(eventType);
- setEventTypeOpen(false);
- onEventTypeChange && onEventTypeChange(filter);
- };
-
- // Close dropdowns when clicking outside
- useEffect(() => {
- const handleClickOutside = () => {
- setWorkspaceOpen(false);
- setEventTypeOpen(false);
- };
-
- if (workspaceOpen || eventTypeOpen) {
- document.addEventListener("click", handleClickOutside);
- return () => document.removeEventListener("click", handleClickOutside);
- }
- }, [workspaceOpen, eventTypeOpen]);
-
- return (
-
-
-
- {/* Right Side Navigation */}
-
-
- );
-}
-
diff --git a/src/pages/calendar/form/EventFormPage.css b/src/pages/calendar/form/EventFormPage.css
deleted file mode 100644
index 2f5e43d9..00000000
--- a/src/pages/calendar/form/EventFormPage.css
+++ /dev/null
@@ -1,344 +0,0 @@
-.eventFormPage {
- padding: 0;
- background-color: #f5f5f5;
- min-height: 100vh;
-}
-
-.eventFormPage .container {
- max-width: 100%;
- margin: 0;
- padding: 0;
-}
-
-.eventFormPage .panel {
- background: white;
- border-radius: 4px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- margin-bottom: 20px;
- border: 1px solid #ddd;
-}
-
-.eventFormPage .panel-body {
- padding: 20px;
-}
-
-.eventFormPage .breadcrumb {
- background: transparent;
- padding: 0;
- margin-bottom: 20px;
-}
-
-.eventFormPage .breadcrumb li {
- display: inline;
- font-size: 14px;
-}
-
-.eventFormPage .breadcrumb li + li:before {
- content: "/ ";
- padding: 0 8px;
- color: #ccc;
-}
-
-.eventFormPage .breadcrumb a {
- color: #337ab7;
- text-decoration: none;
-}
-
-.eventFormPage .breadcrumb .active {
- color: #666;
-}
-
-.eventFormPage .heading {
- margin: 0 0 20px 0;
- color: #333;
-}
-
-.eventFormPage .form-group {
- margin-bottom: 20px;
- position: relative;
-}
-
-.eventFormPage .form-control {
- width: 100%;
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- height: auto;
- min-height: 42px;
-}
-
-.eventFormPage select.form-control {
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right 10px center;
- padding-right: 30px;
-}
-
-.eventFormPage .form-control:focus {
- outline: none;
- border-color: #66afe9;
- box-shadow: 0 0 5px rgba(102, 175, 233, 0.5);
-}
-
-.eventFormPage textarea.form-control {
- resize: vertical;
- min-height: 80px;
-}
-
-.eventFormPage label {
- display: block;
- margin-bottom: 5px;
- font-weight: 500;
- color: #333;
- font-size: 14px;
-}
-
-.eventFormPage .required-field::after {
- content: " *";
- color: #d9534f;
-}
-
-.eventFormPage .input-counter {
- position: absolute;
- right: 10px;
- top: 0;
- font-size: 12px;
- color: #999;
-}
-
-.eventFormPage .row {
- display: flex;
- margin: 0 -10px;
- flex-wrap: wrap;
- align-items: flex-start;
-}
-
-.eventFormPage .col-sm-5,
-.eventFormPage .col-sm-6,
-.eventFormPage .col-sm-2,
-.eventFormPage .col-md-4,
-.eventFormPage .col-md-6 {
- padding: 0 10px;
- margin-bottom: 15px;
-}
-
-.eventFormPage .col-sm-5 {
- flex: 0 0 40%;
- max-width: 40%;
-}
-
-.eventFormPage .col-sm-6 {
- flex: 0 0 50%;
- max-width: 50%;
-}
-
-.eventFormPage .col-sm-2 {
- flex: 0 0 10%;
- max-width: 10%;
- display: flex;
- align-items: center;
- justify-content: center;
- padding-top: 10px;
-}
-
-.eventFormPage .col-sm-2 h5 {
- margin: 0;
- font-size: 14px;
- font-weight: normal;
- color: #666;
-}
-
-.eventFormPage .col-md-6 {
- flex: 0 0 50%;
- max-width: 50%;
-}
-
-.eventFormPage hr {
- border: 0;
- border-top: 1px solid #eee;
- margin: 30px 0;
-}
-
-.eventFormPage .btn {
- padding: 10px 24px;
- border: none;
- border-radius: 4px;
- font-size: 16px;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.eventFormPage .btn-success {
- background-color: #5cb85c;
- color: white;
-}
-
-.eventFormPage .btn-success:hover:not(:disabled) {
- background-color: #449d44;
-}
-
-.eventFormPage .btn-default {
- background-color: #f5f5f5;
- color: #333;
- border: 1px solid #ccc;
-}
-
-.eventFormPage .btn-default:hover:not(:disabled) {
- background-color: #e6e6e6;
-}
-
-.eventFormPage .btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.eventFormPage .text-right {
- text-align: right;
-}
-
-.eventFormPage .text-uppercase {
- text-transform: uppercase;
-}
-
-.eventFormPage .text-pad-right {
- margin-right: 15px;
-}
-
-.eventFormPage .text-sm {
- font-size: 12px;
- color: #666;
-}
-
-
-
-
-/* Color Picker Styles */
-.eventFormPage .theme-picker {
- margin-top: 10px;
-}
-
-.eventFormPage .theme-colors {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- gap: 10px;
-}
-
-.eventFormPage .theme-colors li {
- margin: 0;
-}
-
-.eventFormPage .theme-colors a {
- display: block;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- border: 3px solid transparent;
- transition: all 0.2s;
- cursor: pointer;
-}
-
-.eventFormPage .theme-colors li.active a {
- border-color: #333;
- box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
-}
-
-.eventFormPage .theme-colors a.primary {
- background-color: #337ab7;
-}
-
-.eventFormPage .theme-colors a.success {
- background-color: #5cb85c;
-}
-
-.eventFormPage .theme-colors a.info {
- background-color: #5bc0de;
-}
-
-.eventFormPage .theme-colors a.warning {
- background-color: #f0ad4e;
-}
-
-.eventFormPage .theme-colors a.danger {
- background-color: #d9534f;
-}
-
-/* Checkbox Styles */
-.eventFormPage .checkbox.stylized {
- margin-top: 15px;
-}
-
-.eventFormPage .checkbox.stylized label {
- display: flex;
- align-items: center;
- cursor: pointer;
- font-weight: normal;
-}
-
-.eventFormPage .checkbox.stylized input[type="checkbox"] {
- margin-right: 8px;
- width: 18px;
- height: 18px;
- cursor: pointer;
-}
-
-/* Panel Placeholder for Related Record */
-.eventFormPage .panel-placeholder {
- border: 2px dashed #ddd;
- border-radius: 4px;
- padding: 20px;
- background-color: #fafafa;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.eventFormPage .panel-placeholder:hover {
- border-color: #337ab7;
- background-color: #f0f7ff;
-}
-
-.eventFormPage .panel-placeholder .panel-body {
- padding: 0;
-}
-
-.eventFormPage .panel-placeholder p {
- margin: 0;
-}
-
-.eventFormPage .text-muted {
- color: #999;
-}
-
-.eventFormPage .text-primary {
- color: #337ab7;
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .eventFormPage .col-sm-5,
- .eventFormPage .col-sm-6,
- .eventFormPage .col-sm-2,
- .eventFormPage .col-md-4,
- .eventFormPage .col-md-6 {
- flex: 0 0 100%;
- max-width: 100%;
- }
-
- .eventFormPage .col-sm-2 {
- justify-content: flex-start;
- padding-top: 0;
- }
-
- .eventFormPage .col-sm-2 h5 {
- margin: 10px 0;
- }
-
- .eventFormPage .panel-body {
- padding: 15px;
- }
-}
diff --git a/src/pages/calendar/form/EventFormPage.js b/src/pages/calendar/form/EventFormPage.js
deleted file mode 100644
index a7aa5341..00000000
--- a/src/pages/calendar/form/EventFormPage.js
+++ /dev/null
@@ -1,456 +0,0 @@
-import { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router-dom";
-import { toast } from "react-toastify";
-import { getEventById, addEvent, editEvent } from "../api";
-import "./EventFormPage.css";
-
-export default function EventFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- title: "",
- description: "",
- location: "",
- recordID: null,
- recordName: "",
- recordTargetType: "",
- reminderMinutes: 15,
- eventColor: "primary",
- startDate: new Date().toISOString().split("T")[0],
- startTime: "08:00 AM",
- endDate: new Date().toISOString().split("T")[0],
- endTime: "08:30 AM",
- allDay: false,
- });
-
- const [loading, setLoading] = useState(false);
- const [titleCounter, setTitleCounter] = useState(100);
- const [locationCounter, setLocationCounter] = useState(150);
-
- // Time options for dropdowns (30-minute intervals)
- const timeOptions = [
- "12:00 AM", "12:30 AM", "01:00 AM", "01:30 AM", "02:00 AM", "02:30 AM",
- "03:00 AM", "03:30 AM", "04:00 AM", "04:30 AM", "05:00 AM", "05:30 AM",
- "06:00 AM", "06:30 AM", "07:00 AM", "07:30 AM", "08:00 AM", "08:30 AM",
- "09:00 AM", "09:30 AM", "10:00 AM", "10:30 AM", "11:00 AM", "11:30 AM",
- "12:00 PM", "12:30 PM", "01:00 PM", "01:30 PM", "02:00 PM", "02:30 PM",
- "03:00 PM", "03:30 PM", "04:00 PM", "04:30 PM", "05:00 PM", "05:30 PM",
- "06:00 PM", "06:30 PM", "07:00 PM", "07:30 PM", "08:00 PM", "08:30 PM",
- "09:00 PM", "09:30 PM", "10:00 PM", "10:30 PM", "11:00 PM", "11:30 PM",
- ];
-
- // Reminder options
- const reminderOptions = [
- { value: 0, label: "Never" },
- { value: 5, label: "5 minutes before" },
- { value: 10, label: "10 minutes before" },
- { value: 15, label: "15 minutes before" },
- { value: 30, label: "30 minutes before" },
- { value: 60, label: "1 hour before" },
- { value: 120, label: "2 hours before" },
- { value: 240, label: "4 hours before" },
- { value: 360, label: "6 hours before" },
- { value: 720, label: "12 hours before" },
- { value: 1440, label: "1 day before" },
- ];
-
- // Load event data if editing
- useEffect(() => {
- if (isEdit && id) {
- const loadEvent = async () => {
- try {
- const event = await getEventById(id);
- if (event) {
- setFormData(prev => {
- const merged = { ...prev };
- for (const key of Object.keys(merged)) {
- if (event[key] != null) merged[key] = event[key];
- }
- return merged;
- });
- setTitleCounter(100 - (event.title?.length || 0));
- setLocationCounter(150 - (event.location?.length || 0));
- }
- } catch (err) {
- console.error("Failed to load event", err);
- toast.error("Failed to load event");
- }
- };
- loadEvent();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value, type, checked } = e.target;
- const newValue = type === "checkbox" ? checked : value;
-
- setFormData((prev) => ({
- ...prev,
- [name]: newValue,
- }));
-
- // Update character counters
- if (name === "title") {
- setTitleCounter(100 - value.length);
- } else if (name === "location") {
- setLocationCounter(150 - value.length);
- }
- };
-
- const handleColorSelect = (color) => {
- setFormData((prev) => ({
- ...prev,
- eventColor: color,
- }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- if (isEdit) {
- await editEvent({ ...formData, id: parseInt(id) });
- toast.success("Event updated successfully!");
- } else {
- await addEvent(formData);
- toast.success("Event created successfully!");
- }
- navigate("/calendar");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save event");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate("/calendar");
- };
-
- return (
-
-
-
-
-
- -
- Calendar
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
- );
-}
-
-
diff --git a/src/pages/calendar/list/CalendarPage.css b/src/pages/calendar/list/CalendarPage.css
deleted file mode 100644
index 8f6a17e1..00000000
--- a/src/pages/calendar/list/CalendarPage.css
+++ /dev/null
@@ -1,178 +0,0 @@
-.calendarPage {
- padding: 20px;
- background-color: #f5f5f5;
- min-height: calc(100vh - 60px);
-}
-
-.calendarPage .breadcrumb {
- font-size: 14px;
- margin-bottom: 20px;
- color: #666;
-}
-
-.calendarPage .breadcrumbItem {
- color: #333;
- font-weight: 500;
-}
-
-/* Calendar Container */
-.calendarContainer {
- background: white;
- padding: 20px;
- border-radius: 4px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- position: relative;
-}
-
-.calendarLoading {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- z-index: 100;
- text-align: center;
- color: #999;
-}
-
-.calendarLoading i {
- margin-bottom: 10px;
-}
-
-.calendarLoading p {
- margin: 0;
- font-size: 14px;
-}
-
-/* Calendar Title */
-.calendarTitle {
- text-align: center;
- margin-bottom: 20px;
- padding-bottom: 15px;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.calendarTitle h2 {
- margin: 0;
- font-size: 24px;
- font-weight: 500;
- color: #666;
-}
-
-/* FullCalendar Customization */
-.calendarPage .fc {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-}
-
-/* Calendar cells */
-.calendarPage .fc-daygrid-day {
- border-color: #ddd;
-}
-
-.calendarPage .fc-scrollgrid {
- border-color: #ddd;
-}
-
-.calendarPage .fc-theme-standard td,
-.calendarPage .fc-theme-standard th {
- border-color: #ddd;
-}
-
-.calendarPage .fc-toolbar-title {
- font-size: 1.5rem;
- font-weight: 600;
- color: #333;
-}
-
-.calendarPage .fc-button {
- background: #337ab7;
- border-color: #337ab7;
- text-transform: capitalize;
-}
-
-.calendarPage .fc-button:hover {
- background: #286090;
- border-color: #286090;
-}
-
-.calendarPage .fc-button-active {
- background: #204d74 !important;
- border-color: #204d74 !important;
-}
-
-.calendarPage .fc-button:focus {
- box-shadow: 0 0 0 2px rgba(51, 122, 183, 0.2);
-}
-
-.calendarPage .fc-daygrid-event {
- cursor: pointer;
- margin: 1px 2px;
- padding: 2px 4px;
- border-radius: 3px;
-}
-
-.calendarPage .fc-daygrid-event:hover {
- opacity: 0.85;
-}
-
-.calendarPage .fc-event-title {
- font-weight: 500;
-}
-
-.calendarPage .fc-daygrid-day-number {
- padding: 8px;
- font-size: 14px;
- color: #333;
-}
-
-.calendarPage .fc-col-header-cell-cushion {
- padding: 10px;
- font-weight: 600;
- color: #666;
-}
-
-/* Today's date - Pinkish/Beige highlight like legacy */
-.calendarPage .fc-daygrid-day.fc-day-today {
- background-color: #fffbf0 !important;
-}
-
-.calendarPage .fc-daygrid-day.fc-day-today .fc-daygrid-day-number {
- background-color: #fff8dc;
- color: #333;
- font-weight: 600;
-}
-
-/* Dates from other months - Gray background like legacy */
-.calendarPage .fc-day-other {
- background-color: #f0f0f0 !important;
-}
-
-.calendarPage .fc-day-other .fc-daygrid-day-number {
- color: #bbb !important;
-}
-
-/* Hide events on dates from other months */
-.calendarPage .fc-day-other .fc-daygrid-event-harness {
- display: none !important;
-}
-
-
-/* Responsive Styles */
-@media (max-width: 768px) {
- .calendarPage {
- padding: 10px;
- }
-
- .calendarContainer {
- padding: 10px;
- }
-
- .calendarTitle h2 {
- font-size: 18px;
- }
-
- .calendarPage .fc-daygrid-day-number {
- font-size: 12px;
- padding: 4px;
- }
-}
-
diff --git a/src/pages/calendar/list/CalendarPage.js b/src/pages/calendar/list/CalendarPage.js
deleted file mode 100644
index 97e4475e..00000000
--- a/src/pages/calendar/list/CalendarPage.js
+++ /dev/null
@@ -1,250 +0,0 @@
-import { useState, useEffect, useRef } from "react";
-import { useNavigate } from "react-router-dom";
-import { toast } from "react-toastify";
-import FullCalendar from "@fullcalendar/react";
-import dayGridPlugin from "@fullcalendar/daygrid";
-import timeGridPlugin from "@fullcalendar/timegrid";
-import interactionPlugin from "@fullcalendar/interaction";
-import { fetchEventsForCalendar, deleteEvent } from "../api";
-import DeleteModal from "../../../components/DeleteModal";
-import CalendarActionBar from "../components/CalendarActionBar";
-import "./CalendarPage.css";
-
-export default function CalendarPage() {
- const navigate = useNavigate();
- const calendarRef = useRef(null);
-
- const [events, setEvents] = useState([]);
- const [loading, setLoading] = useState(false);
- const [eventTypeFilter, setEventTypeFilter] = useState("Events");
- const [workspaceFilter, setWorkspaceFilter] = useState("All");
- const [currentTitle, setCurrentTitle] = useState("March 2026");
- const [deleteModal, setDeleteModal] = useState({
- isOpen: false,
- item: null,
- loading: false,
- });
-
- // Load events for the current calendar view
- const loadEvents = async (start, end) => {
- setLoading(true);
- try {
- const data = await fetchEventsForCalendar(start, end);
-
- // Convert to FullCalendar format
- const formattedEvents = data.map((event) => {
- // Extract date part only (YYYY-MM-DD) from datetime string
- const startDateOnly = event.startDate?.split('T')[0] || event.startDate?.substring(0, 10);
- const endDateOnly = event.endDate?.split('T')[0] || event.endDate?.substring(0, 10);
-
- let startValue, endValue;
-
- if (event.allDay) {
- // For all-day events, FullCalendar uses exclusive end dates
- // So if event is on April 5th, end should be April 6th
- startValue = startDateOnly;
- const endDate = new Date(endDateOnly);
- endDate.setDate(endDate.getDate() + 1); // Add 1 day for exclusive end
- endValue = endDate.toISOString().split('T')[0];
- } else {
- // For timed events, combine date and time
- startValue = `${startDateOnly}T${convertTimeToISO(event.startTime)}`;
- endValue = `${endDateOnly}T${convertTimeToISO(event.endTime)}`;
- }
-
- return {
- id: event.id,
- title: event.title,
- start: startValue,
- end: endValue,
- allDay: event.allDay,
- backgroundColor: getColorHex(event.eventColor),
- borderColor: getColorHex(event.eventColor),
- extendedProps: {
- description: event.description,
- location: event.location,
- eventColor: event.eventColor,
- rawEvent: event,
- },
- };
- });
-
- setEvents(formattedEvents);
- } catch (err) {
- console.error("Failed to load events", err);
- toast.error("Failed to load events");
- } finally {
- setLoading(false);
- }
- };
-
- // Convert "08:00 AM" to "08:00:00"
- const convertTimeToISO = (timeStr) => {
- if (!timeStr) return "00:00:00";
- const [time, period] = timeStr.split(" ");
- let [hours, minutes] = time.split(":");
- hours = parseInt(hours);
-
- if (period === "PM" && hours !== 12) {
- hours += 12;
- } else if (period === "AM" && hours === 12) {
- hours = 0;
- }
-
- return `${hours.toString().padStart(2, "0")}:${minutes}:00`;
- };
-
- // Get color hex from color name
- const getColorHex = (colorName) => {
- const colors = {
- primary: "#337ab7",
- success: "#5cb85c",
- info: "#5bc0de",
- warning: "#f0ad4e",
- danger: "#d9534f",
- };
- return colors[colorName] || colors.primary;
- };
-
- // Initial load
- useEffect(() => {
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) {
- const start = calendarApi.view.activeStart;
- const end = calendarApi.view.activeEnd;
- loadEvents(start, end);
- }
- }, [eventTypeFilter, workspaceFilter]);
-
- // Handle date range change (when user navigates to different month/week)
- const handleDatesSet = (dateInfo) => {
- loadEvents(dateInfo.start, dateInfo.end);
-
- // Update title
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) {
- setCurrentTitle(calendarApi.view.title);
- }
- };
-
- // Handle event click
- const handleEventClick = (clickInfo) => {
- const eventId = clickInfo.event.id;
- navigate(`/calendar/edit/${eventId}`);
- };
-
- // Handle date click (create new event)
- const handleDateClick = (dateInfo) => {
- // Navigate to create event with pre-filled date
- navigate(`/calendar/new?date=${dateInfo.dateStr}`);
- };
-
- // Handle delete
- const handleDeleteConfirm = async () => {
- setDeleteModal((prev) => ({ ...prev, loading: true }));
- try {
- await deleteEvent(deleteModal.item.id);
- toast.success("Event deleted successfully!");
- setDeleteModal({ isOpen: false, item: null, loading: false });
-
- // Reload events
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) {
- const start = calendarApi.view.activeStart;
- const end = calendarApi.view.activeEnd;
- loadEvents(start, end);
- }
- } catch (err) {
- console.error(err);
- toast.error("Failed to delete event");
- setDeleteModal((prev) => ({ ...prev, loading: false }));
- }
- };
-
- // Navigate to today
- const handleToday = () => {
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) {
- calendarApi.today();
- }
- };
-
- // Change view
- const handleChangeView = (viewType) => {
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) {
- calendarApi.changeView(viewType);
- }
- };
-
-
- return (
- <>
- setWorkspaceFilter(type)}
- onEventTypeChange={(filter) => setEventTypeFilter(filter)}
- onTodayClick={handleToday}
- onPrevClick={() => {
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) calendarApi.prev();
- }}
- onNextClick={() => {
- const calendarApi = calendarRef.current?.getApi();
- if (calendarApi) calendarApi.next();
- }}
- onViewChange={handleChangeView}
- />
-
-
- {/* Calendar Container */}
-
- {loading && (
-
- )}
-
- {/* Custom Title Display */}
-
-
{currentTitle}
-
-
-
-
-
- setDeleteModal({ isOpen: false, item: null, loading: false })}
- onConfirm={handleDeleteConfirm}
- title="Delete Event"
- message={`Are you sure you want to delete "${deleteModal.item?.title}"? This action cannot be undone.`}
- loading={deleteModal.loading}
- />
-
- >
- );
-}
-
diff --git a/src/pages/calendar/list/Table.js b/src/pages/calendar/list/Table.js
deleted file mode 100644
index 84ec4c2d..00000000
--- a/src/pages/calendar/list/Table.js
+++ /dev/null
@@ -1,163 +0,0 @@
-export default function Table({
- loading,
- error,
- rows,
- onEdit,
- onDelete,
- page,
- setPage,
- pageSize,
- setPageSize,
- totalCount,
- totalPages,
-}) {
- if (loading) {
- return (
-
- );
- }
-
- if (error) {
- return (
-
- );
- }
-
- if (rows.length === 0) {
- return (
-
-
-
-
No events found
-
Create your first event to get started
-
-
- );
- }
-
- const formatDate = (dateString) => {
- if (!dateString) return "";
- const date = new Date(dateString);
- return date.toLocaleDateString("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- });
- };
-
- const getColorBadgeClass = (color) => {
- return `colorBadge badge-${color || "primary"}`;
- };
-
- return (
-
-
-
-
- | Title |
- Start Date |
- End Date |
- Location |
- Color |
- All Day |
- Actions |
-
-
-
- {rows.map((row) => (
-
- |
- {row.title}
- {row.description && (
- {row.description.substring(0, 100)}
- )}
- |
-
- {formatDate(row.startDate)}
- {row.startTime && {row.startTime} }
- |
-
- {formatDate(row.endDate)}
- {row.endTime && {row.endTime} }
- |
- {row.location || "-"} |
-
-
- {row.eventColor || "primary"}
-
- |
- {row.allDay ? "Yes" : "No"} |
-
-
-
-
-
- |
-
- ))}
-
-
-
-
-
- Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, totalCount)} of{" "}
- {totalCount} events
-
-
-
-
-
-
-
-
- Page {page} of {totalPages}
-
-
-
-
-
- );
-}
-
diff --git a/src/pages/calendar/list/useEvents.js b/src/pages/calendar/list/useEvents.js
deleted file mode 100644
index a2668855..00000000
--- a/src/pages/calendar/list/useEvents.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useState, useEffect, useCallback, useMemo } from "react";
-import { fetchEventsPaged } from "../api";
-
-const useEvents = () => {
- const [rows, setRows] = useState([]);
- const [search, setSearch] = useState("");
- const [page, setPage] = useState(1);
- const [pageSize, setPageSize] = useState(12);
- const [totalCount, setTotalCount] = useState(0);
- const [loading, setLoading] = useState(true);
- const [err, setErr] = useState("");
-
- const reload = useCallback(() => {
- setLoading(true);
- setErr("");
- fetchEventsPaged({ search, page, pageSize })
- .then(({ rows, totalCount }) => {
- setRows(rows);
- setTotalCount(totalCount);
- })
- .catch((e) => setErr(e.message || "Failed to load"))
- .finally(() => setLoading(false));
- }, [search, page, pageSize]);
-
- useEffect(() => {
- reload();
- }, [reload]);
-
- return useMemo(
- () => ({
- rows,
- search,
- setSearch,
- page,
- setPage,
- pageSize,
- setPageSize,
- totalCount,
- loading,
- err,
- reload,
- }),
- [rows, search, page, pageSize, totalCount, loading, err, reload]
- );
-};
-
-export default useEvents;
-
diff --git a/src/pages/contacts/edit/api.js b/src/pages/contacts/edit/api.js
deleted file mode 100644
index e71d8822..00000000
--- a/src/pages/contacts/edit/api.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import {post } from "../../../apiUtil";
-import { API_URL } from '../../../constants';
-
-export const editContact = (item) => post(`${API_URL}/Contact/EditContacts`, item)
\ No newline at end of file
diff --git a/src/pages/contacts/form/ContactFormPage.js b/src/pages/contacts/form/ContactFormPage.js
deleted file mode 100644
index 35dffe11..00000000
--- a/src/pages/contacts/form/ContactFormPage.js
+++ /dev/null
@@ -1,673 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-import "../../../styles/FormPage.css";
-
-const ContactFormPage = () => {
- const navigate = useNavigate();
- const { id } = useParams();
- const isEdit = Boolean(id);
-
- const [formData, setFormData] = useState({
- firstName: "",
- middleName: "",
- lastName: "",
- title: "",
- contactType: "",
- gender: "",
- dateOfBirth: "",
- territory: "",
- owner: "",
- privacySetting: "PublicReadWrite",
- about: "",
- tags: "",
- street: "",
- streetLineTwo: "",
- city: "",
- zip: "",
- country: "US",
- state: "",
- county: "",
- timeZone: "Eastern Standard Time",
- smsAddress: "",
- website: "",
- facebookUrl: "",
- linkedInUrl: "",
- twitterUrl: "",
- });
-
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (isEdit) {
- // TODO: Fetch contact data by ID
- // For now, using mock data
- setFormData({
- firstName: "John",
- middleName: "A",
- lastName: "Doe",
- title: "Manager",
- contactType: "",
- gender: "",
- dateOfBirth: "",
- territory: "",
- owner: "",
- privacySetting: "PublicReadWrite",
- about: "",
- tags: "",
- street: "",
- streetLineTwo: "",
- city: "",
- zip: "",
- country: "US",
- state: "",
- county: "",
- timeZone: "Eastern Standard Time",
- smsAddress: "",
- website: "",
- facebookUrl: "",
- linkedInUrl: "",
- twitterUrl: "",
- });
- }
- }, [isEdit, id]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData((prev) => ({
- ...prev,
- [name]: value,
- }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- // TODO: Implement actual save logic
- console.log("Saving contact:", formData);
- setTimeout(() => {
- setLoading(false);
- navigate("/contacts");
- }, 1000);
- } catch (error) {
- console.error("Error saving contact:", error);
- setLoading(false);
- }
- };
-
- // eslint-disable-next-line no-unused-vars
- const handleCancel = () => {
- navigate("/contacts");
- };
-
- return (
-
-
-
-
- -
- {
- e.preventDefault();
- navigate("/contacts");
- }}
- >
- Contacts
-
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
-
- );
-};
-
-export default ContactFormPage;
-
diff --git a/src/pages/contacts/list/Delete.js b/src/pages/contacts/list/Delete.js
deleted file mode 100644
index 394ae1a9..00000000
--- a/src/pages/contacts/list/Delete.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { deleteContact } from "./api";
-
-export default function DeleteModal({ open, item, onClose, onSuccess }) {
- if(!open) return null;
- const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
- return (
-
-
-
-
-
-
Are you sure you want to delete this contact?
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/contacts/list/Form.js b/src/pages/contacts/list/Form.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/contacts/list/List.js b/src/pages/contacts/list/List.js
deleted file mode 100644
index e087c962..00000000
--- a/src/pages/contacts/list/List.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { useState } from "react";
-import { useNavigate } from "react-router";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchContactsPaged } from "./api";
-import Table from "./Table";
-import ActionBar from "../../../components/ActionBar";
-import "./styles.css";
-
-export default function ContactsList() {
- const navigate = useNavigate();
- const { rows, search, setSearch, page, setPage, pageSize, setPageSize,
- totalCount, loading, err } = usePaginatedList(fetchContactsPaged);
-
- const [viewMode, setViewMode] = useState("list");
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- return (
- <>
-
navigate("/contacts/new")}
- onBulkEdit={() => console.log("Bulk edit")}
- selectedCount={0}
- />
-
-
-
- Contacts
-
-
-
-
-
-
-
-
-
- Search for Contacts
-
- Use the settings menu to adjust how to filter the list, and which columns to display.
-
-
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Enter search criteria here..."
- />
-
-
-
-
-
-
-
-
-
-
-
- {page}
-
-
-
-
-
-
- navigate(`/contacts/edit/${item.id}`)}
- onDelete={(item) => console.log("Delete", item)}
- page={page} setPage={setPage}
- pageSize={pageSize} setPageSize={setPageSize}
- totalCount={totalCount} totalPages={totalPages} />
-
- >
- );
-}
diff --git a/src/pages/contacts/list/Table.js b/src/pages/contacts/list/Table.js
deleted file mode 100644
index 9c48c523..00000000
--- a/src/pages/contacts/list/Table.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-export default function Table({ loading, error, rows, onEdit, onDelete,
- page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
-
- const columns = [
- { key: "name", label: "Name", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
- { key: "account", label: "Account" },
- { key: "title", label: "Title" },
- { key: "email", label: "Email" },
- { key: "phoneNumber", label: "Phone" },
- { key: "owner", label: "Owner" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/contacts/list/api.js b/src/pages/contacts/list/api.js
deleted file mode 100644
index 726de4d9..00000000
--- a/src/pages/contacts/list/api.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { get, post } from "../../../apiUtil";
-import { API_URL } from '../../../constants';
-
-
-export const fetchContactsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
- const res = await get(`${API_URL}/Contact/GetContactList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id,
- name: x.name,
- account: x.account ?? "",
- title: x.title ?? "",
- email: x.email ?? "",
- phoneNumber: x.phone ?? "",
- owner: x.owner ?? "",
- }));
- return {
- rows,
- page: res?.pageNumber ?? 1,
- pageSize: res?.pageSize ?? pageSize,
- totalCount: res?.totalCount ?? rows.length,
- totalPages: res?.totalPages ?? 1,
- };
-};
-
-
-export const deleteContact = (id) =>
- get(`/api/Contact/DeleteContacts?id=${id}`);
diff --git a/src/pages/contacts/list/styles.css b/src/pages/contacts/list/styles.css
deleted file mode 100644
index aa78320a..00000000
--- a/src/pages/contacts/list/styles.css
+++ /dev/null
@@ -1,231 +0,0 @@
-/* Modal styles for Contacts module */
-.modalRoot{position:fixed;inset:0;z-index:1000;}
-.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
-.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/contacts/new/api.js b/src/pages/contacts/new/api.js
deleted file mode 100644
index dd9639f8..00000000
--- a/src/pages/contacts/new/api.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import {post } from "../../../apiUtil";
-import { API_URL } from '../../../constants';
-
-export const addContact = (item) => post(`${API_URL}/Contact/AddContacts`, item);
\ No newline at end of file
diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css
deleted file mode 100644
index 18845d08..00000000
--- a/src/pages/dashboard/Dashboard.css
+++ /dev/null
@@ -1,70 +0,0 @@
-/* Dashboard Styles */
-
-.dashboard-container {
- padding: 20px;
-}
-
-.kpi-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
-}
-
-.kpi-card {
- background: white;
- border-radius: 8px;
- padding: 20px;
- text-align: center;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- transition: transform 0.2s, box-shadow 0.2s;
-}
-
-.kpi-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
-}
-
-.kpi-label {
- font-size: 12px;
- color: #666;
- margin-bottom: 8px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.kpi-value {
- font-size: 32px;
- font-weight: bold;
- color: #003a5d;
-}
-
-.analytics-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
-}
-
-.analysis-card {
- background: white;
- border-radius: 8px;
- padding: 20px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.analysis-card h3 {
- margin: 0 0 20px 0;
- font-size: 16px;
- font-weight: 600;
- color: #333;
-}
-
-@media (max-width: 768px) {
- .analytics-grid {
- grid-template-columns: 1fr;
- }
-
- .kpi-grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
diff --git a/src/pages/dashboard/Dashboard.js b/src/pages/dashboard/Dashboard.js
deleted file mode 100644
index 80d16d53..00000000
--- a/src/pages/dashboard/Dashboard.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { getDashboardKPIs, getPriorityAnalysis, getInvoiceAnalysis } from "./api";
-import "./Dashboard.css";
-
-export default function Dashboard() {
- const [kpis, setKpis] = useState(null);
- const [priorityData, setPriorityData] = useState([]);
- const [invoiceData, setInvoiceData] = useState([]);
- const [loading, setLoading] = useState(true);
-
- useEffect(() => {
- const loadDashboard = async () => {
- try {
- const [kpiRes, priorityRes, invoiceRes] = await Promise.all([
- getDashboardKPIs(),
- getPriorityAnalysis(),
- getInvoiceAnalysis(),
- ]);
- setKpis(kpiRes);
- setPriorityData(priorityRes);
- setInvoiceData(invoiceRes);
- } catch (err) {
- console.error("Failed to load dashboard", err);
- } finally {
- setLoading(false);
- }
- };
-
- loadDashboard();
- }, []);
-
- if (loading) return Loading dashboard...
;
-
- return (
-
- Dashboard
-
- {/* KPI Cards */}
-
-
-
Total Accounts
-
{kpis?.totalAccounts}
-
-
-
Active Work Orders
-
{kpis?.activeWorkOrders}
-
-
-
Completed Orders
-
{kpis?.completedOrders}
-
-
-
Pending Follow-ups
-
{kpis?.pendingFollowUps}
-
-
-
- {/* Analytics Tables */}
-
- {/* Priority Analysis */}
-
-
Priority Analysis
-
-
-
- | Priority |
- Count |
- % |
-
-
-
- {priorityData.map((row, idx) => (
-
- |
-
- {row.priority}
-
- |
- {row.count} |
- {row.percentage}% |
-
- ))}
-
-
-
-
- {/* Invoice Analysis */}
-
-
Invoice Analysis
-
-
-
- | Status |
- Amount |
- Count |
-
-
-
- {invoiceData.map((row, idx) => (
-
- |
-
- {row.status}
-
- |
- ${row.amount.toLocaleString()} |
- {row.count} |
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/src/pages/dashboard/api.js b/src/pages/dashboard/api.js
deleted file mode 100644
index 10aa4a3a..00000000
--- a/src/pages/dashboard/api.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// Mock dashboard data
-export const getDashboardKPIs = async () => {
- await new Promise(resolve => setTimeout(resolve, 300));
- return {
- totalAccounts: 24,
- activeWorkOrders: 12,
- completedOrders: 89,
- pendingFollowUps: 7,
- };
-};
-
-export const getPriorityAnalysis = async () => {
- await new Promise(resolve => setTimeout(resolve, 300));
- return [
- { priority: "Urgent", count: 3, percentage: 15 },
- { priority: "High", count: 5, percentage: 25 },
- { priority: "Medium", count: 8, percentage: 40 },
- { priority: "Low", count: 4, percentage: 20 },
- ];
-};
-
-export const getInvoiceAnalysis = async () => {
- await new Promise(resolve => setTimeout(resolve, 300));
- return [
- { status: "Paid", amount: 15000, count: 8 },
- { status: "Pending", amount: 8500, count: 5 },
- { status: "Overdue", amount: 3200, count: 2 },
- ];
-};
diff --git a/src/pages/employees/api.js b/src/pages/employees/api.js
deleted file mode 100644
index d2e944cb..00000000
--- a/src/pages/employees/api.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import { employeeApi } from "../../services/api";
-
-// Transform backend data to frontend format
-const transformEmployee = (employee) => {
- if (!employee) return null;
-
- return {
- id: employee.id,
- name: `${employee.firstName || ""} ${employee.lastName || ""}`.trim(),
- email: employee.email || "",
- phone: employee.phone || "",
- jobTitle: employee.jobTitle || "",
- role: employee.role || "",
- status: employee.status || "",
- hireDate: employee.hireDate || "",
- address: employee.address || "",
- department: employee.department || "",
- // Include all other fields for form editing
- firstName: employee.firstName || "",
- middleName: employee.middleName || "",
- lastName: employee.lastName || "",
- dateOfBirth: employee.dateOfBirth || "",
- street: employee.street || "",
- streetLineTwo: employee.streetLineTwo || "",
- city: employee.city || "",
- state: employee.state || "",
- zipCode: employee.zipCode || "",
- country: employee.country || "US",
- county: employee.county || "",
- timeZone: employee.timeZone || "Eastern Standard Time",
- gender: employee.gender || "",
- employeeNumber: employee.employeeNumber || "",
- regionName: employee.regionName || "",
- userName: employee.userName || "",
- smsAddress: employee.smsAddress || "",
- website: employee.website || "",
- facebookUrl: employee.facebookUrl || "",
- linkedInUrl: employee.linkedInUrl || "",
- twitterUrl: employee.twitterUrl || "",
- };
-};
-
-// Transform frontend data to backend format
-const transformToBackend = (employee) => {
- return {
- id: employee.id,
- firstName: employee.firstName || "",
- middleName: employee.middleName || null,
- lastName: employee.lastName || "",
- email: employee.email || "",
- phone: employee.phone || "",
- jobTitle: employee.jobTitle || "",
- role: employee.role || "Technician",
- status: employee.status || "Active",
- hireDate: employee.hireDate || null,
- dateOfBirth: employee.dateOfBirth || null,
- address: employee.address || "",
- street: employee.street || "",
- streetLineTwo: employee.streetLineTwo || null,
- city: employee.city || "",
- state: employee.state || "",
- zipCode: employee.zipCode || "",
- country: employee.country || "US",
- county: employee.county || null,
- department: employee.department || "Field Service",
- timeZone: employee.timeZone || "Eastern Standard Time",
- gender: employee.gender || null,
- employeeNumber: employee.employeeNumber || null,
- regionName: employee.regionName || null,
- userName: employee.userName || employee.email || "",
- smsAddress: employee.smsAddress || null,
- website: employee.website || null,
- facebookUrl: employee.facebookUrl || null,
- linkedInUrl: employee.linkedInUrl || null,
- twitterUrl: employee.twitterUrl || null,
- };
-};
-
-export const getEmployees = async (params = {}) => {
- try {
- const response = await employeeApi.getAll(params);
- // Handle both paginated and non-paginated responses
- const employees = response.data || response;
- return Array.isArray(employees) ? employees.map(transformEmployee) : [];
- } catch (error) {
- console.error("Error fetching employees:", error);
- throw error;
- }
-};
-
-export const getEmployeeById = async (id) => {
- try {
- const employee = await employeeApi.getById(id);
- return transformEmployee(employee);
- } catch (error) {
- console.error("Error fetching employee:", error);
- throw error;
- }
-};
-
-export const addEmployee = async (employee) => {
- try {
- const backendData = transformToBackend(employee);
- const created = await employeeApi.create(backendData);
- return transformEmployee(created);
- } catch (error) {
- console.error("Error creating employee:", error);
- throw error;
- }
-};
-
-export const editEmployee = async (employee) => {
- try {
- const backendData = transformToBackend(employee);
- const updated = await employeeApi.update(employee.id, backendData);
- return transformEmployee(updated);
- } catch (error) {
- console.error("Error updating employee:", error);
- throw error;
- }
-};
-
-export const deleteEmployee = async (id) => {
- try {
- await employeeApi.delete(id);
- return true;
- } catch (error) {
- console.error("Error deleting employee:", error);
- throw error;
- }
-};
diff --git a/src/pages/employees/edit/api.js b/src/pages/employees/edit/api.js
deleted file mode 100644
index 7da1e39c..00000000
--- a/src/pages/employees/edit/api.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const editEmployee = async (payload) => {
- return post(`${API_URL}/Employee/UpdateEmployee`, payload);
-};
diff --git a/src/pages/employees/form/ContactFormPage.css b/src/pages/employees/form/ContactFormPage.css
deleted file mode 100644
index 790e2fc1..00000000
--- a/src/pages/employees/form/ContactFormPage.css
+++ /dev/null
@@ -1,180 +0,0 @@
-/* Wrapper & card */
-.contactFormWrapper {
- display: flex;
- justify-content: center;
- padding: 25px 20px;
- background: #f5f6fa;
- min-height: 100vh;
- font-family: "Roboto", "Open Sans", Arial, sans-serif;
-}
-
-.formCard {
- background: #fff;
- border-radius: 6px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
- width: 100%;
- max-width: 1100px;
- padding: 20px 22px 26px;
- border: 1px solid #e6e9ee;
-}
-
-/* Header */
-.formHeader {
- padding-bottom: 10px;
- border-bottom: 1px solid #e9ecef;
- margin-bottom: 16px;
-}
-.formHeader h2 {
- margin: 0;
- color: #1f3b4d;
- font-size: 20px;
- font-weight: 600;
-}
-
-/* Section titles */
-.sectionTitle {
- margin: 0 0 12px;
- color: #2b2f3b;
- font-size: 13px;
- text-transform: uppercase;
- letter-spacing: 0.6px;
- font-weight: 700;
-}
-
-/* Account widget (assignment) */
-.account-widget {
- display: flex;
- gap: 14px;
- align-items: center;
- padding: 12px;
- border-radius: 4px;
- background: #fbfcfd;
- border: 1px dashed #d6e6ef;
- color: #0a4b6b;
-}
-.account-icon {
- width: 48px;
- height: 48px;
- background: #e9f4fb;
- color: #0077a8;
- border-radius: 4px;
- display:flex;
- align-items:center;
- justify-content:center;
- font-size:20px;
-}
-.account-info .account-title {
- font-weight:700;
-}
-.account-info .account-action {
- color: #0b63a7;
- font-weight:700;
- margin-top:4px;
-}
-.account-sub {
- color:#667085;
- font-size:12px;
- margin-top:4px;
-}
-
-/* Form layout */
-.contactFormBody {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-gap: 12px 18px;
- align-items: start;
- margin-bottom: 6px;
-}
-
-/* Form groups */
-.formGroup {
- display: flex;
- flex-direction: column;
- margin-bottom: 8px;
-}
-.formGroup.fullWidth {
- grid-column: 1 / -1;
-}
-
-/* Labels and controls */
-.formLabel {
- font-size: 13px;
- font-weight: 600;
- color: #253244;
- margin-bottom: 6px;
-}
-.required { color: #d93025; margin-left: 4px; }
-
-.formControl {
- display: block;
- width: 100%;
- height: 36px;
- padding: 6px 10px;
- font-size: 13px;
- color: #1f2937;
- background: #fff;
- border: 1px solid #d6dbe4;
- border-radius: 4px;
- box-sizing: border-box;
-}
-.formControl:focus {
- outline: none;
- border-color: #0b78b0;
- box-shadow: 0 0 0 4px rgba(11,120,176,0.08);
-}
-
-/* Hints / muted */
-.muted {
- color: #6b7280;
- font-size: 13px;
- margin-bottom: 8px;
-}
-.formHint {
- font-size: 12px;
- color: #6b7280;
- margin-top: 6px;
-}
-
-/* Form actions */
-.formActions {
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-top: 1px solid #e9eef3;
- padding-top: 14px;
- margin-top: 14px;
-}
-.formNote {
- color: #555;
- font-size: 13px;
-}
-.btnPrimary {
- background-color: #198754;
- border: 1px solid #198754;
- color: #fff;
- padding: 10px 18px;
- font-size: 14px;
- border-radius: 4px;
- cursor: pointer;
-}
-.btnPrimary:disabled {
- background-color: #c9c9c9;
- border-color: #c9c9c9;
- cursor: not-allowed;
-}
-
-/* Responsive adjustments */
-@media (max-width: 992px) {
- .contactFormBody {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (max-width: 640px) {
- .contactFormBody {
- grid-template-columns: 1fr;
- }
- .formCard {
- padding: 16px;
- }
-}
\ No newline at end of file
diff --git a/src/pages/employees/form/EmployeeFormPage.css b/src/pages/employees/form/EmployeeFormPage.css
deleted file mode 100644
index 7338b9d2..00000000
--- a/src/pages/employees/form/EmployeeFormPage.css
+++ /dev/null
@@ -1,418 +0,0 @@
-/* Employee Form Page */
-.employeeFormPage {
- background: #f5f5f5;
- min-height: 100vh;
- padding: 0;
-}
-
-.panel {
- margin-bottom: 20px;
- background-color: #fff;
- border: 1px solid transparent;
- border-radius: 4px;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.panel-default {
- border-color: #ddd;
-}
-
-.panel-connected {
- border-radius: 0;
- border: none;
- box-shadow: none;
-}
-
-.panel-body {
- padding: 15px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 15px;
- margin-bottom: 20px;
- list-style: none;
- background-color: #f5f5f5;
- border-radius: 4px;
-}
-
-.breadcrumb-alt {
- background-color: white;
- border-bottom: 1px solid #e5e5e5;
- padding: 10px 20px;
- margin: 0 0 20px 0;
- border-radius: 0;
-}
-
-.breadcrumb > li {
- display: inline-block;
-}
-
-.breadcrumb > li + li:before {
- content: ">\00a0";
- padding: 0 5px;
- color: #ccc;
-}
-
-.breadcrumb > li a {
- color: #0088cc;
- text-decoration: none;
-}
-
-.breadcrumb > li a:hover {
- text-decoration: underline;
-}
-
-.breadcrumb > .active {
- color: #00a0d1;
- font-weight: bold;
- cursor: default;
-}
-
-/* Container */
-.container {
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-
-/* Form Header */
-.header {
- margin-bottom: 15px;
-}
-
-.heading {
- margin: 0 0 10px 0;
- font-weight: 300;
- line-height: 1.1;
- color: #333;
-}
-
-.heading.thin {
- font-weight: 300;
- font-size: 24px;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.heading.text-uppercase {
- text-transform: uppercase;
- font-size: 14px;
- margin-bottom: 15px;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle;
-}
-
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.fa-stack-2x {
- font-size: 2em;
-}
-
-.fa-stack-1x {
- font-size: 1em;
- line-height: inherit;
-}
-
-.fa-inverse {
- color: #fff;
-}
-
-/* Form Groups */
-.form-group {
- margin-bottom: 15px;
- position: relative;
-}
-
-/* Row and Columns */
-.row {
- margin-right: -15px;
- margin-left: -15px;
- display: flex;
- flex-wrap: wrap;
-}
-
-.row::after {
- content: "";
- display: table;
- clear: both;
-}
-
-[class*="col-"] {
- position: relative;
- min-height: 1px;
- padding-right: 15px;
- padding-left: 15px;
-}
-
-.col-sm-2 {
- width: 16.66666667%;
-}
-
-.col-sm-4 {
- width: 33.33333333%;
-}
-
-.col-sm-5 {
- width: 41.66666667%;
-}
-
-.col-sm-6 {
- width: 50%;
-}
-
-@media (min-width: 992px) {
- .col-md-4 {
- width: 33.33333333%;
- }
-}
-
-/* Labels */
-label {
- display: inline-block;
- max-width: 100%;
- margin-bottom: 5px;
- font-weight: 600;
- font-size: 13px;
- color: #333;
-}
-
-.required-field::after {
- content: " *";
- color: #d9534f;
-}
-
-/* Input Counter */
-.input-counter {
- position: absolute;
- top: 0;
- right: 15px;
- font-size: 11px;
- color: #999;
- z-index: 10;
-}
-
-/* Form Controls */
-.form-control {
- display: block;
- width: 100%;
- height: 34px;
- padding: 6px 12px;
- font-size: 14px;
- line-height: 1.42857143;
- color: #555;
- background-color: #fff;
- background-image: none;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-}
-
-select.form-control {
- height: 34px;
- padding: 6px 12px;
-}
-
-/* Form Actions */
-.form-actions {
- padding: 19px 20px 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- border-top: 1px solid #e5e5e5;
- text-align: right;
-}
-
-/* Buttons */
-.btn {
- display: inline-block;
- padding: 6px 12px;
- margin-bottom: 0;
- font-size: 14px;
- font-weight: 400;
- line-height: 1.42857143;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- cursor: pointer;
- border: 1px solid transparent;
- border-radius: 4px;
- user-select: none;
-}
-
-.btn:focus,
-.btn:active:focus {
- outline: thin dotted;
- outline: 5px auto -webkit-focus-ring-color;
- outline-offset: -2px;
-}
-
-.btn:hover,
-.btn:focus {
- color: #333;
- text-decoration: none;
-}
-
-.btn-default {
- color: #333;
- background-color: #fff;
- border-color: #ccc;
-}
-
-.btn-default:hover,
-.btn-default:focus {
- color: #333;
- background-color: #e6e6e6;
- border-color: #adadad;
-}
-
-.btn-primary {
- color: #fff;
- background-color: #0088cc;
- border-color: #0088cc;
-}
-
-.btn-primary:hover,
-.btn-primary:focus {
- color: #fff;
- background-color: #006699;
- border-color: #005580;
-}
-
-.btn:disabled {
- cursor: not-allowed;
- opacity: 0.65;
- box-shadow: none;
-}
-
-/* Sub-heading */
-.sub-heading {
- color: #666;
- font-size: 13px;
- margin-top: 5px;
- margin-bottom: 10px;
-}
-
-/* Input Group */
-.input-group {
- position: relative;
- display: table;
- border-collapse: separate;
- width: 100%;
-}
-
-.input-group .form-control {
- position: relative;
- z-index: 2;
- float: left;
- width: 100%;
- margin-bottom: 0;
- display: table-cell;
-}
-
-.input-group-btn {
- position: relative;
- font-size: 0;
- white-space: nowrap;
- width: 1%;
- vertical-align: middle;
- display: table-cell;
-}
-
-.input-group-btn > .btn {
- position: relative;
- border-radius: 0 4px 4px 0;
-}
-
-.input-group .form-control:first-child {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
-}
-
-/* Form Control Label */
-.form-control-label {
- display: block;
- margin-top: 5px;
- font-size: 12px;
- color: #666;
-}
-
-/* Text Utilities */
-.text-muted {
- color: #777;
-}
-
-.text-sm {
- font-size: 13px;
-}
-
-.text-right {
- text-align: right;
-}
-
-.text-pad-right {
- padding-right: 10px;
-}
-
-/* Button Success */
-.btn-success {
- color: #fff;
- background-color: #5cb85c;
- border-color: #4cae4c;
-}
-
-.btn-success:hover,
-.btn-success:focus {
- color: #fff;
- background-color: #449d44;
- border-color: #398439;
-}
-
-.btn-lg {
- padding: 10px 16px;
- font-size: 18px;
- line-height: 1.33;
- border-radius: 6px;
-}
-
-/* HR */
-hr {
- margin-top: 20px;
- margin-bottom: 20px;
- border: 0;
- border-top: 1px solid #e5e5e5;
-}
-
-/* Responsive */
-@media (max-width: 767px) {
- [class*="col-sm-"] {
- width: 100%;
- }
-}
-
diff --git a/src/pages/employees/form/EmployeeFormPage.js b/src/pages/employees/form/EmployeeFormPage.js
deleted file mode 100644
index a4fdb6f0..00000000
--- a/src/pages/employees/form/EmployeeFormPage.js
+++ /dev/null
@@ -1,682 +0,0 @@
-import { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { getEmployeeById, addEmployee, editEmployee } from "../api";
-import "./EmployeeFormPage.css";
-
-export default function EmployeeFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- firstName: "",
- middleName: "",
- lastName: "",
- email: "",
- phone: "",
- jobTitle: "",
- role: "Technician",
- status: "Active",
- hireDate: "",
- dateOfBirth: "",
- address: "",
- department: "Field Service",
- timeZone: "Eastern Standard Time",
- gender: "",
- employeeNumber: "",
- regionName: "",
- userName: "",
- confirmUserName: "",
- street: "",
- streetLineTwo: "",
- city: "",
- zip: "",
- country: "US",
- state: "",
- county: "",
- smsAddress: "",
- website: "",
- facebookUrl: "",
- linkedInUrl: "",
- twitterUrl: "",
- });
-
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (isEdit && id) {
- const loadEmployee = async () => {
- try {
- const employee = await getEmployeeById(id);
- if (employee) {
- setFormData(prev => {
- const merged = { ...prev };
- for (const key of Object.keys(merged)) {
- if (employee[key] != null) merged[key] = employee[key];
- }
- return merged;
- });
- }
- } catch (err) {
- console.error("Failed to load employee", err);
- toast.error("Failed to load employee");
- }
- };
- loadEmployee();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData(prev => ({
- ...prev,
- [name]: value,
- }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- if (isEdit) {
- await editEmployee(formData);
- toast.success("Employee updated successfully!");
- } else {
- await addEmployee(formData);
- toast.success("Employee created successfully!");
- }
- navigate("/employees");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save employee");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate("/employees");
- };
-
- return (
-
-
-
-
- -
- { e.preventDefault(); navigate("/employees"); }}>
- Employees
-
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/employees/list/Delete.js b/src/pages/employees/list/Delete.js
deleted file mode 100644
index 394ae1a9..00000000
--- a/src/pages/employees/list/Delete.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { deleteContact } from "./api";
-
-export default function DeleteModal({ open, item, onClose, onSuccess }) {
- if(!open) return null;
- const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
- return (
-
-
-
-
-
-
Are you sure you want to delete this contact?
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/employees/list/Form.js b/src/pages/employees/list/Form.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/employees/list/List.js b/src/pages/employees/list/List.js
deleted file mode 100644
index a94484d5..00000000
--- a/src/pages/employees/list/List.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchEmployeesPaged } from "./api";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import ActionBar from "../../../components/ActionBar";
-import { deleteEmployee } from "../api";
-import "./styles.css";
-
-export default function EmployeeList() {
- const navigate = useNavigate();
- const { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload } = usePaginatedList(fetchEmployeesPaged);
- const [deleteModal, setDeleteModal] = useState({ isOpen: false, item: null, loading: false });
- const [viewMode, setViewMode] = useState("list"); // "list" or "card"
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- const handleDeleteClick = (item) => {
- setDeleteModal({ isOpen: true, item, loading: false });
- };
-
- const handleDeleteConfirm = async () => {
- setDeleteModal(prev => ({ ...prev, loading: true }));
- try {
- await deleteEmployee(deleteModal.item.id);
- toast.success(`Employee deleted successfully!`);
- setDeleteModal({ isOpen: false, item: null, loading: false });
- reload();
- } catch (err) {
- console.error(err);
- toast.error("Failed to delete employee");
- setDeleteModal(prev => ({ ...prev, loading: false }));
- }
- };
-
- return (
- <>
-
navigate("/employees/new")}
- onBulkEdit={() => console.log("Bulk edit")}
- selectedCount={0}
- />
-
-
-
- Employees
-
-
-
-
-
-
-
-
-
- Search for Employees
-
- Use the settings menu to adjust how to filter the list, and which columns to display.
-
-
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Enter search criteria here..."
- />
-
-
-
-
-
-
-
-
-
-
-
- {page}
-
-
-
-
-
-
- navigate(`/employees/edit/${item.id}`)}
- onDelete={handleDeleteClick}
- page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize}
- totalCount={totalCount} totalPages={totalPages} />
-
- setDeleteModal({ isOpen: false, item: null, loading: false })}
- onConfirm={handleDeleteConfirm}
- title="Delete Employee"
- message={`Are you sure you want to delete "${deleteModal.item?.name}"? This action cannot be undone.`}
- loading={deleteModal.loading}
- />
-
- >
- );
-}
diff --git a/src/pages/employees/list/Table.js b/src/pages/employees/list/Table.js
deleted file mode 100644
index d7b71248..00000000
--- a/src/pages/employees/list/Table.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-export default function Table({ loading, error, rows, onEdit, onDelete,
- page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
-
- const columns = [
- { key: "name", label: "Name", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
- { key: "jobTitle", label: "Job Title" },
- { key: "role", label: "Role" },
- { key: "email", label: "Email" },
- { key: "phone", label: "Phone" },
- { key: "status", label: "Status" },
- { key: "lastLogin", label: "Last Login" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/employees/list/api.js b/src/pages/employees/list/api.js
deleted file mode 100644
index 05a2df17..00000000
--- a/src/pages/employees/list/api.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchEmployeesPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
- const res = await get(`${API_URL}/Employee/GetEmployeeList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id,
- name: `${x.firstName ?? ""} ${x.lastName ?? ""}`.trim(),
- jobTitle: x.jobTitle ?? "",
- email: x.email ?? "",
- phone: x.phone ?? "",
- department: x.department ?? "",
- status: x.status ?? "",
- }));
- return {
- rows,
- page: res?.pageNumber ?? 1,
- pageSize: res?.pageSize ?? pageSize,
- totalCount: res?.totalCount ?? rows.length,
- totalPages: res?.totalPages ?? 1,
- };
-};
-
-export const deleteEmployee = (id) =>
- get(`${API_URL}/Employee/DeleteEmployee?id=${id}`);
diff --git a/src/pages/employees/list/styles.css b/src/pages/employees/list/styles.css
deleted file mode 100644
index ac854bf0..00000000
--- a/src/pages/employees/list/styles.css
+++ /dev/null
@@ -1,231 +0,0 @@
-/* Modal styles for Employees module */
-.modalRoot{position:fixed;inset:0;z-index:1000;}
-.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
-.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/employees/new/api.js b/src/pages/employees/new/api.js
deleted file mode 100644
index 5f3c6ca2..00000000
--- a/src/pages/employees/new/api.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const addEmployee = async (payload) => {
- return post(`${API_URL}/Employee/CreateEmployee`, payload);
-};
diff --git a/src/pages/followup/api.js b/src/pages/followup/api.js
deleted file mode 100644
index 5bc7c488..00000000
--- a/src/pages/followup/api.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { get, post } from "../../apiUtil";
-import { API_URL } from "../../constants";
-
-export const getFollowUpById = id =>
- get(`${API_URL}/FollowUp/GetById?id=${id}`);
-
-export const addFollowUp = payload =>
- post(`${API_URL}/FollowUp/Create`, payload);
-
-export const editFollowUp = payload =>
- post(`${API_URL}/FollowUp/Update`, payload);
-
-export const deleteFollowUp = id =>
- post(`${API_URL}/FollowUp/Delete?id=${id}`);
-
-export const completeFollowUp = id =>
- post(`${API_URL}/FollowUp/Complete?id=${id}`);
-
-export const cancelFollowUp = id =>
- post(`${API_URL}/FollowUp/Cancel?id=${id}`);
diff --git a/src/pages/followup/edit/api.js b/src/pages/followup/edit/api.js
deleted file mode 100644
index 7da1e39c..00000000
--- a/src/pages/followup/edit/api.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const editEmployee = async (payload) => {
- return post(`${API_URL}/Employee/UpdateEmployee`, payload);
-};
diff --git a/src/pages/followup/form/FollowUpFormPage.css b/src/pages/followup/form/FollowUpFormPage.css
deleted file mode 100644
index bc823fa6..00000000
--- a/src/pages/followup/form/FollowUpFormPage.css
+++ /dev/null
@@ -1,289 +0,0 @@
-/* Follow-up Form Page - Matching Employee Form Style */
-.followUpFormPage {
- background: #f5f5f5;
- min-height: 100vh;
- padding: 0;
-}
-
-.panel {
- margin-bottom: 20px;
- background-color: #fff;
- border: 1px solid transparent;
- border-radius: 4px;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.panel-default {
- border-color: #ddd;
-}
-
-.panel-connected {
- border-radius: 0;
- border: none;
- box-shadow: none;
-}
-
-.panel-body {
- padding: 15px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 15px;
- margin-bottom: 20px;
- list-style: none;
- background-color: #f5f5f5;
- border-radius: 4px;
-}
-
-.breadcrumb-alt {
- background-color: white;
- border-bottom: 1px solid #e5e5e5;
- padding: 10px 20px;
- margin: 0 0 20px 0;
- border-radius: 0;
-}
-
-.breadcrumb > li {
- display: inline-block;
-}
-
-.breadcrumb > li + li:before {
- content: ">\00a0";
- padding: 0 5px;
- color: #ccc;
-}
-
-.breadcrumb > li a {
- color: #0088cc;
- text-decoration: none;
-}
-
-.breadcrumb > li a:hover {
- text-decoration: underline;
-}
-
-.breadcrumb > .active {
- color: #00a0d1;
- font-weight: bold;
- cursor: default;
-}
-
-/* Container */
-.container {
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-
-/* Form Header */
-.header {
- margin-bottom: 15px;
-}
-
-.heading {
- margin: 0 0 10px 0;
- font-weight: 300;
- line-height: 1.1;
- color: #333;
-}
-
-.heading.thin {
- font-weight: 300;
- font-size: 24px;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.heading.text-uppercase {
- text-transform: uppercase;
- font-size: 14px;
- margin-bottom: 15px;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle;
-}
-
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.fa-stack-2x {
- font-size: 2em;
-}
-
-.fa-stack-1x {
- font-size: 1em;
- line-height: inherit;
-}
-
-.fa-inverse {
- color: #fff;
-}
-
-/* Form Groups */
-.form-group {
- margin-bottom: 15px;
- position: relative;
-}
-
-/* Row and Columns */
-.row {
- margin-right: -15px;
- margin-left: -15px;
- display: flex;
- flex-wrap: wrap;
-}
-
-.row::after {
- content: "";
- display: table;
- clear: both;
-}
-
-[class*="col-"] {
- position: relative;
- min-height: 1px;
- padding-right: 15px;
- padding-left: 15px;
-}
-
-.col-sm-4 {
- width: 33.33333333%;
-}
-
-.col-sm-6 {
- width: 50%;
-}
-
-@media (min-width: 992px) {
- .col-md-4 {
- width: 33.33333333%;
- }
-}
-
-/* Labels */
-label {
- display: inline-block;
- max-width: 100%;
- margin-bottom: 5px;
- font-weight: 600;
- font-size: 13px;
- color: #333;
-}
-
-.required-field::after {
- content: " *";
- color: #d9534f;
-}
-
-/* Form Controls */
-.form-control {
- display: block;
- width: 100%;
- height: 34px;
- padding: 6px 12px;
- font-size: 14px;
- line-height: 1.42857143;
- color: #555;
- background-color: #fff;
- background-image: none;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-}
-
-select.form-control {
- height: 34px;
- padding: 6px 12px;
-}
-
-textarea.form-control {
- height: auto;
- min-height: 100px;
-}
-
-/* Form Actions */
-.form-actions {
- padding: 19px 20px 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- border-top: 1px solid #e5e5e5;
- text-align: right;
-}
-
-/* Buttons */
-.btn {
- display: inline-block;
- padding: 6px 12px;
- margin-bottom: 0;
- font-size: 14px;
- font-weight: 400;
- line-height: 1.42857143;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- cursor: pointer;
- border: 1px solid transparent;
- border-radius: 4px;
- user-select: none;
-}
-
-.btn-default {
- color: #333;
- background-color: #fff;
- border-color: #ccc;
-}
-
-.btn-default:hover {
- background-color: #e6e6e6;
- border-color: #adadad;
-}
-
-.btn-primary {
- color: #fff;
- background-color: #0088cc;
- border-color: #0088cc;
-}
-
-.btn-primary:hover {
- background-color: #006699;
- border-color: #005580;
-}
-
-.btn:disabled {
- cursor: not-allowed;
- opacity: 0.65;
-}
-
-/* Responsive */
-@media (max-width: 767px) {
- [class*="col-sm-"] {
- width: 100%;
- }
-}
-
diff --git a/src/pages/followup/form/FollowUpFormPage.js b/src/pages/followup/form/FollowUpFormPage.js
deleted file mode 100644
index 6bbb2810..00000000
--- a/src/pages/followup/form/FollowUpFormPage.js
+++ /dev/null
@@ -1,343 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { getFollowUpById, addFollowUp, editFollowUp } from "../api";
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-import "./FollowUpFormPage.css";
-
-function normalizeId(v) {
- if (v === "" || v === null || v === undefined) return null;
- const n = parseInt(v, 10);
- return Number.isNaN(n) ? null : n;
-}
-
-function toTimeString(t) {
- if (!t) return "09:00";
- const m = String(t).match(/^(\d{2}:\d{2})/);
- return m ? m[1] : "09:00";
-}
-
-export default function FollowUpFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- reason: "",
- scheduleDate: "",
- scheduleStartTime: "09:00",
- employeeId: "",
- accountId: "",
- locationId: "",
- workOrderId: "",
- });
-
- const [loading, setLoading] = useState(false);
- const [employees, setEmployees] = useState([]);
- const [accounts, setAccounts] = useState([]);
- const [locations, setLocations] = useState([]);
-
- useEffect(() => {
- get(`${API_URL}/Employee/GetEmployeeList?pageSize=500`)
- .then((res) => {
- const rows = res?.data || res?.Data || [];
- setEmployees(
- rows.map((e) => ({
- id: e.id ?? e.Id,
- name: `${e.firstName ?? e.FirstName ?? ""} ${e.lastName ?? e.LastName ?? ""}`.trim(),
- }))
- );
- })
- .catch(() => {});
-
- get(`${API_URL}/Account/GetAccountList?pageSize=500`)
- .then((res) => {
- const rows = res?.data || res?.Data || [];
- setAccounts(
- rows.map((a) => ({
- id: a.id ?? a.Id,
- name: a.name ?? a.Name ?? "",
- }))
- );
- })
- .catch(() => {});
-
- get(`${API_URL}/Contact/GetLocations`)
- .then((data) => {
- const rows = Array.isArray(data) ? data : [];
- setLocations(
- rows.map((l) => ({
- id: l.id ?? l.Id,
- name: l.name ?? l.Name ?? "",
- }))
- );
- })
- .catch(() => {});
- }, []);
-
- useEffect(() => {
- if (!isEdit || !id) return;
- getFollowUpById(id)
- .then((data) => {
- if (!data) return;
- setFormData({
- reason: data.reason ?? data.Reason ?? "",
- scheduleDate: (data.scheduleDate ?? data.ScheduleDate ?? "").slice(0, 10),
- scheduleStartTime: toTimeString(data.scheduleStartTime ?? data.ScheduleStartTime),
- employeeId: data.employeeId ?? data.EmployeeId ?? "",
- accountId: data.accountId ?? data.AccountId ?? "",
- locationId: data.locationId ?? data.LocationId ?? "",
- workOrderId: data.workOrderId ?? data.WorkOrderId ?? "",
- });
- })
- .catch((err) => {
- console.error("Failed to load follow up", err);
- toast.error("Failed to load follow up");
- });
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData((prev) => ({ ...prev, [name]: value }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- const payload = {
- Id: isEdit ? parseInt(id, 10) : 0,
- Reason: formData.reason,
- ScheduleDate: formData.scheduleDate,
- ScheduleStartTime: `${toTimeString(formData.scheduleStartTime)}:00`,
- EmployeeId: normalizeId(formData.employeeId),
- AccountId: normalizeId(formData.accountId),
- LocationId: normalizeId(formData.locationId),
- WorkOrderId: normalizeId(formData.workOrderId),
- };
-
- try {
- if (isEdit) {
- await editFollowUp(payload);
- toast.success("Follow-up updated successfully!");
- } else {
- await addFollowUp(payload);
- toast.success("Follow-up created successfully!");
- }
- navigate("/followups");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save follow-up");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate("/followups");
- };
-
- return (
-
-
-
-
- -
- {
- e.preventDefault();
- navigate("/followups");
- }}
- >
- Follow-ups
-
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/followup/list/Delete.js b/src/pages/followup/list/Delete.js
deleted file mode 100644
index 394ae1a9..00000000
--- a/src/pages/followup/list/Delete.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { deleteContact } from "./api";
-
-export default function DeleteModal({ open, item, onClose, onSuccess }) {
- if(!open) return null;
- const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
- return (
-
-
-
-
-
-
Are you sure you want to delete this contact?
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/followup/list/Form.js b/src/pages/followup/list/Form.js
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/pages/followup/list/List.js b/src/pages/followup/list/List.js
deleted file mode 100644
index 92ae7b08..00000000
--- a/src/pages/followup/list/List.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useMemo, useState } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchFollowUpsPaged } from "./api";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import { deleteFollowUp } from "../api";
-import "./styles.css";
-
-const STATUSES = ["Open", "Completed", "Cancelled"];
-
-export default function FollowUpList() {
- const navigate = useNavigate();
- const [statusFilter, setStatusFilter] = useState("Open");
- const extraParams = useMemo(() => ({ status: statusFilter }), [statusFilter]);
- const {
- rows,
- search,
- setSearch,
- page,
- setPage,
- pageSize,
- setPageSize,
- totalCount,
- loading,
- err,
- reload,
- } = usePaginatedList(fetchFollowUpsPaged, extraParams);
- const [deleteModal, setDeleteModal] = useState({ isOpen: false, item: null, loading: false });
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- const handleDeleteClick = (item) => {
- setDeleteModal({ isOpen: true, item, loading: false });
- };
-
- const handleDeleteConfirm = async () => {
- setDeleteModal((prev) => ({ ...prev, loading: true }));
- try {
- await deleteFollowUp(deleteModal.item.id);
- toast.success("Follow-up deleted successfully!");
- setDeleteModal({ isOpen: false, item: null, loading: false });
- reload();
- } catch (err) {
- console.error(err);
- toast.error("Failed to delete follow-up");
- setDeleteModal((prev) => ({ ...prev, loading: false }));
- }
- };
-
- return (
-
-
-
-
Follow Ups
- {totalCount} total
-
-
- { setSearch(e.target.value); setPage(1); }}
- placeholder="Search by reason, account, or assignee..."
- />
-
-
-
-
-
- {STATUSES.map((s) => (
-
- ))}
-
-
- navigate(`/followups/edit/${item.id}`)}
- onDelete={handleDeleteClick}
- page={page}
- setPage={setPage}
- pageSize={pageSize}
- setPageSize={setPageSize}
- totalCount={totalCount}
- totalPages={totalPages}
- />
-
- setDeleteModal({ isOpen: false, item: null, loading: false })}
- onConfirm={handleDeleteConfirm}
- title="Delete Follow-up"
- message={`Are you sure you want to delete this follow-up: "${deleteModal.item?.reason || ""}"? This action cannot be undone.`}
- loading={deleteModal.loading}
- />
-
- );
-}
diff --git a/src/pages/followup/list/Table.js b/src/pages/followup/list/Table.js
deleted file mode 100644
index 72a9586d..00000000
--- a/src/pages/followup/list/Table.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-function formatScheduled(date, time) {
- if (!date) return "";
- const d = new Date(date);
- if (isNaN(d)) return "";
- const dateStr = d.toISOString().slice(0, 10);
- if (!time) return dateStr;
- const m = String(time).match(/^(\d{2}:\d{2})/);
- return m ? `${dateStr} ${m[1]}` : dateStr;
-}
-
-export default function Table({
- loading,
- error,
- rows,
- onEdit,
- onDelete,
- page,
- setPage,
- pageSize,
- setPageSize,
- totalCount,
- totalPages,
-}) {
- const columns = [
- {
- key: "reason",
- label: "Reason",
- render: (val, row) => (
-
- {
- e.preventDefault();
- onEdit && onEdit(row);
- }}
- >
- {val || "(no reason)"}
-
-
- ),
- },
- { key: "assignedTo", label: "Assigned To" },
- {
- key: "scheduleDate",
- label: "Scheduled",
- render: (_, row) => formatScheduled(row.scheduleDate, row.scheduleStartTime),
- },
- { key: "accountName", label: "Account" },
- { key: "locationName", label: "Location" },
- {
- key: "workOrderNumber",
- label: "Work Order",
- render: (val, row) => val || row.internalWONumber || "",
- },
- { key: "createdByName", label: "Created By" },
- { key: "status", label: "Status" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/followup/list/api.js b/src/pages/followup/list/api.js
deleted file mode 100644
index 8d55ba5b..00000000
--- a/src/pages/followup/list/api.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchFollowUpsPaged = async ({
- search = "",
- page = 1,
- pageSize = 12,
- status = "Open"
-}) => {
- const res = await get(
- `${API_URL}/FollowUp/GetFollowUpList?search=${encodeURIComponent(
- search
- )}&page=${page}&pageSize=${pageSize}&status=${encodeURIComponent(status)}`
- );
-
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id ?? x.Id,
- reason: x.reason ?? x.Reason ?? "",
- scheduleDate: x.scheduleDate ?? x.ScheduleDate ?? "",
- scheduleStartTime: x.scheduleStartTime ?? x.ScheduleStartTime ?? "",
- employeeId: x.employeeId ?? x.EmployeeId ?? null,
- workOrderId: x.workOrderId ?? x.WorkOrderId ?? null,
- accountId: x.accountId ?? x.AccountId ?? null,
- locationId: x.locationId ?? x.LocationId ?? null,
- accountName: x.accountName ?? x.AccountName ?? "",
- assignedTo: x.assignedTo ?? x.AssignedTo ?? "",
- locationName: x.locationName ?? x.LocationName ?? "",
- workOrderNumber: x.workOrderNumber ?? x.WorkOrderNumber ?? "",
- internalWONumber: x.internalWONumber ?? x.InternalWONumber ?? "",
- status: x.status ?? x.Status ?? "",
- createdDate: x.createdDate ?? x.CreatedDate ?? "",
- createdByName: x.createdByName ?? x.CreatedByName ?? ""
- }));
-
- return {
- rows,
- page: res?.pageNumber ?? res?.PageNumber ?? 1,
- pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
- totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
- totalPages: res?.totalPages ?? res?.TotalPages ?? 1
- };
-};
diff --git a/src/pages/followup/list/styles.css b/src/pages/followup/list/styles.css
deleted file mode 100644
index 731008cd..00000000
--- a/src/pages/followup/list/styles.css
+++ /dev/null
@@ -1,353 +0,0 @@
-/* Modal styles for Follow-ups module */
-.modalRoot{position:fixed;inset:0;z-index:1000;}
-.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
-.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Toolbar header (mirrors work orders list look) */
-.fuListToolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.fuListToolbarLeft {
- display: flex;
- align-items: baseline;
- gap: 12px;
-}
-
-.fuListTitle {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #333;
-}
-
-.fuListCount {
- font-size: 13px;
- color: #999;
-}
-
-.fuListToolbarRight {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.fuSearchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-size: 14px;
- outline: none;
- width: 260px;
- box-sizing: border-box;
-}
-
-.fuSearchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.fuNewBtn {
- padding: 8px 16px;
- background: #007bff;
- color: #fff;
- border: none;
- border-radius: 4px;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
-}
-
-.fuNewBtn:hover {
- background: #0069d9;
-}
-
-.fuFilterBar {
- display: flex;
- gap: 6px;
- padding: 10px 20px;
- border-bottom: 1px solid #e5e5e5;
- background: #fafafa;
-}
-
-.fuFilterChip {
- padding: 5px 14px;
- border: 1px solid #ddd;
- border-radius: 16px;
- background: #fff;
- font-size: 12px;
- color: #666;
- cursor: pointer;
-}
-
-.fuFilterChip:hover {
- border-color: #007bff;
- color: #007bff;
-}
-
-.fuFilterChip.active {
- background: #007bff;
- color: #fff;
- border-color: #007bff;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.followUpStatusTabs {
- display: flex;
- gap: 4px;
- margin-bottom: 16px;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.followUpStatusTab {
- padding: 8px 18px;
- background: transparent;
- border: none;
- border-bottom: 2px solid transparent;
- color: #666;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: color .15s ease, border-color .15s ease;
-}
-
-.followUpStatusTab:hover {
- color: #0088cc;
-}
-
-.followUpStatusTab.active {
- color: #0088cc;
- border-bottom-color: #0088cc;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/followup/new/api.js b/src/pages/followup/new/api.js
deleted file mode 100644
index 016f17b3..00000000
--- a/src/pages/followup/new/api.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { get, post } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchFollowUpsPaged = async ({
- search = "",
- page = 1,
- pageSize = 12,
- completed = false,
-}) => {
- const res = await get(
- `${API_URL}/FollowUp/GetFollowUpList?search=${encodeURIComponent(
- search
- )}&page=${page}&pageSize=${pageSize}&completed=${completed}`
- );
-
- const rows = (res?.Data || []).map((x) => ({
- id: x.Id,
- reason: x.Reason,
- scheduleDate: x.ScheduleDate,
- account: x.Account,
- assignedTo: x.AssignedTo,
- locationName: x.LocationName,
- workOrderNumber: x.WorkOrderNumber,
- status: x.Status,
- }));
-
- return {
- rows,
- page: res.PageNumber,
- pageSize: res.PageSize,
- totalCount: res.TotalCount,
- totalPages: res.TotalPages,
- };
-};
-
-export const getFollowUpById = (id) =>
- get(`${API_URL}/FollowUp/GetById?id=${id}`);
-
-export const saveFollowUp = (payload) =>
- post(`${API_URL}/FollowUp/Save`, payload);
-
-export const deleteFollowUp = (id) =>
- get(`${API_URL}/FollowUp/Delete?id=${id}`);
diff --git a/src/pages/locations/api.js b/src/pages/locations/api.js
deleted file mode 100644
index 49fbeac4..00000000
--- a/src/pages/locations/api.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { locationApi } from "../../services/api";
-
-export const getLocations = async (params = {}) => {
- try {
- const response = await locationApi.getAll(params);
- const locations = response.data || response;
- return Array.isArray(locations) ? locations : [];
- } catch (error) {
- console.error("Error fetching locations:", error);
- throw error;
- }
-};
-
-export const getLocationById = async (id) => {
- try {
- return await locationApi.getById(id);
- } catch (error) {
- console.error("Error fetching location:", error);
- throw error;
- }
-};
-
-export const addLocation = async (location) => {
- try {
- return await locationApi.create(location);
- } catch (error) {
- console.error("Error creating location:", error);
- throw error;
- }
-};
-
-export const editLocation = async (location) => {
- try {
- return await locationApi.update(location.id, location);
- } catch (error) {
- console.error("Error updating location:", error);
- throw error;
- }
-};
-
-export const deleteLocation = async (id) => {
- try {
- await locationApi.delete(id);
- return true;
- } catch (error) {
- console.error("Error deleting location:", error);
- throw error;
- }
-};
diff --git a/src/pages/locations/form/LocationFormPage.css b/src/pages/locations/form/LocationFormPage.css
deleted file mode 100644
index 67e3a13f..00000000
--- a/src/pages/locations/form/LocationFormPage.css
+++ /dev/null
@@ -1,289 +0,0 @@
-/* Location Form Page - Matching Employee Form Style */
-.locationFormPage {
- background: #f5f5f5;
- min-height: 100vh;
- padding: 0;
-}
-
-.panel {
- margin-bottom: 20px;
- background-color: #fff;
- border: 1px solid transparent;
- border-radius: 4px;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.panel-default {
- border-color: #ddd;
-}
-
-.panel-connected {
- border-radius: 0;
- border: none;
- box-shadow: none;
-}
-
-.panel-body {
- padding: 15px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 15px;
- margin-bottom: 20px;
- list-style: none;
- background-color: #f5f5f5;
- border-radius: 4px;
-}
-
-.breadcrumb-alt {
- background-color: white;
- border-bottom: 1px solid #e5e5e5;
- padding: 10px 20px;
- margin: 0 0 20px 0;
- border-radius: 0;
-}
-
-.breadcrumb > li {
- display: inline-block;
-}
-
-.breadcrumb > li + li:before {
- content: ">\00a0";
- padding: 0 5px;
- color: #ccc;
-}
-
-.breadcrumb > li a {
- color: #0088cc;
- text-decoration: none;
-}
-
-.breadcrumb > li a:hover {
- text-decoration: underline;
-}
-
-.breadcrumb > .active {
- color: #00a0d1;
- font-weight: bold;
- cursor: default;
-}
-
-/* Container */
-.container {
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-
-/* Form Header */
-.header {
- margin-bottom: 15px;
-}
-
-.heading {
- margin: 0 0 10px 0;
- font-weight: 300;
- line-height: 1.1;
- color: #333;
-}
-
-.heading.thin {
- font-weight: 300;
- font-size: 24px;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.heading.text-uppercase {
- text-transform: uppercase;
- font-size: 14px;
- margin-bottom: 15px;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle;
-}
-
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.fa-stack-2x {
- font-size: 2em;
-}
-
-.fa-stack-1x {
- font-size: 1em;
- line-height: inherit;
-}
-
-.fa-inverse {
- color: #fff;
-}
-
-/* Form Groups */
-.form-group {
- margin-bottom: 15px;
- position: relative;
-}
-
-/* Row and Columns */
-.row {
- margin-right: -15px;
- margin-left: -15px;
- display: flex;
- flex-wrap: wrap;
-}
-
-.row::after {
- content: "";
- display: table;
- clear: both;
-}
-
-[class*="col-"] {
- position: relative;
- min-height: 1px;
- padding-right: 15px;
- padding-left: 15px;
-}
-
-.col-sm-4 {
- width: 33.33333333%;
-}
-
-.col-sm-6 {
- width: 50%;
-}
-
-@media (min-width: 992px) {
- .col-md-4 {
- width: 33.33333333%;
- }
-}
-
-/* Labels */
-label {
- display: inline-block;
- max-width: 100%;
- margin-bottom: 5px;
- font-weight: 600;
- font-size: 13px;
- color: #333;
-}
-
-.required-field::after {
- content: " *";
- color: #d9534f;
-}
-
-/* Form Controls */
-.form-control {
- display: block;
- width: 100%;
- height: 34px;
- padding: 6px 12px;
- font-size: 14px;
- line-height: 1.42857143;
- color: #555;
- background-color: #fff;
- background-image: none;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-}
-
-select.form-control {
- height: 34px;
- padding: 6px 12px;
-}
-
-textarea.form-control {
- height: auto;
- min-height: 100px;
-}
-
-/* Form Actions */
-.form-actions {
- padding: 19px 20px 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- border-top: 1px solid #e5e5e5;
- text-align: right;
-}
-
-/* Buttons */
-.btn {
- display: inline-block;
- padding: 6px 12px;
- margin-bottom: 0;
- font-size: 14px;
- font-weight: 400;
- line-height: 1.42857143;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- cursor: pointer;
- border: 1px solid transparent;
- border-radius: 4px;
- user-select: none;
-}
-
-.btn-default {
- color: #333;
- background-color: #fff;
- border-color: #ccc;
-}
-
-.btn-default:hover {
- background-color: #e6e6e6;
- border-color: #adadad;
-}
-
-.btn-primary {
- color: #fff;
- background-color: #0088cc;
- border-color: #0088cc;
-}
-
-.btn-primary:hover {
- background-color: #006699;
- border-color: #005580;
-}
-
-.btn:disabled {
- cursor: not-allowed;
- opacity: 0.65;
-}
-
-/* Responsive */
-@media (max-width: 767px) {
- [class*="col-sm-"] {
- width: 100%;
- }
-}
-
diff --git a/src/pages/locations/form/LocationFormPage.js b/src/pages/locations/form/LocationFormPage.js
deleted file mode 100644
index d3ba3b1a..00000000
--- a/src/pages/locations/form/LocationFormPage.js
+++ /dev/null
@@ -1,321 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { getLocationById, addLocation, editLocation } from "../api";
-import "./LocationFormPage.css";
-
-export default function LocationFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- name: "",
- address: "",
- city: "",
- state: "IL",
- zipCode: "",
- phone: "",
- contact: "",
- contactEmail: "",
- status: "Active",
- accountId: "",
- });
-
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (isEdit && id) {
- const loadLocation = async () => {
- try {
- const location = await getLocationById(id);
- if (location) {
- setFormData(prev => {
- const merged = { ...prev };
- for (const key of Object.keys(merged)) {
- if (location[key] != null) merged[key] = location[key];
- }
- return merged;
- });
- }
- } catch (err) {
- console.error("Failed to load location", err);
- toast.error("Failed to load location");
- }
- };
- loadLocation();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData(prev => ({
- ...prev,
- [name]: value,
- }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- if (isEdit) {
- await editLocation(formData);
- toast.success("Location updated successfully!");
- } else {
- await addLocation(formData);
- toast.success("Location created successfully!");
- }
- navigate("/locations");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save location");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate("/locations");
- };
-
- return (
-
-
-
-
- -
- { e.preventDefault(); navigate("/locations"); }}>
- Locations
-
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/locations/list/List.js b/src/pages/locations/list/List.js
deleted file mode 100644
index d4872b48..00000000
--- a/src/pages/locations/list/List.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import { useState } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchLocationsPaged } from "./api";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import ActionBar from "../../../components/ActionBar";
-import { deleteLocation } from "../api";
-import "./styles.css";
-
-export default function LocationsList() {
- const navigate = useNavigate();
-
- const {
- rows,
- search,
- setSearch,
- page,
- setPage,
- pageSize,
- setPageSize,
- totalCount,
- loading,
- err,
- reload
- } = usePaginatedList(fetchLocationsPaged);
-
- const [viewMode, setViewMode] = useState("list");
- const [delState, setDelState] = useState({ open: false, item: null, loading: false });
-
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- const handleDeleteConfirm = async () => {
- setDelState(prev => ({ ...prev, loading: true }));
- try {
- await deleteLocation(delState.item.id);
- toast.success("Location deleted successfully!");
- setDelState({ open: false, item: null, loading: false });
- reload();
- } catch (err) {
- console.error(err);
- toast.error("Failed to delete location");
- setDelState(prev => ({ ...prev, loading: false }));
- }
- };
-
- return (
- <>
- navigate("/locations/new")}
- onBulkEdit={() => console.log("Bulk edit")}
- selectedCount={0}
- />
-
-
-
- Locations
-
-
-
-
-
-
-
-
-
- Search for Locations
-
- Use the settings menu to adjust how to filter the list, and which columns to display.
-
-
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Enter search criteria here..."
- />
-
-
-
-
-
-
-
-
-
-
-
- {page}
-
-
-
-
-
-
- navigate(`/locations/edit/${item.id}`)}
- onDelete={(item) => setDelState({ open: true, item, loading: false })}
- page={page}
- setPage={setPage}
- pageSize={pageSize}
- setPageSize={setPageSize}
- totalCount={totalCount}
- totalPages={totalPages}
- />
-
- setDelState({ open: false, item: null, loading: false })}
- onConfirm={handleDeleteConfirm}
- title="Delete Location"
- message={`Are you sure you want to delete "${delState.item?.name}"?`}
- loading={delState.loading}
- />
-
- >
- );
-}
diff --git a/src/pages/locations/list/Table.js b/src/pages/locations/list/Table.js
deleted file mode 100644
index 82646d50..00000000
--- a/src/pages/locations/list/Table.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
- const columns = [
- { key: "name", label: "Location Name", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
- { key: "address", label: "Address" },
- { key: "city", label: "City" },
- { key: "state", label: "State" },
- { key: "phone", label: "Phone" },
- { key: "contactPerson", label: "Contact" },
- { key: "status", label: "Status" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/locations/list/api.js b/src/pages/locations/list/api.js
deleted file mode 100644
index ec2befb3..00000000
--- a/src/pages/locations/list/api.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchLocationsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
- const res = await get(`${API_URL}/Location/GetLocationList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id ?? x.Id,
- name: x.name ?? x.Name ?? "",
- address: x.address ?? x.Address ?? "",
- city: x.city ?? x.City ?? "",
- state: x.state ?? x.State ?? "",
- zipCode: x.zipCode ?? x.ZipCode ?? x.Zip ?? "",
- phone: x.phone ?? x.Phone ?? "",
- contact: x.contact ?? x.Contact ?? "",
- contactEmail: x.contactEmail ?? x.ContactEmail ?? "",
- status: x.status ?? x.Status ?? "",
- accountId: x.accountId ?? x.AccountId ?? "",
- }));
- return {
- rows,
- page: res?.pageNumber ?? 1,
- pageSize: res?.pageSize ?? pageSize,
- totalCount: res?.totalCount ?? rows.length,
- totalPages: res?.totalPages ?? 1,
- };
-};
-
-export const deleteLocation = (id) =>
- get(`${API_URL}/Location/DeleteLocation?id=${id}`);
-
diff --git a/src/pages/locations/list/styles.css b/src/pages/locations/list/styles.css
deleted file mode 100644
index 8b17a23c..00000000
--- a/src/pages/locations/list/styles.css
+++ /dev/null
@@ -1,231 +0,0 @@
-/* Modal styles for Locations module */
-.modalRoot{position:fixed;inset:0;z-index:1000;}
-.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
-.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-left: none;
- border-right: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- border-right: 1px solid #ddd;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/settings/DropdownOptionsPage.css b/src/pages/settings/DropdownOptionsPage.css
deleted file mode 100644
index 0b7e420a..00000000
--- a/src/pages/settings/DropdownOptionsPage.css
+++ /dev/null
@@ -1,182 +0,0 @@
-.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;
-}
diff --git a/src/pages/settings/DropdownOptionsPage.js b/src/pages/settings/DropdownOptionsPage.js
deleted file mode 100644
index 219e7eee..00000000
--- a/src/pages/settings/DropdownOptionsPage.js
+++ /dev/null
@@ -1,195 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { get, post, put, deleteItem } from "../../apiUtil";
-import { API_URL } from "../../constants";
-import { toast } from "react-toastify";
-import SettingsNav from "./SettingsNav";
-import "../settings/TaskTemplatesPage.css";
-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 (
-
-
-
-
Dropdown Options
-
Manage configurable dropdown values for work orders
-
-
-
-
- {CATEGORIES.map((cat) => (
-
- ))}
-
-
-
-
- {categoryConfig?.parent && (
-
- )}
- setNewValue(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder={`Add new ${categoryConfig?.label?.slice(0, -1) || "option"}...`}
- />
-
-
-
- {loading ? (
-
Loading...
- ) : options.length === 0 ? (
-
No options configured yet
- ) : (
-
- {options.map((item) => (
-
- {editingId === item.id ? (
-
setEditValue(e.target.value)}
- onKeyDown={(e) => { if (e.key === "Enter") handleEditSave(item); if (e.key === "Escape") setEditingId(null); }}
- autoFocus
- />
- ) : (
-
- {item.value}
- {item.parentValue && {item.parentValue}}
-
- )}
-
- {editingId === item.id ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )}
-
-
- ))}
-
- )}
-
-
-
- );
-}
diff --git a/src/pages/settings/SettingsNav.js b/src/pages/settings/SettingsNav.js
deleted file mode 100644
index 4df76995..00000000
--- a/src/pages/settings/SettingsNav.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import { NavLink } from "react-router-dom";
-
-const links = [
- { path: "/settings/dropdowns", label: "Dropdown Options" },
- { path: "/settings/task-templates", label: "Task Templates" },
-];
-
-export default function SettingsNav() {
- return (
-
- {links.map((link) => (
- `settingsNavLink ${isActive ? "active" : ""}`}
- >
- {link.label}
-
- ))}
-
- );
-}
diff --git a/src/pages/settings/TaskTemplatesPage.css b/src/pages/settings/TaskTemplatesPage.css
deleted file mode 100644
index c0d9b465..00000000
--- a/src/pages/settings/TaskTemplatesPage.css
+++ /dev/null
@@ -1,263 +0,0 @@
-.settingsNav {
- display: flex;
- gap: 0;
- margin-bottom: 20px;
- border-bottom: 2px solid #e5e5e5;
-}
-
-.settingsNavLink {
- padding: 8px 20px;
- font-size: 13px;
- font-weight: 600;
- color: #999;
- text-decoration: none;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
-}
-
-.settingsNavLink:hover {
- color: #333;
-}
-
-.settingsNavLink.active {
- color: #007bff;
- border-bottom-color: #007bff;
-}
-
-.taskTemplatesPage {
- padding: 20px;
-}
-
-.taskTemplatesHeader {
- margin-bottom: 24px;
-}
-
-.taskTemplatesHeader h2 {
- margin: 0 0 4px 0;
- font-size: 20px;
- font-weight: 600;
- color: #333;
-}
-
-.taskTemplatesHeader p {
- margin: 0;
- font-size: 13px;
- color: #999;
-}
-
-.taskTemplatesLayout {
- display: flex;
- gap: 0;
- border: 1px solid #ddd;
- border-radius: 6px;
- overflow: hidden;
- min-height: 500px;
-}
-
-.taskTemplatesSidebar {
- width: 240px;
- background: #f8f9fa;
- border-right: 1px solid #ddd;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
-}
-
-.taskTemplatesNewBtn {
- display: block;
- width: 100%;
- padding: 12px 16px;
- border: none;
- background: #007bff;
- color: #fff;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- text-align: left;
-}
-
-.taskTemplatesNewBtn:hover {
- background: #0069d9;
-}
-
-.taskTemplatesSidebarItem {
- padding: 10px 16px;
- cursor: pointer;
- border-bottom: 1px solid #eee;
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.taskTemplatesSidebarItem:hover {
- background: #eee;
-}
-
-.taskTemplatesSidebarItem.active {
- background: #fff;
- border-right: 2px solid #007bff;
-}
-
-.taskTemplatesSidebarName {
- font-size: 13px;
- font-weight: 500;
- color: #333;
-}
-
-.taskTemplatesSidebarCount {
- font-size: 11px;
- color: #999;
-}
-
-.taskTemplatesContent {
- flex: 1;
- padding: 20px;
- display: flex;
- flex-direction: column;
-}
-
-.taskTemplatesEmpty {
- display: flex;
- align-items: center;
- justify-content: center;
- flex: 1;
- color: #999;
- font-size: 14px;
-}
-
-.taskTemplatesLoading {
- padding: 20px;
- text-align: center;
- color: #999;
- font-size: 13px;
-}
-
-.taskTemplatesForm {
- flex: 1;
-}
-
-.taskTemplatesFormRow {
- display: flex;
- gap: 12px;
- margin-bottom: 20px;
-}
-
-.taskTemplatesFormField {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-.taskTemplatesFormField label {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- text-transform: uppercase;
-}
-
-.taskTemplatesItemsHeader {
- margin-bottom: 8px;
-}
-
-.taskTemplatesItemsHeader h4 {
- margin: 0;
- font-size: 13px;
- font-weight: 600;
- color: #333;
-}
-
-.taskTemplatesItemList {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-bottom: 12px;
-}
-
-.taskTemplatesItem {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 12px;
- background: #f8f9fa;
- border: 1px solid #eee;
- border-radius: 4px;
-}
-
-.taskTemplatesItemNum {
- font-size: 12px;
- font-weight: 600;
- color: #999;
- min-width: 20px;
-}
-
-.taskTemplatesItemText {
- flex: 1;
- font-size: 13px;
- color: #333;
-}
-
-.taskTemplatesItemActions {
- display: flex;
- gap: 2px;
-}
-
-.taskTemplatesItemBtn {
- width: 24px;
- height: 24px;
- border: none;
- background: none;
- color: #999;
- cursor: pointer;
- font-size: 12px;
- border-radius: 3px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.taskTemplatesItemBtn:hover:not(:disabled) {
- background: #e0e0e0;
- color: #333;
-}
-
-.taskTemplatesItemBtn:disabled {
- opacity: 0.3;
- cursor: not-allowed;
-}
-
-.taskTemplatesItemBtn.delete {
- font-size: 16px;
-}
-
-.taskTemplatesItemBtn.delete:hover {
- color: #dc3545;
- background: #fff5f5;
-}
-
-.taskTemplatesAddItem {
- display: flex;
- gap: 8px;
-}
-
-.taskTemplatesAddItem .form-control {
- flex: 1;
-}
-
-.taskTemplatesActions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding-top: 16px;
- border-top: 1px solid #eee;
- margin-top: 16px;
-}
-
-.taskTemplatesDeleteBtn {
- margin-right: auto;
- color: #dc3545;
-}
-
-.taskTemplatesDeleteBtn:hover {
- background: #fff5f5;
-}
diff --git a/src/pages/settings/TaskTemplatesPage.js b/src/pages/settings/TaskTemplatesPage.js
deleted file mode 100644
index db36e45a..00000000
--- a/src/pages/settings/TaskTemplatesPage.js
+++ /dev/null
@@ -1,203 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { taskTemplateApi } from "../../services/api";
-import { toast } from "react-toastify";
-import SettingsNav from "./SettingsNav";
-import "./TaskTemplatesPage.css";
-
-export default function TaskTemplatesPage() {
- const [templates, setTemplates] = useState([]);
- const [selectedId, setSelectedId] = useState(null);
- const [form, setForm] = useState({ name: "", description: "", items: [] });
- const [newItemText, setNewItemText] = useState("");
- const [saving, setSaving] = useState(false);
- const [loading, setLoading] = useState(true);
-
- const loadTemplates = async () => {
- try {
- const data = await taskTemplateApi.getAll();
- setTemplates(Array.isArray(data) ? data : []);
- } catch {
- toast.error("Failed to load templates");
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => { loadTemplates(); }, []);
-
- const handleSelect = (template) => {
- setSelectedId(template.id);
- setForm({
- name: template.name || "",
- description: template.description || "",
- items: (template.items || []).map((i, idx) => ({ text: i.itemText, sortOrder: i.sortOrder ?? idx })),
- });
- setNewItemText("");
- };
-
- const handleNew = () => {
- setSelectedId("new");
- setForm({ name: "", description: "", items: [] });
- setNewItemText("");
- };
-
- const handleAddItem = () => {
- if (!newItemText.trim()) return;
- setForm((f) => ({
- ...f,
- items: [...f.items, { text: newItemText.trim(), sortOrder: f.items.length }],
- }));
- setNewItemText("");
- };
-
- const handleRemoveItem = (idx) => {
- setForm((f) => ({
- ...f,
- items: f.items.filter((_, i) => i !== idx).map((item, i) => ({ ...item, sortOrder: i })),
- }));
- };
-
- const handleMoveItem = (idx, dir) => {
- setForm((f) => {
- const items = [...f.items];
- const target = idx + dir;
- if (target < 0 || target >= items.length) return f;
- [items[idx], items[target]] = [items[target], items[idx]];
- return { ...f, items: items.map((item, i) => ({ ...item, sortOrder: i })) };
- });
- };
-
- const handleSave = async () => {
- if (!form.name.trim()) { toast.error("Template name is required"); return; }
- setSaving(true);
- try {
- const payload = {
- name: form.name,
- description: form.description,
- isActive: true,
- items: form.items.map((i) => ({ itemText: i.text, sortOrder: i.sortOrder })),
- };
- if (selectedId === "new") {
- const result = await taskTemplateApi.create(payload);
- setSelectedId(result.id);
- toast.success("Template created");
- } else {
- await taskTemplateApi.update(selectedId, payload);
- toast.success("Template updated");
- }
- loadTemplates();
- } catch {
- toast.error("Failed to save template");
- } finally {
- setSaving(false);
- }
- };
-
- const handleDelete = async () => {
- if (!selectedId || selectedId === "new") return;
- try {
- await taskTemplateApi.delete(selectedId);
- setSelectedId(null);
- setForm({ name: "", description: "", items: [] });
- loadTemplates();
- toast.success("Template deleted");
- } catch {
- toast.error("Failed to delete template");
- }
- };
-
- return (
-
-
-
-
Task List Templates
-
Create reusable checklists for work order dispatches
-
-
-
-
-
- {loading ?
Loading...
: templates.map((t) => (
-
handleSelect(t)}
- >
- {t.name}
- {t.items?.length || 0} items
-
- ))}
-
-
-
- {!selectedId ? (
-
Select a template or create a new one
- ) : (
- <>
-
-
-
-
-
Checklist Items ({form.items.length})
-
-
-
- {form.items.map((item, idx) => (
-
-
{idx + 1}
-
{item.text}
-
-
-
-
-
-
- ))}
-
-
-
- setNewItemText(e.target.value)}
- onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleAddItem(); } }}
- placeholder="Add checklist item..."
- />
-
-
-
-
-
- {selectedId !== "new" && (
-
- )}
-
-
- >
- )}
-
-
-
- );
-}
diff --git a/src/pages/uplifts/UpliftQueuePage.css b/src/pages/uplifts/UpliftQueuePage.css
deleted file mode 100644
index 350047a8..00000000
--- a/src/pages/uplifts/UpliftQueuePage.css
+++ /dev/null
@@ -1,135 +0,0 @@
-.upliftQueuePage {
- padding: 24px;
-}
-
-.upliftQueueHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- margin-bottom: 16px;
-}
-
-.upliftQueueHeader h2 {
- margin: 0;
- font-size: 22px;
- color: #1f2937;
-}
-
-.upliftQueueFilters {
- display: flex;
- gap: 16px;
- align-items: center;
- font-size: 14px;
- color: #374151;
-}
-
-.upliftQueueFilters label {
- display: flex;
- gap: 6px;
- align-items: center;
-}
-
-.upliftQueueFilters select {
- padding: 6px 10px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- font-size: 14px;
- background: #fff;
-}
-
-.upliftQueueCount {
- color: #6b7280;
- font-size: 13px;
-}
-
-.upliftQueueLoading,
-.upliftQueueEmpty {
- padding: 40px;
- text-align: center;
- color: #6b7280;
- background: #fff;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
-}
-
-.upliftQueueTable {
- width: 100%;
- border-collapse: collapse;
- background: #fff;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
- overflow: hidden;
- font-size: 14px;
-}
-
-.upliftQueueTable th,
-.upliftQueueTable td {
- padding: 10px 12px;
- text-align: left;
- border-bottom: 1px solid #f1f3f5;
-}
-
-.upliftQueueTable th {
- background: #f9fafb;
- font-weight: 600;
- color: #374151;
- font-size: 13px;
-}
-
-.upliftQueueTable tr:last-child td {
- border-bottom: none;
-}
-
-.upliftStatusPill {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
-}
-
-.upliftStatusPill-pending { background: #fef3c7; color: #92400e; }
-.upliftStatusPill-approved { background: #d1fae5; color: #065f46; }
-.upliftStatusPill-denied { background: #fee2e2; color: #991b1b; }
-.upliftStatusPill-cancelled { background: #e5e7eb; color: #4b5563; }
-
-.upliftTier {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
-}
-
-.upliftTier-1 { background: #e0e7ff; color: #3730a3; }
-.upliftTier-2 { background: #fef3c7; color: #92400e; }
-
-.upliftActions {
- display: flex;
- gap: 6px;
- flex-wrap: wrap;
-}
-
-.upliftQueuePager {
- display: flex;
- gap: 12px;
- align-items: center;
- justify-content: center;
- margin-top: 16px;
- font-size: 14px;
-}
-
-.upliftQueuePager button {
- padding: 6px 14px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- background: #fff;
- cursor: pointer;
-}
-
-.upliftQueuePager button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
diff --git a/src/pages/uplifts/UpliftQueuePage.js b/src/pages/uplifts/UpliftQueuePage.js
deleted file mode 100644
index ebd7bc9d..00000000
--- a/src/pages/uplifts/UpliftQueuePage.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import React, { useCallback, useEffect, useState } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { upliftApi } from "../../services/api";
-import "./UpliftQueuePage.css";
-
-function formatDateTime(value) {
- if (!value) return "";
- const d = new Date(value);
- if (isNaN(d)) return "";
- const pad = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
-}
-
-const STATUS_OPTIONS = ["Pending", "Approved", "Denied", "Cancelled"];
-
-export default function UpliftQueuePage() {
- const navigate = useNavigate();
- const [status, setStatus] = useState("Pending");
- const [tier, setTier] = useState("");
- const [items, setItems] = useState([]);
- const [total, setTotal] = useState(0);
- const [page, setPage] = useState(1);
- const [pageSize] = useState(25);
- const [loading, setLoading] = useState(false);
-
- const load = useCallback(async () => {
- setLoading(true);
- try {
- const params = { status, page, pageSize };
- if (tier) params.tier = tier;
- const res = await upliftApi.list(params);
- const data = res?.data ?? res;
- setItems(data?.items || []);
- setTotal(data?.total || 0);
- } catch (err) {
- toast.error(err.message || "Failed to load uplift queue");
- } finally {
- setLoading(false);
- }
- }, [status, tier, page, pageSize]);
-
- useEffect(() => { load(); }, [load]);
-
- async function approve(req) {
- const note = window.prompt("Optional approval note:", "") || "";
- if (!window.confirm(`Approve uplift to $${Number(req.requestedNTE).toFixed(2)}?`)) return;
- try {
- await upliftApi.approve(req.id, note);
- toast.success("Uplift approved — NTE updated");
- load();
- } catch (err) {
- toast.error(err.message || "Failed to approve");
- }
- }
-
- async function deny(req) {
- const note = window.prompt("Denial reason (required):", "");
- if (!note || !note.trim()) {
- toast.error("A reason is required when denying.");
- return;
- }
- try {
- await upliftApi.deny(req.id, note.trim());
- toast.success("Uplift denied");
- load();
- } catch (err) {
- toast.error(err.message || "Failed to deny");
- }
- }
-
- const totalPages = Math.max(1, Math.ceil(total / pageSize));
-
- return (
-
-
-
Uplift Queue
-
-
-
- {total} total
-
-
-
- {loading ? (
- Loading…
- ) : items.length === 0 ? (
- No uplift requests match these filters.
- ) : (
-
-
-
- | Dispatch |
- PO |
- Vendor |
- Current NTE |
- Requested |
- Delta |
- Tier |
- Status |
- Requested At |
- Actions |
-
-
-
- {items.map((r) => (
-
- | {r.dispatchNumber} |
- {r.poNumber} |
- {r.vendorCompanyName} |
- ${Number(r.currentNTE ?? 0).toFixed(2)} |
- ${Number(r.requestedNTE).toFixed(2)} |
- ${Number(r.delta).toFixed(2)} |
-
-
- Tier {r.requiredTier}
-
- |
-
-
- {r.status}
-
- |
- {r.requestedAt ? formatDateTime(r.requestedAt) : ""} |
-
-
- {r.workOrderId && (
-
- )}
- {r.status === "Pending" && (
- <>
-
-
- >
- )}
-
- |
-
- ))}
-
-
- )}
-
- {totalPages > 1 && (
-
-
- Page {page} of {totalPages}
-
-
- )}
-
- );
-}
diff --git a/src/pages/user/api.js b/src/pages/user/api.js
deleted file mode 100644
index 8bea6d37..00000000
--- a/src/pages/user/api.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { post } from '../../apiUtil';
-import { API_URL } from '../../constants';
-
-export const login = (item) => post(`${API_URL}/Authentication/login`, item);
-
diff --git a/src/pages/vendor-portal/DashboardPage.js b/src/pages/vendor-portal/DashboardPage.js
deleted file mode 100644
index 780d89de..00000000
--- a/src/pages/vendor-portal/DashboardPage.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { useVendorPortal } from "./VendorPortalContext";
-import { vendorPortalApi } from "./api";
-import { statusClass, formatDateTime } from "./statusHelpers";
-
-const FILTERS = [
- { key: "open", label: "Open", match: (d) => d.status !== "Completed" && d.status !== "Verified" && d.status !== "Cancelled" },
- { key: "completed", label: "Completed", match: (d) => d.status === "Completed" || d.status === "Verified" },
- { key: "cancelled", label: "Cancelled", match: (d) => d.status === "Cancelled" },
- { key: "all", label: "All", match: () => true },
-];
-
-export default function DashboardPage() {
- const { token } = useVendorPortal();
- const navigate = useNavigate();
- const [dispatches, setDispatches] = useState([]);
- const [loading, setLoading] = useState(true);
- const [err, setErr] = useState(null);
- const [filter, setFilter] = useState("open");
-
- useEffect(() => {
- setLoading(true);
- vendorPortalApi
- .listDispatches(token)
- .then((data) => setDispatches(Array.isArray(data) ? data : []))
- .catch((e) => setErr(e.message))
- .finally(() => setLoading(false));
- }, [token]);
-
- const activeFilter = FILTERS.find((f) => f.key === filter) ?? FILTERS[0];
- const filtered = dispatches.filter(activeFilter.match);
-
- return (
- <>
-
-
Your Work
-
- Active dispatches and work orders assigned to your company.
-
-
- {FILTERS.map((f) => (
-
- ))}
-
-
-
- {loading && Loading dispatches…
}
- {err && {err}
}
-
- {!loading && !err && filtered.length === 0 && (
- No dispatches in this view.
- )}
-
-
- {filtered.map((d) => (
-
navigate(`../dispatch/${d.id}`)}
- >
-
-
- {d.dispatchNumber} — {d.workOrderTitle || "(untitled)"}
-
-
- WO#{d.internalWONumber}
- {d.locationName ? ` · ${d.locationName}` : ""}
- {d.locationCity ? `, ${d.locationCity}` : ""}
- {d.locationState ? `, ${d.locationState}` : ""}
- {d.scheduledDate ? ` · Scheduled ${formatDateTime(d.scheduledDate)}` : ""}
-
-
-
{d.status}
-
- ))}
-
- >
- );
-}
diff --git a/src/pages/vendor-portal/DispatchDetailPage.js b/src/pages/vendor-portal/DispatchDetailPage.js
deleted file mode 100644
index 6b1125a0..00000000
--- a/src/pages/vendor-portal/DispatchDetailPage.js
+++ /dev/null
@@ -1,488 +0,0 @@
-import React, { useCallback, useEffect, useState } from "react";
-import { Link, useParams } from "react-router-dom";
-import { useVendorPortal } from "./VendorPortalContext";
-import { vendorPortalApi } from "./api";
-import { statusClass, formatDateTime } from "./statusHelpers";
-import SignaturePad from "./SignaturePad";
-
-function ChecklistSection({ token, dispatchId, items, locked, onItemUpdated }) {
- async function toggle(item) {
- if (locked) return;
- try {
- const updated = await vendorPortalApi.updateChecklistItem(token, dispatchId, item.id, !item.isCompleted);
- onItemUpdated(updated);
- } catch (e) {
- alert(e.message);
- }
- }
-
- if (!items?.length) {
- return No checklist items for this dispatch.
;
- }
-
- return (
-
- {items.map((item) => (
-
- toggle(item)}
- disabled={locked}
- />
-
- {item.isCompleted && item.completedAt && (
- {formatDateTime(item.completedAt)}
- )}
-
- ))}
-
- );
-}
-
-function CommentsSection({ token, dispatchId, comments, onAdded }) {
- const [text, setText] = useState("");
- const [submitting, setSubmitting] = useState(false);
-
- async function submit(e) {
- e.preventDefault();
- if (!text.trim()) return;
- setSubmitting(true);
- try {
- const added = await vendorPortalApi.addComment(token, dispatchId, text.trim());
- onAdded(added);
- setText("");
- } catch (err) {
- alert(err.message);
- } finally {
- setSubmitting(false);
- }
- }
-
- return (
- <>
- {comments.length === 0 && No comments yet.
}
- {comments.map((c) => (
-
-
- {c.commenter || "Unknown"} · {formatDateTime(c.createdDate)}
-
-
{c.commenttext}
-
- ))}
-
- >
- );
-}
-
-function UpliftRequestsSection({ token, dispatchId, currentNTE, upliftRequests, locked, onChanged }) {
- const [requested, setRequested] = useState("");
- const [reason, setReason] = useState("");
- const [submitting, setSubmitting] = useState(false);
-
- const requests = upliftRequests || [];
- const pending = requests.find((r) => r.status === "Pending");
-
- async function submit(e) {
- e.preventDefault();
- const amt = parseFloat(requested);
- if (!Number.isFinite(amt) || amt <= 0) {
- alert("Enter a valid requested NTE amount.");
- return;
- }
- if (currentNTE != null && amt <= Number(currentNTE)) {
- alert(`Requested NTE must be greater than the current NTE ($${Number(currentNTE).toFixed(2)}).`);
- return;
- }
- if (!reason.trim()) {
- alert("Please include a reason for the uplift request.");
- return;
- }
- setSubmitting(true);
- try {
- await vendorPortalApi.requestUplift(token, dispatchId, { requestedNTE: amt, reason: reason.trim() });
- setRequested("");
- setReason("");
- await onChanged();
- } catch (err) {
- alert(err.message);
- } finally {
- setSubmitting(false);
- }
- }
-
- async function cancel(requestId) {
- if (!window.confirm("Cancel this uplift request?")) return;
- try {
- await vendorPortalApi.cancelUplift(token, dispatchId, requestId);
- await onChanged();
- } catch (err) {
- alert(err.message);
- }
- }
-
- return (
- <>
- {requests.length === 0 && !pending && (
-
- No uplift requests yet. Use the form below if the work will exceed the current NTE.
-
- )}
-
- {requests.map((r) => (
-
-
- Requested
- ${Number(r.requestedNTE).toFixed(2)} (was ${Number(r.currentNTE ?? 0).toFixed(2)})
-
-
- Status
-
- {r.status}
- {r.requiredTier === 2 && r.status === "Pending" && (
- · Manager approval required
- )}
-
-
- {r.vendorReason && (
-
Reason{r.vendorReason}
- )}
-
- Requested
- {formatDateTime(r.requestedAt)}
-
- {r.decidedAt && (
-
- Decided
-
- {formatDateTime(r.decidedAt)}
- {r.decidedByName ? ` by ${r.decidedByName}` : ""}
-
-
- )}
- {r.decisionNote && (
-
Note{r.decisionNote}
- )}
- {r.status === "Pending" && (
-
-
-
- )}
-
- ))}
-
- {!locked && !pending && (
-
- )}
- >
- );
-}
-
-function SignoffSection({ token, dispatchId, existing, signoffType, nameLabel, onAdded }) {
- const [name, setName] = useState("");
- const [method, setMethod] = useState("drawn");
- const [typedSig, setTypedSig] = useState("");
- const [drawnSig, setDrawnSig] = useState("");
- const [submitting, setSubmitting] = useState(false);
-
- const existingSignoff = existing?.find((s) => s.signoffType === signoffType);
- if (existingSignoff) {
- return (
-
-
Signed by{existingSignoff.name}
-
Method{existingSignoff.signatureMethod}
-
Signed at{formatDateTime(existingSignoff.signedAt)}
-
- );
- }
-
- async function submit(e) {
- e.preventDefault();
- const signature = method === "drawn" ? drawnSig : typedSig;
- if (!name.trim() || !signature) {
- alert("Name and signature are required");
- return;
- }
- setSubmitting(true);
- try {
- const added = await vendorPortalApi.addSignoff(token, dispatchId, {
- name: name.trim(),
- signature,
- signatureMethod: method,
- signoffType,
- });
- onAdded(added);
- } catch (err) {
- alert(err.message);
- } finally {
- setSubmitting(false);
- }
- }
-
- return (
-
- );
-}
-
-export default function DispatchDetailPage() {
- const { token } = useVendorPortal();
- const { id } = useParams();
- const dispatchId = Number(id);
- const [data, setData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [err, setErr] = useState(null);
- const [busy, setBusy] = useState(false);
-
- const refresh = useCallback(async () => {
- setLoading(true);
- try {
- const d = await vendorPortalApi.getDispatch(token, dispatchId);
- setData(d);
- } catch (e) {
- setErr(e.message);
- } finally {
- setLoading(false);
- }
- }, [token, dispatchId]);
-
- useEffect(() => { refresh(); }, [refresh]);
-
- async function runAction(fn, confirmMsg) {
- if (confirmMsg && !window.confirm(confirmMsg)) return;
- setBusy(true);
- try {
- await fn();
- await refresh();
- } catch (e) {
- alert(e.message);
- } finally {
- setBusy(false);
- }
- }
-
- if (loading && !data) return Loading dispatch…
;
- if (err && !data) return {err}
;
- if (!data) return null;
-
- const locked = data.status === "Verified" || data.status === "Cancelled";
-
- return (
- <>
- ← Back to dispatches
-
-
-
-
-
{data.dispatchNumber}
-
{data.workOrder?.workerOrderTitle}
-
-
{data.status}
-
-
-
- {data.workOrder?.internalWONumber &&
WO #{data.workOrder.internalWONumber}
}
- {data.poNumber &&
PO #{data.poNumber}
}
- {data.nteAmount != null &&
NTE${Number(data.nteAmount).toFixed(2)}
}
- {data.workOrder?.priority &&
Priority{data.workOrder.priority}
}
- {data.workOrder?.trade &&
Trade{data.workOrder.trade}{data.workOrder.subTrade ? ` / ${data.workOrder.subTrade}` : ""}
}
- {data.workOrder?.problem &&
Problem{data.workOrder.problem}
}
- {data.scheduledDate &&
Scheduled{formatDateTime(data.scheduledDate)}
}
- {data.acknowledgedAt &&
Acknowledged{formatDateTime(data.acknowledgedAt)}
}
- {data.completedDate &&
Completed{formatDateTime(data.completedDate)}
}
-
-
- {data.location && (
- <>
-
Location
-
{data.location.name}
-
- {[data.location.address, data.location.city, data.location.state, data.location.zipCode].filter(Boolean).join(", ")}
-
- >
- )}
-
- {(data.description || data.workOrder?.description) && (
- <>
-
Description
-
{data.description || data.workOrder?.description}
- >
- )}
-
-
- {data.status === "Sent" && (
-
- )}
- {data.status === "Acknowledged" && (
-
- )}
- {data.status === "In Progress" && (
-
- )}
- {!locked && data.status !== "Sent" && (
-
- )}
-
-
-
-
-
Checklist
- setData((d) => ({
- ...d,
- checklist: d.checklist.map((it) => it.id === updated.id ? { ...it, ...updated } : it),
- }))}
- />
-
-
-
-
NTE Uplift Requests
-
- Need more than the current NTE? Submit a request and the dispatcher will review.
-
-
-
-
-
-
Customer Signoff
-
- Have the on-site customer representative sign to acknowledge completion.
-
-
refresh()}
- />
-
-
-
-
Vendor Signoff
- refresh()}
- />
-
-
-
-
Comments
- setData((d) => ({ ...d, comments: [...(d.comments || []), c] }))}
- />
-
- >
- );
-}
diff --git a/src/pages/vendor-portal/PosPage.js b/src/pages/vendor-portal/PosPage.js
deleted file mode 100644
index 47aa6501..00000000
--- a/src/pages/vendor-portal/PosPage.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, { useEffect, useMemo, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { useVendorPortal } from "./VendorPortalContext";
-import { vendorPortalApi } from "./api";
-import { statusClass, formatDateTime } from "./statusHelpers";
-
-const FILTERS = [
- { key: "open", label: "Open", match: (d) => d.status !== "Completed" && d.status !== "Verified" && d.status !== "Cancelled" },
- { key: "completed", label: "Completed", match: (d) => d.status === "Completed" || d.status === "Verified" },
- { key: "cancelled", label: "Cancelled", match: (d) => d.status === "Cancelled" },
- { key: "all", label: "All", match: () => true },
-];
-
-function fmtDate(value) {
- if (!value) return "—";
- const d = new Date(value);
- if (isNaN(d)) return "—";
- const pad = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
-}
-
-function fmtMoney(value) {
- if (value == null) return "—";
- const n = Number(value);
- if (isNaN(n)) return "—";
- return `$${n.toFixed(2)}`;
-}
-
-export default function PosPage() {
- const { token } = useVendorPortal();
- const navigate = useNavigate();
- const [dispatches, setDispatches] = useState([]);
- const [loading, setLoading] = useState(true);
- const [err, setErr] = useState(null);
- const [filter, setFilter] = useState("open");
- const [search, setSearch] = useState("");
-
- useEffect(() => {
- setLoading(true);
- vendorPortalApi
- .listDispatches(token)
- .then((data) => setDispatches(Array.isArray(data) ? data : []))
- .catch((e) => setErr(e.message))
- .finally(() => setLoading(false));
- }, [token]);
-
- const activeFilter = FILTERS.find((f) => f.key === filter) ?? FILTERS[0];
-
- const visible = useMemo(() => {
- const needle = search.trim().toLowerCase();
- return dispatches.filter((d) => {
- if (!activeFilter.match(d)) return false;
- if (!needle) return true;
- return (
- (d.poNumber || "").toLowerCase().includes(needle) ||
- (d.dispatchNumber || "").toLowerCase().includes(needle) ||
- (d.internalWONumber || "").toLowerCase().includes(needle) ||
- (d.workOrderTitle || "").toLowerCase().includes(needle) ||
- (d.locationName || "").toLowerCase().includes(needle)
- );
- });
- }, [dispatches, activeFilter, search]);
-
- return (
- <>
-
-
Your Purchase Orders
-
- Every PO issued to your company, with current status and dollar value.
-
-
- {FILTERS.map((f) => (
-
- ))}
- setSearch(e.target.value)}
- style={{ marginLeft: "auto", minWidth: 240 }}
- />
-
-
-
- {loading && Loading POs…
}
- {err && {err}
}
-
- {!loading && !err && visible.length === 0 && (
- No POs match this view.
- )}
-
- {!loading && !err && visible.length > 0 && (
-
-
-
-
- | PO # |
- Dispatch # |
- Status |
- NTE |
- Location |
- Dispatched |
- Scheduled |
- Completed |
-
-
-
- {visible.map((d) => (
- navigate(`../dispatch/${d.id}`)}>
- |
- {d.poNumber || "—"}
- |
- {d.dispatchNumber} |
-
- {d.status}
- |
- {fmtMoney(d.nteAmount)} |
-
- {d.locationName || "—"}
- {d.locationCity ? ` · ${d.locationCity}` : ""}
- {d.locationState ? `, ${d.locationState}` : ""}
- |
- {d.dispatchedAt ? formatDateTime(d.dispatchedAt) : "—"} |
- {fmtDate(d.scheduledDate)} |
- {fmtDate(d.completedDate)} |
-
- ))}
-
-
-
- )}
- >
- );
-}
diff --git a/src/pages/vendor-portal/SignaturePad.js b/src/pages/vendor-portal/SignaturePad.js
deleted file mode 100644
index 028f4b01..00000000
--- a/src/pages/vendor-portal/SignaturePad.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import React, { useEffect, useRef, useState } from "react";
-
-export default function SignaturePad({ onChange }) {
- const canvasRef = useRef(null);
- const drawingRef = useRef(false);
- const lastRef = useRef({ x: 0, y: 0 });
- const [isEmpty, setIsEmpty] = useState(true);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- const ratio = window.devicePixelRatio || 1;
- const rect = canvas.getBoundingClientRect();
- canvas.width = rect.width * ratio;
- canvas.height = rect.height * ratio;
- const ctx = canvas.getContext("2d");
- ctx.scale(ratio, ratio);
- ctx.lineCap = "round";
- ctx.lineJoin = "round";
- ctx.strokeStyle = "#111";
- ctx.lineWidth = 2;
- }, []);
-
- function pointFromEvent(e) {
- const canvas = canvasRef.current;
- const rect = canvas.getBoundingClientRect();
- const touch = e.touches?.[0];
- const clientX = touch ? touch.clientX : e.clientX;
- const clientY = touch ? touch.clientY : e.clientY;
- return { x: clientX - rect.left, y: clientY - rect.top };
- }
-
- function start(e) {
- e.preventDefault();
- drawingRef.current = true;
- lastRef.current = pointFromEvent(e);
- }
-
- function move(e) {
- if (!drawingRef.current) return;
- e.preventDefault();
- const ctx = canvasRef.current.getContext("2d");
- const p = pointFromEvent(e);
- ctx.beginPath();
- ctx.moveTo(lastRef.current.x, lastRef.current.y);
- ctx.lineTo(p.x, p.y);
- ctx.stroke();
- lastRef.current = p;
- if (isEmpty) setIsEmpty(false);
- }
-
- function end(e) {
- if (!drawingRef.current) return;
- e.preventDefault();
- drawingRef.current = false;
- onChange?.(canvasRef.current.toDataURL("image/png"));
- }
-
- function clear() {
- const canvas = canvasRef.current;
- const ctx = canvas.getContext("2d");
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- setIsEmpty(true);
- onChange?.("");
- }
-
- return (
-
- );
-}
diff --git a/src/pages/vendor-portal/VendorPortalContext.js b/src/pages/vendor-portal/VendorPortalContext.js
deleted file mode 100644
index 1c03f1d9..00000000
--- a/src/pages/vendor-portal/VendorPortalContext.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { createContext, useContext, useEffect, useState } from "react";
-import { useParams } from "react-router-dom";
-import { vendorPortalApi } from "./api";
-
-const VendorPortalContext = createContext(null);
-
-export function useVendorPortal() {
- const ctx = useContext(VendorPortalContext);
- if (!ctx) throw new Error("useVendorPortal used outside provider");
- return ctx;
-}
-
-export function VendorPortalProvider({ children }) {
- const { token } = useParams();
- const [vendor, setVendor] = useState(null);
- const [status, setStatus] = useState("loading");
- const [error, setError] = useState(null);
-
- useEffect(() => {
- let cancelled = false;
- setStatus("loading");
- vendorPortalApi
- .session(token)
- .then((data) => {
- if (cancelled) return;
- setVendor(data);
- setStatus("ready");
- })
- .catch((err) => {
- if (cancelled) return;
- setError(err);
- setStatus("error");
- });
- return () => {
- cancelled = true;
- };
- }, [token]);
-
- const value = { token, vendor, status, error };
- return {children};
-}
diff --git a/src/pages/vendor-portal/VendorPortalLayout.js b/src/pages/vendor-portal/VendorPortalLayout.js
deleted file mode 100644
index ec849c40..00000000
--- a/src/pages/vendor-portal/VendorPortalLayout.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import React from "react";
-import { NavLink, Outlet, useLocation, useParams } from "react-router-dom";
-import { VendorPortalProvider, useVendorPortal } from "./VendorPortalContext";
-import "./vendor-portal.css";
-
-function Header() {
- const { vendor, status } = useVendorPortal();
- return (
-
-
Sea Haven Industries — Vendor Portal
- {status === "ready" && vendor?.companyName && (
-
{vendor.companyName}
- )}
-
- );
-}
-
-function Nav() {
- const { token } = useParams();
- const { pathname } = useLocation();
- const onDetail = /\/dispatch\//.test(pathname);
- return (
-
- );
-}
-
-function Body() {
- const { status, error } = useVendorPortal();
-
- if (status === "loading") {
- return ;
- }
- if (status === "error") {
- return (
-
-
-
Access Denied
-
{error?.message || "This link is invalid or has expired. Please contact your dispatcher for a new link."}
-
-
- );
- }
-
- return (
- <>
-
-
-
-
- >
- );
-}
-
-export default function VendorPortalLayout() {
- return (
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/vendor-portal/api.js b/src/pages/vendor-portal/api.js
deleted file mode 100644
index d67fff1c..00000000
--- a/src/pages/vendor-portal/api.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const PREFIX = "/api/vendor-portal";
-
-async function call(token, path, { method = "GET", body } = {}) {
- const res = await fetch(`${PREFIX}${path}`, {
- method,
- headers: {
- "Content-Type": "application/json",
- "X-Vendor-Token": token,
- },
- body: body ? JSON.stringify(body) : undefined,
- });
- if (res.status === 401) {
- const err = new Error("Invalid or expired token");
- err.status = 401;
- throw err;
- }
- const payload = await res.json().catch(() => ({}));
- if (!res.ok) {
- const err = new Error(payload?.message || payload?.Message || `HTTP ${res.status}`);
- err.status = res.status;
- throw err;
- }
- return payload?.data ?? payload?.Data ?? payload;
-}
-
-export const vendorPortalApi = {
- session: (token) => call(token, "/session"),
- listDispatches: (token, status) => call(token, status ? `/dispatches?status=${encodeURIComponent(status)}` : "/dispatches"),
- getDispatch: (token, id) => call(token, `/dispatches/${id}`),
- accept: (token, id) => call(token, `/dispatches/${id}/accept`, { method: "POST" }),
- changeStatus: (token, id, status) => call(token, `/dispatches/${id}/status`, { method: "POST", body: { status } }),
- requestCancel: (token, id, reason) => call(token, `/dispatches/${id}/request-cancel`, { method: "POST", body: { reason } }),
- updateChecklistItem: (token, id, itemId, isCompleted) =>
- call(token, `/dispatches/${id}/checklist/${itemId}`, { method: "POST", body: { isCompleted } }),
- addSignoff: (token, id, payload) => call(token, `/dispatches/${id}/signoff`, { method: "POST", body: payload }),
- addComment: (token, id, commentText) => call(token, `/dispatches/${id}/comments`, { method: "POST", body: { commentText } }),
- requestUplift: (token, id, payload) =>
- call(token, `/dispatches/${id}/uplift-request`, { method: "POST", body: payload }),
- cancelUplift: (token, id, requestId) =>
- call(token, `/dispatches/${id}/uplift-request/${requestId}/cancel`, { method: "POST" }),
-};
diff --git a/src/pages/vendor-portal/statusHelpers.js b/src/pages/vendor-portal/statusHelpers.js
deleted file mode 100644
index 496a0704..00000000
--- a/src/pages/vendor-portal/statusHelpers.js
+++ /dev/null
@@ -1,18 +0,0 @@
-export function statusClass(status) {
- const map = {
- Sent: "vp-status-sent",
- Acknowledged: "vp-status-acknowledged",
- "In Progress": "vp-status-in-progress",
- Completed: "vp-status-completed",
- Verified: "vp-status-verified",
- Cancelled: "vp-status-cancelled",
- };
- return `vp-status-pill ${map[status] || ""}`;
-}
-
-export function formatDateTime(d) {
- if (!d) return "";
- const date = new Date(d);
- if (isNaN(date.getTime())) return "";
- return date.toLocaleString();
-}
diff --git a/src/pages/vendor-portal/vendor-portal.css b/src/pages/vendor-portal/vendor-portal.css
deleted file mode 100644
index 6238d97a..00000000
--- a/src/pages/vendor-portal/vendor-portal.css
+++ /dev/null
@@ -1,375 +0,0 @@
-.vp-shell {
- min-height: 100vh;
- background: #f5f6f8;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- color: #222;
-}
-
-.vp-header {
- background: #1f2d3d;
- color: #fff;
- padding: 14px 24px;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.vp-brand {
- font-size: 18px;
- font-weight: 600;
- letter-spacing: 0.3px;
-}
-
-.vp-vendor-name {
- font-size: 13px;
- opacity: 0.85;
-}
-
-.vp-container {
- max-width: 1080px;
- margin: 0 auto;
- padding: 24px;
-}
-
-.vp-card {
- background: #fff;
- border: 1px solid #e4e7eb;
- border-radius: 10px;
- padding: 20px;
- margin-bottom: 16px;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
-}
-
-.vp-card h2 {
- margin: 0 0 4px;
- font-size: 20px;
-}
-
-.vp-card h3 {
- margin: 0 0 12px;
- font-size: 16px;
- color: #1f2d3d;
-}
-
-.vp-muted {
- color: #6b7280;
- font-size: 13px;
-}
-
-.vp-row {
- display: grid;
- grid-template-columns: 140px 1fr;
- gap: 8px;
- margin: 6px 0;
- font-size: 14px;
-}
-
-.vp-row strong {
- color: #374151;
-}
-
-.vp-dispatch-list {
- display: grid;
- gap: 12px;
-}
-
-.vp-dispatch-card {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- background: #fff;
- border: 1px solid #e4e7eb;
- border-radius: 10px;
- cursor: pointer;
- transition: border-color 0.15s, box-shadow 0.15s;
-}
-
-.vp-dispatch-card:hover {
- border-color: #3b82f6;
- box-shadow: 0 2px 6px rgba(59, 130, 246, 0.12);
-}
-
-.vp-dispatch-title {
- font-weight: 600;
- font-size: 15px;
- margin-bottom: 4px;
-}
-
-.vp-dispatch-meta {
- font-size: 13px;
- color: #6b7280;
-}
-
-.vp-status-pill {
- display: inline-block;
- padding: 4px 10px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- letter-spacing: 0.2px;
-}
-
-.vp-status-sent { background: #fef3c7; color: #92400e; }
-.vp-status-acknowledged { background: #dbeafe; color: #1e40af; }
-.vp-status-in-progress { background: #e0e7ff; color: #3730a3; }
-.vp-status-completed { background: #d1fae5; color: #065f46; }
-.vp-status-verified { background: #cffafe; color: #155e75; }
-.vp-status-cancelled { background: #fee2e2; color: #991b1b; }
-
-.vp-btn {
- display: inline-block;
- padding: 10px 18px;
- font-size: 14px;
- font-weight: 600;
- border-radius: 6px;
- border: none;
- cursor: pointer;
- transition: background 0.15s;
-}
-
-.vp-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.vp-btn-primary { background: #2563eb; color: #fff; }
-.vp-btn-primary:hover:not(:disabled) { background: #1d4ed8; }
-.vp-btn-success { background: #10b981; color: #fff; }
-.vp-btn-success:hover:not(:disabled) { background: #059669; }
-.vp-btn-warn { background: #f59e0b; color: #fff; }
-.vp-btn-warn:hover:not(:disabled) { background: #d97706; }
-.vp-btn-danger { background: #ef4444; color: #fff; }
-.vp-btn-danger:hover:not(:disabled) { background: #dc2626; }
-.vp-btn-ghost { background: transparent; color: #374151; border: 1px solid #d1d5db; }
-.vp-btn-ghost:hover:not(:disabled) { background: #f3f4f6; }
-
-.vp-actions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
- margin-top: 12px;
-}
-
-.vp-checklist-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 0;
- border-bottom: 1px solid #f1f3f5;
- font-size: 14px;
-}
-
-.vp-checklist-item:last-child { border-bottom: none; }
-
-.vp-checklist-item input[type="checkbox"] {
- width: 18px;
- height: 18px;
- cursor: pointer;
-}
-
-.vp-checklist-item.done label { text-decoration: line-through; color: #9ca3af; }
-
-.vp-comment {
- padding: 10px 12px;
- background: #f9fafb;
- border-left: 3px solid #d1d5db;
- margin-bottom: 8px;
- font-size: 14px;
-}
-
-.vp-comment.vendor { border-left-color: #10b981; }
-.vp-comment.dispatcher { border-left-color: #3b82f6; }
-.vp-comment.customer { border-left-color: #8b5cf6; }
-.vp-comment.internal { border-left-color: #f59e0b; }
-
-.vp-comment-meta {
- font-size: 12px;
- color: #6b7280;
- margin-bottom: 4px;
-}
-
-.vp-textarea, .vp-input {
- width: 100%;
- padding: 10px 12px;
- font-size: 14px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- font-family: inherit;
- box-sizing: border-box;
-}
-
-.vp-textarea { min-height: 80px; resize: vertical; }
-
-.vp-error {
- background: #fee2e2;
- color: #991b1b;
- padding: 16px;
- border-radius: 8px;
- text-align: center;
-}
-
-.vp-empty {
- text-align: center;
- padding: 40px;
- color: #6b7280;
-}
-
-.vp-signature-canvas {
- border: 1px dashed #9ca3af;
- border-radius: 6px;
- background: #fff;
- touch-action: none;
- display: block;
- width: 100%;
- max-width: 500px;
- height: 180px;
-}
-
-.vp-back-link {
- color: #2563eb;
- text-decoration: none;
- font-size: 14px;
- display: inline-block;
- margin-bottom: 12px;
-}
-
-.vp-back-link:hover { text-decoration: underline; }
-
-.vp-uplift-row {
- padding: 12px;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
- background: #fafbfc;
- margin-bottom: 10px;
-}
-
-.vp-uplift-status-pending {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #fef3c7;
- color: #92400e;
-}
-
-.vp-uplift-status-approved {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #d1fae5;
- color: #065f46;
-}
-
-.vp-uplift-status-denied {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #fee2e2;
- color: #991b1b;
-}
-
-.vp-uplift-status-cancelled {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #e5e7eb;
- color: #4b5563;
-}
-
-.vp-nav {
- background: #fff;
- border-bottom: 1px solid #e4e7eb;
- padding: 0 24px;
-}
-
-.vp-nav-inner {
- max-width: 1080px;
- margin: 0 auto;
- display: flex;
- gap: 4px;
-}
-
-.vp-nav-tab {
- background: none;
- border: none;
- padding: 12px 16px;
- font-size: 14px;
- font-weight: 500;
- color: #6b7280;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- transition: color 120ms, border-color 120ms;
- text-decoration: none;
- display: inline-block;
-}
-
-.vp-nav-tab:hover {
- color: #1f2d3d;
-}
-
-.vp-nav-tab.active {
- color: #1f2d3d;
- border-bottom-color: #2563eb;
-}
-
-.vp-po-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 14px;
-}
-
-.vp-po-table th,
-.vp-po-table td {
- padding: 10px 12px;
- text-align: left;
- border-bottom: 1px solid #f1f3f5;
-}
-
-.vp-po-table th {
- background: #f9fafb;
- font-weight: 600;
- color: #374151;
- font-size: 13px;
-}
-
-.vp-po-table tbody tr {
- cursor: pointer;
- transition: background 120ms;
-}
-
-.vp-po-table tbody tr:hover {
- background: #f9fafb;
-}
-
-.vp-po-table tbody tr:last-child td {
- border-bottom: none;
-}
-
-.vp-po-number {
- font-weight: 600;
- color: #1f2d3d;
-}
-
-.vp-po-muted-cell {
- color: #6b7280;
- font-size: 13px;
-}
-
-.vp-po-uplift-badge {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #fef3c7;
- color: #92400e;
- margin-left: 6px;
-}
diff --git a/src/pages/vendor-pos/list/List.js b/src/pages/vendor-pos/list/List.js
deleted file mode 100644
index 3998d92d..00000000
--- a/src/pages/vendor-pos/list/List.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useNavigate } from "react-router";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchDispatchesPaged } from "./api";
-import { vendorApi } from "../../../services/api";
-import Table from "./Table";
-import "./styles.css";
-
-const STATUS_OPTIONS = ["", "Sent", "Acknowledged", "In Progress", "Completed", "Verified", "Cancelled"];
-
-export default function VendorPoList() {
- const navigate = useNavigate();
- const [status, setStatus] = useState("");
- const [vendorId, setVendorId] = useState("");
- const [dateFrom, setDateFrom] = useState("");
- const [dateTo, setDateTo] = useState("");
- const [vendors, setVendors] = useState([]);
-
- const extraParams = { status, vendorId, dateFrom, dateTo };
-
- const {
- rows, search, setSearch, page, setPage, pageSize, setPageSize,
- totalCount, loading, err,
- } = usePaginatedList(fetchDispatchesPaged, extraParams);
-
- useEffect(() => {
- vendorApi.getAll({ page: 1, pageSize: 500 })
- .then((res) => {
- const data = res?.data ?? res?.Data ?? res;
- const items = data?.items || data || [];
- setVendors(
- items.map((v) => ({
- id: v.id ?? v.Id,
- companyName: v.companyName ?? v.CompanyName ?? "",
- })).filter((v) => v.companyName)
- );
- })
- .catch(() => setVendors([]));
- }, []);
-
- const clearFilters = () => {
- setStatus("");
- setVendorId("");
- setDateFrom("");
- setDateTo("");
- setPage(1);
- };
-
- return (
-
-
-
-
Vendor POs
- {totalCount} total
-
-
- { setSearch(e.target.value); setPage(1); }}
- placeholder="Search by PO #, Dispatch #, vendor, WO #, location..."
- />
-
-
-
-
-
-
-
-
-
-
-
- navigate(`/vendor-pos/${item.id}`)}
- page={page}
- setPage={setPage}
- pageSize={pageSize}
- setPageSize={setPageSize}
- totalCount={totalCount}
- />
-
- );
-}
diff --git a/src/pages/vendor-pos/list/Table.js b/src/pages/vendor-pos/list/Table.js
deleted file mode 100644
index 39438c2d..00000000
--- a/src/pages/vendor-pos/list/Table.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-function fmtDate(value) {
- if (!value) return "";
- const d = new Date(value);
- if (isNaN(d)) return "";
- const pad = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
-}
-
-function fmtMoney(value) {
- if (value == null) return "";
- const n = Number(value);
- if (isNaN(n)) return "";
- return `$${n.toFixed(2)}`;
-}
-
-export default function Table({ loading, error, rows, onView, page, setPage, pageSize, setPageSize, totalCount }) {
- const columns = [
- {
- key: "poNumber",
- label: "PO #",
- render: (val, row) => (
-
- { e.preventDefault(); onView && onView(row); }}>
- {val || "—"}
-
-
- ),
- },
- { key: "dispatchNumber", label: "Dispatch #" },
- { key: "vendorName", label: "Vendor" },
- { key: "primaryWOInternalNumber", label: "WO #" },
- {
- key: "workOrderCount",
- label: "WOs",
- render: (val) => (val == null ? "" : String(val)),
- },
- { key: "locationName", label: "Location" },
- {
- key: "nteAmount",
- label: "NTE",
- render: (val) => fmtMoney(val),
- },
- {
- key: "status",
- label: "Status",
- render: (val) => (
-
- {val || "—"}
-
- ),
- },
- {
- key: "pendingUpliftCount",
- label: "Uplifts",
- render: (val) => (val > 0 ? {val} pending : ""),
- },
- {
- key: "dispatchedAt",
- label: "Dispatched",
- render: (val) => fmtDate(val),
- },
- {
- key: "scheduledDate",
- label: "Scheduled",
- render: (val) => fmtDate(val),
- },
- {
- key: "completedDate",
- label: "Completed",
- render: (val) => fmtDate(val),
- },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/vendor-pos/list/api.js b/src/pages/vendor-pos/list/api.js
deleted file mode 100644
index 066c21eb..00000000
--- a/src/pages/vendor-pos/list/api.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { dispatchApi } from "../../../services/api";
-
-export const fetchDispatchesPaged = async ({
- search = "",
- page = 1,
- pageSize = 12,
- sortBy = "dispatchedAt",
- sortDir = "desc",
- status = "",
- vendorId = "",
- dateFrom = "",
- dateTo = "",
-}) => {
- const params = { page, pageSize, sortBy, sortDir };
- if (search) params.search = search;
- if (status) params.status = status;
- if (vendorId) params.vendorId = vendorId;
- if (dateFrom) params.dateFrom = dateFrom;
- if (dateTo) params.dateTo = dateTo;
-
- const res = await dispatchApi.list(params);
- const data = res?.data ?? res?.Data ?? res;
- const items = data?.items || [];
- return {
- rows: items,
- page: data?.page ?? page,
- pageSize: data?.pageSize ?? pageSize,
- totalCount: data?.total ?? items.length,
- totalPages: Math.max(1, Math.ceil((data?.total ?? items.length) / pageSize)),
- };
-};
diff --git a/src/pages/vendor-pos/list/styles.css b/src/pages/vendor-pos/list/styles.css
deleted file mode 100644
index f8d2cc33..00000000
--- a/src/pages/vendor-pos/list/styles.css
+++ /dev/null
@@ -1,111 +0,0 @@
-.vendorPoListPage {
- padding: 24px;
-}
-
-.vendorPoListToolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 12px;
- margin-bottom: 16px;
-}
-
-.vendorPoListToolbarLeft {
- display: flex;
- align-items: baseline;
- gap: 12px;
-}
-
-.vendorPoListTitle {
- margin: 0;
- font-size: 22px;
- color: #1f2937;
-}
-
-.vendorPoListCount {
- color: #6b7280;
- font-size: 13px;
-}
-
-.vendorPoListToolbarRight {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-.vendorPoSearchInput {
- padding: 7px 12px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- font-size: 14px;
- min-width: 240px;
- background: #fff;
-}
-
-.vendorPoListFilters {
- display: flex;
- gap: 12px;
- align-items: center;
- flex-wrap: wrap;
- margin-bottom: 12px;
- padding: 10px 12px;
- background: #f9fafb;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
- font-size: 13px;
- color: #374151;
-}
-
-.vendorPoListFilters label {
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.vendorPoListFilters select,
-.vendorPoListFilters input[type="date"] {
- padding: 5px 8px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- font-size: 13px;
- background: #fff;
-}
-
-.vendorPoListFiltersClear {
- border: 1px solid #d1d5db;
- background: #fff;
- border-radius: 6px;
- padding: 5px 10px;
- cursor: pointer;
- font-size: 12px;
- color: #374151;
-}
-
-.vendorPoStatusPill {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #e5e7eb;
- color: #4b5563;
-}
-
-.vendorPoStatusPill-sent { background: #dbeafe; color: #1e40af; }
-.vendorPoStatusPill-acknowledged { background: #e0e7ff; color: #3730a3; }
-.vendorPoStatusPill-in-progress { background: #fef3c7; color: #92400e; }
-.vendorPoStatusPill-completed { background: #d1fae5; color: #065f46; }
-.vendorPoStatusPill-verified { background: #bbf7d0; color: #14532d; }
-.vendorPoStatusPill-cancelled { background: #fee2e2; color: #991b1b; }
-
-.vendorPoUpliftBadge {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #fef3c7;
- color: #92400e;
-}
diff --git a/src/pages/vendor-pos/view/VendorPoViewPage.css b/src/pages/vendor-pos/view/VendorPoViewPage.css
deleted file mode 100644
index 8c43cdc8..00000000
--- a/src/pages/vendor-pos/view/VendorPoViewPage.css
+++ /dev/null
@@ -1,297 +0,0 @@
-.vendorPoPage {
- padding: 24px;
- max-width: 1200px;
- margin: 0 auto;
-}
-
-.vendorPoTopBar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- flex-wrap: wrap;
- gap: 12px;
-}
-
-.vendorPoTopBar .vendorPoBack {
- background: none;
- border: none;
- color: #2563eb;
- cursor: pointer;
- font-size: 14px;
- padding: 0;
-}
-
-.vendorPoTopActions {
- display: flex;
- gap: 8px;
-}
-
-.vendorPoPrintBtn {
- background: #fff;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- padding: 6px 14px;
- cursor: pointer;
- font-size: 14px;
- color: #374151;
-}
-
-.vendorPoPrintBtn:hover { background: #f9fafb; }
-
-.vendorPoHeader {
- background: #fff;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
- padding: 20px 24px;
- margin-bottom: 16px;
-}
-
-.vendorPoHeader h1 {
- margin: 0 0 4px 0;
- font-size: 24px;
- color: #111827;
-}
-
-.vendorPoHeaderSub {
- color: #6b7280;
- font-size: 14px;
- display: flex;
- gap: 12px;
- flex-wrap: wrap;
- align-items: center;
-}
-
-.vendorPoStatusPill {
- display: inline-block;
- padding: 3px 10px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
- background: #e5e7eb;
- color: #4b5563;
-}
-
-.vendorPoStatusPill-sent { background: #dbeafe; color: #1e40af; }
-.vendorPoStatusPill-acknowledged { background: #e0e7ff; color: #3730a3; }
-.vendorPoStatusPill-in-progress { background: #fef3c7; color: #92400e; }
-.vendorPoStatusPill-completed { background: #d1fae5; color: #065f46; }
-.vendorPoStatusPill-verified { background: #bbf7d0; color: #14532d; }
-.vendorPoStatusPill-cancelled { background: #fee2e2; color: #991b1b; }
-
-.vendorPoCard {
- background: #fff;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
- padding: 16px 20px;
- margin-bottom: 16px;
-}
-
-.vendorPoCard h3 {
- margin: 0 0 12px 0;
- font-size: 16px;
- color: #1f2937;
- border-bottom: 1px solid #f1f3f5;
- padding-bottom: 8px;
-}
-
-.vendorPoInfoGrid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
- gap: 12px 24px;
- font-size: 14px;
-}
-
-.vendorPoInfoItem {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.vendorPoInfoLabel {
- font-size: 12px;
- color: #6b7280;
- text-transform: uppercase;
- letter-spacing: 0.02em;
-}
-
-.vendorPoInfoValue {
- font-size: 14px;
- color: #1f2937;
- font-weight: 500;
-}
-
-.vendorPoTable {
- width: 100%;
- border-collapse: collapse;
- font-size: 14px;
-}
-
-.vendorPoTable th,
-.vendorPoTable td {
- padding: 8px 10px;
- text-align: left;
- border-bottom: 1px solid #f1f3f5;
-}
-
-.vendorPoTable th {
- background: #f9fafb;
- font-weight: 600;
- color: #374151;
- font-size: 13px;
-}
-
-.vendorPoTable tr:last-child td {
- border-bottom: none;
-}
-
-.vendorPoTable a {
- color: #2563eb;
- text-decoration: none;
-}
-.vendorPoTable a:hover { text-decoration: underline; }
-
-.vendorPoChecklistItem {
- display: flex;
- gap: 8px;
- padding: 6px 0;
- border-bottom: 1px solid #f1f3f5;
- font-size: 14px;
-}
-.vendorPoChecklistItem:last-child { border-bottom: none; }
-.vendorPoChecklistItem.done { color: #6b7280; text-decoration: line-through; }
-
-.vendorPoSignoffGrid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 16px;
-}
-
-.vendorPoSignoff {
- border: 1px solid #e4e7eb;
- border-radius: 6px;
- padding: 12px;
- background: #fafbfc;
-}
-
-.vendorPoSignoff h4 {
- margin: 0 0 8px 0;
- font-size: 13px;
- text-transform: uppercase;
- color: #374151;
- letter-spacing: 0.03em;
-}
-
-.vendorPoSignature {
- margin-top: 8px;
- max-width: 100%;
- max-height: 120px;
- border: 1px solid #e4e7eb;
- background: #fff;
- padding: 4px;
-}
-
-.vendorPoTypedSignature {
- font-family: "Dancing Script", "Great Vibes", cursive;
- font-size: 28px;
- color: #1f2937;
- padding: 8px 0;
-}
-
-.vendorPoComment {
- padding: 8px 0;
- border-bottom: 1px solid #f1f3f5;
- font-size: 14px;
-}
-.vendorPoComment:last-child { border-bottom: none; }
-.vendorPoComment-header {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- margin-bottom: 4px;
- font-size: 12px;
- color: #6b7280;
-}
-.vendorPoComment-author {
- font-weight: 600;
- color: #374151;
-}
-.vendorPoComment-body {
- white-space: pre-wrap;
- color: #1f2937;
-}
-
-.vendorPoUpliftRow {
- padding: 10px 12px;
- border: 1px solid #e4e7eb;
- border-radius: 6px;
- margin-bottom: 8px;
- background: #fafbfc;
- font-size: 14px;
-}
-.vendorPoUpliftRow-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 12px;
- flex-wrap: wrap;
-}
-.vendorPoUpliftAmount {
- font-weight: 600;
- color: #1f2937;
-}
-.vendorPoUpliftStatus {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
-}
-.vendorPoUpliftStatus-pending { background: #fef3c7; color: #92400e; }
-.vendorPoUpliftStatus-approved { background: #d1fae5; color: #065f46; }
-.vendorPoUpliftStatus-denied { background: #fee2e2; color: #991b1b; }
-.vendorPoUpliftStatus-cancelled { background: #e5e7eb; color: #4b5563; }
-
-.vendorPoEmpty {
- color: #6b7280;
- font-style: italic;
- padding: 6px 0;
- font-size: 14px;
-}
-
-.vendorPoAuditTable td:first-child {
- color: #6b7280;
- white-space: nowrap;
- font-size: 13px;
-}
-
-@media print {
- body {
- background: #fff !important;
- }
- .sidebar,
- .topbar,
- .vendorPoBack,
- .vendorPoPrintBtn,
- .vendorPoTopActions {
- display: none !important;
- }
- .app.nav-collapsed .mainArea,
- .app .mainArea {
- margin-left: 0 !important;
- }
- .vendorPoPage {
- max-width: 100% !important;
- padding: 0 !important;
- }
- .vendorPoCard,
- .vendorPoHeader {
- break-inside: avoid;
- box-shadow: none !important;
- border: 1px solid #ccc !important;
- }
- .vendorPoPage a {
- color: #000 !important;
- text-decoration: none !important;
- }
-}
diff --git a/src/pages/vendor-pos/view/VendorPoViewPage.js b/src/pages/vendor-pos/view/VendorPoViewPage.js
deleted file mode 100644
index aca89dcc..00000000
--- a/src/pages/vendor-pos/view/VendorPoViewPage.js
+++ /dev/null
@@ -1,363 +0,0 @@
-import React, { useCallback, useEffect, useState } from "react";
-import { useNavigate, useParams } from "react-router";
-import { toast } from "react-toastify";
-import { dispatchApi, upliftApi } from "../../../services/api";
-import "./VendorPoViewPage.css";
-
-function fmtDate(value) {
- if (!value) return "";
- const d = new Date(value);
- if (isNaN(d)) return "";
- const pad = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
-}
-
-function fmtDateTime(value) {
- if (!value) return "";
- const d = new Date(value);
- if (isNaN(d)) return "";
- const pad = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
-}
-
-function fmtMoney(value) {
- if (value == null) return "—";
- const n = Number(value);
- if (isNaN(n)) return "—";
- return `$${n.toFixed(2)}`;
-}
-
-function Info({ label, children }) {
- return (
-
- {label}
- {children == null || children === "" ? "—" : children}
-
- );
-}
-
-export default function VendorPoViewPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const [dispatch, setDispatch] = useState(null);
- const [loading, setLoading] = useState(true);
- const [err, setErr] = useState("");
-
- const load = useCallback(async () => {
- setLoading(true);
- setErr("");
- try {
- const res = await dispatchApi.getById(id);
- const data = res?.data ?? res?.Data ?? res;
- setDispatch(data);
- } catch (e) {
- setErr(e.message || "Failed to load");
- } finally {
- setLoading(false);
- }
- }, [id]);
-
- useEffect(() => { load(); }, [load]);
-
- const handleApproveUplift = async (u) => {
- const note = window.prompt("Optional approval note:", "") || "";
- if (!window.confirm(`Approve uplift to ${fmtMoney(u.requestedNTE)}?`)) return;
- try {
- await upliftApi.approve(u.id, note);
- toast.success("Uplift approved — NTE updated");
- load();
- } catch (e) {
- toast.error(e.message || "Failed to approve");
- }
- };
-
- const handleDenyUplift = async (u) => {
- const note = window.prompt("Denial reason (required):", "");
- if (!note || !note.trim()) {
- toast.error("A reason is required when denying.");
- return;
- }
- try {
- await upliftApi.deny(u.id, note.trim());
- toast.success("Uplift denied");
- load();
- } catch (e) {
- toast.error(e.message || "Failed to deny");
- }
- };
-
- if (loading) return ;
- if (err) return ;
- if (!dispatch) return ;
-
- const vendor = dispatch.vendor || dispatch.Vendor;
- const primaryLocation = dispatch.primaryLocation || dispatch.PrimaryLocation;
- const workOrders = dispatch.workOrders || dispatch.WorkOrders || [];
- const checklist = dispatch.checklist || dispatch.Checklist || [];
- const signoffs = dispatch.signoffs || dispatch.Signoffs || [];
- const comments = dispatch.comments || dispatch.Comments || [];
- const upliftRequests = dispatch.upliftRequests || dispatch.UpliftRequests || [];
- const auditLog = dispatch.auditLog || dispatch.AuditLog || [];
-
- const statusClass = String(dispatch.status || dispatch.Status || "").toLowerCase().replace(/\s+/g, "-");
-
- return (
-
-
-
-
-
-
-
-
-
-
{dispatch.poNumber || dispatch.PONumber || "(no PO #)"}
-
- Dispatch {dispatch.dispatchNumber || dispatch.DispatchNumber || "—"}
- {dispatch.status || dispatch.Status || "—"}
- {vendor?.companyName && · {vendor.companyName}}
- {(dispatch.nteAmount ?? dispatch.NTEAmount) != null && (
- · NTE {fmtMoney(dispatch.nteAmount ?? dispatch.NTEAmount)}
- )}
-
-
-
-
-
Vendor
- {vendor ? (
-
- {vendor.companyName}
- {vendor.contactName}
- {vendor.email}
- {vendor.phone}
-
- {[vendor.address, vendor.city, vendor.state, vendor.zip].filter(Boolean).join(", ")}
-
- {vendor.tradeSpecialties}
-
- ) : (
-
No vendor attached.
- )}
-
-
-
-
PO Details
-
- {dispatch.poNumber || dispatch.PONumber}
- {dispatch.dispatchNumber || dispatch.DispatchNumber}
- {dispatch.status || dispatch.Status}
- {fmtMoney(dispatch.nteAmount ?? dispatch.NTEAmount)}
- {fmtDateTime(dispatch.dispatchedAt ?? dispatch.DispatchedAt)}
- {fmtDateTime(dispatch.acknowledgedAt ?? dispatch.AcknowledgedAt)}
- {fmtDate(dispatch.scheduledDate ?? dispatch.ScheduledDate)}
- {fmtDate(dispatch.completedDate ?? dispatch.CompletedDate)}
- {fmtDateTime(dispatch.verifiedAt ?? dispatch.VerifiedAt)}
- {(dispatch.emailSent ?? dispatch.EmailSent) ? "Yes" : "No"}
-
- {(dispatch.description || dispatch.Description) && (
-
-
Description
-
{dispatch.description || dispatch.Description}
-
- )}
-
-
- {primaryLocation && (
-
-
Site
-
- {primaryLocation.name}
-
- {[primaryLocation.address, primaryLocation.city, primaryLocation.state, primaryLocation.zipCode].filter(Boolean).join(", ")}
-
-
-
- )}
-
-
-
Linked Work Orders ({workOrders.length})
- {workOrders.length === 0 ? (
-
No work orders linked.
- ) : (
-
- )}
-
-
-
-
NTE Uplift History ({upliftRequests.length})
- {upliftRequests.length === 0 ? (
-
No uplift requests on this PO.
- ) : (
- upliftRequests.map((u) => {
- const statusKey = String(u.status || "").toLowerCase();
- return (
-
-
-
- {fmtMoney(u.currentNTE)} → {fmtMoney(u.requestedNTE)}
- {" "}
-
- (Δ {fmtMoney(u.delta ?? (Number(u.requestedNTE) - Number(u.currentNTE || 0)))}, Tier {u.requiredTier})
-
-
-
{u.status}
-
- {u.vendorReason && (
-
- Reason: {u.vendorReason}
-
- )}
-
- Requested {fmtDateTime(u.requestedAt)}
- {u.requestedByVendorName ? ` by ${u.requestedByVendorName}` : ""}
- {u.decidedAt && (
- <> · Decided {fmtDateTime(u.decidedAt)}{u.decidedByName ? ` by ${u.decidedByName}` : ""}>
- )}
-
- {u.decisionNote && (
-
- Note: {u.decisionNote}
-
- )}
- {u.status === "Pending" && (
-
-
-
-
- )}
-
- );
- })
- )}
-
-
-
-
Checklist ({checklist.length})
- {checklist.length === 0 ? (
-
No checklist items.
- ) : (
-
- {checklist.map((item) => (
-
- {item.isCompleted ? "☑" : "☐"}
- {item.itemText}
- {item.isCompleted && item.completedBy && (
-
- {item.completedBy} · {fmtDateTime(item.completedAt)}
-
- )}
-
- ))}
-
- )}
-
-
-
-
Signoffs ({signoffs.length})
- {signoffs.length === 0 ? (
-
No signoffs captured.
- ) : (
-
- {signoffs.map((s) => (
-
-
{s.signoffType} Signoff
-
{s.name}
-
- {s.signatureMethod} · {fmtDateTime(s.signedAt)}
-
- {s.signatureMethod === "drawn" && s.signature ? (
-

- ) : s.signature ? (
-
{s.signature}
- ) : null}
-
- ))}
-
- )}
-
-
-
-
Comments ({comments.length})
- {comments.length === 0 ? (
-
No comments on this PO.
- ) : (
- comments.map((c) => (
-
-
-
- {c.userName || c.commenter || "—"}
- {c.commentType ? ` · ${c.commentType}` : ""}
-
- {fmtDateTime(c.createdDate)}
-
-
{c.commenttext}
-
- ))
- )}
-
-
-
-
Audit Log ({auditLog.length})
- {auditLog.length === 0 ? (
-
No audit entries.
- ) : (
-
-
-
- | When |
- Action |
- Field |
- Old → New |
- By |
-
-
-
- {auditLog.map((a) => (
-
- | {fmtDateTime(a.createdAt)} |
- {a.action} |
- {a.fieldName} |
-
- {a.oldValue || "—"} → {a.newValue || "—"}
- |
- {a.userName} |
-
- ))}
-
-
- )}
-
-
- );
-}
diff --git a/src/pages/vendors/form/VendorFormPage.css b/src/pages/vendors/form/VendorFormPage.css
deleted file mode 100644
index 7349d14b..00000000
--- a/src/pages/vendors/form/VendorFormPage.css
+++ /dev/null
@@ -1,81 +0,0 @@
-.vendorTradeGrid {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- padding: 4px 0;
-}
-
-.vendorTradeChip {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 14px;
- border: 1px solid #ddd;
- border-radius: 20px;
- font-size: 13px;
- cursor: pointer;
- user-select: none;
-}
-
-.vendorTradeChip:hover {
- border-color: #007bff;
-}
-
-.vendorTradeChip input:checked + span {
- color: #007bff;
- font-weight: 600;
-}
-
-.vendorActiveLabel {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 14px;
- cursor: pointer;
-}
-
-.vendorPortalPanel {
- border: 1px solid #e4e7eb;
- background: #f8fafc;
- border-radius: 6px;
- padding: 14px 16px;
- font-size: 13px;
-}
-
-.vendorPortalPanel-label {
- font-weight: 600;
- color: #374151;
- margin-bottom: 4px;
- font-size: 12px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.vendorPortalPanel-url {
- display: flex;
- align-items: center;
- gap: 8px;
- margin: 6px 0 10px;
-}
-
-.vendorPortalPanel-url input {
- flex: 1;
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 12px;
- padding: 6px 8px;
- border: 1px solid #d1d5db;
- border-radius: 4px;
- background: #fff;
-}
-
-.vendorPortalPanel-meta {
- color: #6b7280;
- font-size: 12px;
- margin-bottom: 10px;
-}
-
-.vendorPortalPanel-actions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
-}
diff --git a/src/pages/vendors/form/VendorFormPage.js b/src/pages/vendors/form/VendorFormPage.js
deleted file mode 100644
index 7cfb8040..00000000
--- a/src/pages/vendors/form/VendorFormPage.js
+++ /dev/null
@@ -1,239 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { vendorApi } from "../../../services/api";
-import { fetchDropdownOptions } from "../../workorders/api";
-import VendorPortalTokenPanel from "./VendorPortalTokenPanel";
-import "../../../styles/FormPage.css";
-import "./VendorFormPage.css";
-
-export default function VendorFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- companyName: "",
- contactName: "",
- email: "",
- phone: "",
- address: "",
- city: "",
- state: "",
- zip: "",
- tradeSpecialties: "",
- isActive: true,
- });
-
- const [loading, setLoading] = useState(false);
- const [trades, setTrades] = useState([]);
- const [selectedTrades, setSelectedTrades] = useState([]);
-
- useEffect(() => {
- fetchDropdownOptions("Trade").then(setTrades).catch(() => {});
- }, []);
-
- useEffect(() => {
- if (isEdit && id) {
- const load = async () => {
- try {
- const vendor = await vendorApi.getById(id);
- if (vendor) {
- setFormData(prev => {
- const merged = { ...prev };
- for (const key of Object.keys(merged)) {
- if (vendor[key] != null) merged[key] = vendor[key];
- }
- return merged;
- });
- if (vendor.tradeSpecialties) {
- setSelectedTrades(vendor.tradeSpecialties.split(",").map(t => t.trim()).filter(Boolean));
- }
- }
- } catch {
- toast.error("Failed to load vendor");
- }
- };
- load();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value, type, checked } = e.target;
- setFormData(prev => ({
- ...prev,
- [name]: type === "checkbox" ? checked : value,
- }));
- };
-
- const handleTradeToggle = (tradeValue) => {
- setSelectedTrades(prev => {
- const updated = prev.includes(tradeValue)
- ? prev.filter(t => t !== tradeValue)
- : [...prev, tradeValue];
- setFormData(f => ({ ...f, tradeSpecialties: updated.join(", ") }));
- return updated;
- });
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
- try {
- if (isEdit) {
- await vendorApi.update(id, formData);
- toast.success("Vendor updated");
- } else {
- await vendorApi.create(formData);
- toast.success("Vendor created");
- }
- navigate("/vendors");
- } catch {
- toast.error("Failed to save vendor");
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
- -
- { e.preventDefault(); navigate("/vendors"); }}>Vendors
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/vendors/form/VendorPortalTokenPanel.js b/src/pages/vendors/form/VendorPortalTokenPanel.js
deleted file mode 100644
index 48a7a6f9..00000000
--- a/src/pages/vendors/form/VendorPortalTokenPanel.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { toast } from "react-toastify";
-import { vendorApi } from "../../../services/api";
-
-export default function VendorPortalTokenPanel({ vendorId }) {
- const [tokenInfo, setTokenInfo] = useState(null);
- const [loading, setLoading] = useState(false);
- const [busy, setBusy] = useState(false);
-
- useEffect(() => {
- if (!vendorId) return;
- setLoading(true);
- vendorApi.getPortalToken(vendorId)
- .then(setTokenInfo)
- .catch(() => toast.error("Failed to load portal token"))
- .finally(() => setLoading(false));
- }, [vendorId]);
-
- async function copyUrl() {
- if (!tokenInfo?.portalUrl) return;
- try {
- await navigator.clipboard.writeText(tokenInfo.portalUrl);
- toast.success("Portal URL copied");
- } catch {
- toast.error("Copy failed");
- }
- }
-
- async function rotate() {
- if (!window.confirm("Rotate the portal token? The previous link will stop working immediately.")) return;
- setBusy(true);
- try {
- const updated = await vendorApi.rotatePortalToken(vendorId);
- setTokenInfo(updated);
- toast.success("Portal token rotated");
- } catch {
- toast.error("Rotate failed");
- } finally {
- setBusy(false);
- }
- }
-
- async function revoke() {
- if (!window.confirm("Revoke all active portal tokens? The vendor will lose access until a new link is generated.")) return;
- setBusy(true);
- try {
- await vendorApi.revokePortalToken(vendorId);
- setTokenInfo(null);
- toast.success("Portal tokens revoked");
- } catch {
- toast.error("Revoke failed");
- } finally {
- setBusy(false);
- }
- }
-
- async function regenerate() {
- setBusy(true);
- try {
- const fresh = await vendorApi.getPortalToken(vendorId);
- setTokenInfo(fresh);
- } catch {
- toast.error("Failed to generate token");
- } finally {
- setBusy(false);
- }
- }
-
- if (loading) return Loading portal link…
;
-
- if (!tokenInfo) {
- return (
-
-
Vendor Portal
-
No active portal link. Generate one so this vendor can access the portal.
-
-
-
-
- );
- }
-
- return (
-
-
Vendor Portal
-
- e.target.select()} />
-
-
-
- Issued {new Date(tokenInfo.issuedAt).toLocaleDateString()}
- {" · "}Expires {new Date(tokenInfo.expiresAt).toLocaleDateString()}
- {tokenInfo.lastUsedAt ? ` · Last used ${new Date(tokenInfo.lastUsedAt).toLocaleString()}` : " · Never used"}
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/vendors/list/List.js b/src/pages/vendors/list/List.js
deleted file mode 100644
index 7f05aa6a..00000000
--- a/src/pages/vendors/list/List.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import { useState } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchVendorsPaged, deleteVendor } from "./api";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import "./styles.css";
-
-export default function VendorsList() {
- const navigate = useNavigate();
- const {
- rows, search, setSearch, page, setPage, pageSize, setPageSize,
- totalCount, loading, err, reload
- } = usePaginatedList(fetchVendorsPaged);
-
- const [deleteModal, setDeleteModal] = useState({ isOpen: false, item: null, loading: false });
-
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- const handleDeleteClick = (item) => {
- setDeleteModal({ isOpen: true, item, loading: false });
- };
-
- const handleDeleteConfirm = async () => {
- setDeleteModal((prev) => ({ ...prev, loading: true }));
- try {
- await deleteVendor(deleteModal.item.id);
- toast.success("Vendor deleted");
- setDeleteModal({ isOpen: false, item: null, loading: false });
- reload();
- } catch {
- toast.error("Failed to delete vendor");
- setDeleteModal((prev) => ({ ...prev, loading: false }));
- }
- };
-
- return (
-
-
-
-
Vendors
- {totalCount} total
-
-
- { setSearch(e.target.value); setPage(1); }}
- placeholder="Search vendors..."
- />
-
-
-
-
- navigate(`/vendors/edit/${item.id}`)}
- onDelete={handleDeleteClick}
- page={page}
- setPage={setPage}
- pageSize={pageSize}
- setPageSize={setPageSize}
- totalCount={totalCount}
- totalPages={totalPages}
- />
-
- setDeleteModal({ isOpen: false, item: null, loading: false })}
- onConfirm={handleDeleteConfirm}
- title="Delete Vendor"
- message={`Are you sure you want to delete "${deleteModal.item?.companyName}"?`}
- loading={deleteModal.loading}
- />
-
- );
-}
diff --git a/src/pages/vendors/list/Table.js b/src/pages/vendors/list/Table.js
deleted file mode 100644
index a11f885c..00000000
--- a/src/pages/vendors/list/Table.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from "react";
-import SharedTable from "../../../components/SharedTable";
-
-export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount }) {
- const columns = [
- { key: "companyName", label: "Company", render: (val, row) => { e.preventDefault(); onEdit && onEdit(row); }}>{val} },
- { key: "contactName", label: "Contact" },
- { key: "email", label: "Email" },
- { key: "phone", label: "Phone" },
- { key: "tradeSpecialties", label: "Trades" },
- { key: "isActive", label: "Active", render: (val) => val ? "Yes" : "No" },
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/vendors/list/api.js b/src/pages/vendors/list/api.js
deleted file mode 100644
index da6104aa..00000000
--- a/src/pages/vendors/list/api.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchVendorsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
- const res = await get(`${API_URL}/Vendor/GetVendorList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id ?? x.Id,
- companyName: x.companyName ?? x.CompanyName ?? "",
- contactName: x.contactName ?? x.ContactName ?? "",
- email: x.email ?? x.Email ?? "",
- phone: x.phone ?? x.Phone ?? "",
- tradeSpecialties: x.tradeSpecialties ?? x.TradeSpecialties ?? "",
- isActive: x.isActive ?? x.IsActive ?? true,
- }));
- return {
- rows,
- page: res?.pageNumber ?? 1,
- pageSize: res?.pageSize ?? pageSize,
- totalCount: res?.totalCount ?? rows.length,
- totalPages: res?.totalPages ?? 1,
- };
-};
-
-export const deleteVendor = (id) =>
- get(`${API_URL}/Vendor/DeleteVendor?id=${id}`);
diff --git a/src/pages/vendors/list/styles.css b/src/pages/vendors/list/styles.css
deleted file mode 100644
index 093788a1..00000000
--- a/src/pages/vendors/list/styles.css
+++ /dev/null
@@ -1,63 +0,0 @@
-.vendorListToolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.vendorListToolbarLeft {
- display: flex;
- align-items: baseline;
- gap: 12px;
-}
-
-.vendorListTitle {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #333;
-}
-
-.vendorListCount {
- font-size: 13px;
- color: #999;
-}
-
-.vendorListToolbarRight {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.vendorSearchInput {
- width: 260px;
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-size: 14px;
- outline: none;
- box-sizing: border-box;
-}
-
-.vendorSearchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.vendorNewBtn {
- padding: 8px 16px;
- background: #007bff;
- color: #fff;
- border: none;
- border-radius: 4px;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
-}
-
-.vendorNewBtn:hover {
- background: #0069d9;
-}
diff --git a/src/pages/workorders/api.js b/src/pages/workorders/api.js
deleted file mode 100644
index c0c92e23..00000000
--- a/src/pages/workorders/api.js
+++ /dev/null
@@ -1,184 +0,0 @@
-import { workOrderApi } from "../../services/api";
-import { post } from "../../apiUtil";
-import { API_URL } from "../../constants";
-
-export const getWorkOrders = async (params = {}) => {
- try {
- const response = await workOrderApi.getAll(params);
- const workOrders = response.data || response;
- return Array.isArray(workOrders) ? workOrders : [];
- } catch (error) {
- console.error("Error fetching work orders:", error);
- throw error;
- }
-};
-
-export const getWorkOrderById = async (id) => {
- try {
- const wo = await workOrderApi.getById(id);
- return {
- id: wo.id,
- internalWONumber: wo.internalWONumber ?? "",
- workOrderNumber: wo.workerOrderNumber ?? wo.workOrderNumber ?? "",
- title: wo.title ?? wo.workerOrderTitle ?? "",
- location: wo.location ?? "",
- locationId: wo.locationId != null ? String(wo.locationId) : "",
- locationZip: wo.locationZip ?? "",
- assignedTo: wo.assignee ?? wo.assignedTo ?? "",
- dueDate: wo.dueDate ? wo.dueDate.split("T")[0] : "",
- createdDate: wo.createdDate ? wo.createdDate.split("T")[0] : "",
- createdDateFull: wo.createdDate ?? "",
- description: wo.description ?? "",
- priority: wo.priority ?? "",
- 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 ?? [],
- dispatches: wo.dispatches ?? [],
- };
- } catch (error) {
- console.error("Error fetching work order:", error);
- throw error;
- }
-};
-
-export const addWorkOrder = async (workOrder) => {
- try {
- return await workOrderApi.create(workOrder);
- } catch (error) {
- console.error("Error creating work order:", error);
- throw error;
- }
-};
-
-export const editWorkOrder = async (workOrder) => {
- try {
- return await workOrderApi.update(workOrder.id, {
- Id: workOrder.id,
- WorkerOrderNumber: workOrder.workOrderNumber,
- WorkerOrderTitle: workOrder.title,
- Description: workOrder.description,
- PO: workOrder.po,
- TT: workOrder.tt,
- AssignTo: workOrder.assignedTo || null,
- DueDate: workOrder.dueDate || null,
- 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);
- throw error;
- }
-};
-
-export const addComment = async (workOrderId, text, commentType = "internal") => {
- return post(`${API_URL}/WorkOrder/AddCommentJson`, {
- WorkorderId: workOrderId,
- Text: text,
- CommentType: commentType,
- });
-};
-
-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 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 fetchVendorsDropdown = async (trade, siteZip) => {
- const { get: apiGet } = await import("../../apiUtil");
- const params = new URLSearchParams();
- if (trade) params.append("trade", trade);
- if (siteZip) params.append("siteZip", siteZip);
- const qs = params.toString();
- return (await apiGet(`${API_URL}/Vendor/Dropdown${qs ? `?${qs}` : ""}`)) || [];
-};
-
-export const fetchTaskTemplates = async () => {
- const { get: apiGet } = await import("../../apiUtil");
- return (await apiGet(`${API_URL}/TaskListTemplate`)) || [];
-};
-
-export const updateChecklistItem = async (id, isCompleted, completedBy) => {
- return post(`${API_URL}/WorkOrder/UpdateChecklistItem`, { Id: id, IsCompleted: isCompleted, CompletedBy: completedBy });
-};
-
-export const addChecklistItem = async (dispatchId, itemText, workOrderId) => {
- return post(`${API_URL}/WorkOrder/AddChecklistItem`, { DispatchId: dispatchId, ItemText: itemText, WorkOrderId: workOrderId });
-};
-
-export const verifyDispatch = async (dispatchId) => {
- return post(`${API_URL}/WorkOrder/VerifyDispatch?dispatchId=${dispatchId}`);
-};
-
-export const addDispatchSignoff = async (dispatchId, signoffType, name, signature, signatureMethod) => {
- return post(`${API_URL}/WorkOrder/AddDispatchSignoff`, {
- DispatchId: dispatchId,
- SignoffType: signoffType,
- Name: name,
- Signature: signature,
- SignatureMethod: signatureMethod,
- });
-};
-
-export const getDispatchById = async (id) => {
- const { get: apiGet } = await import("../../apiUtil");
- return apiGet(`${API_URL}/WorkOrder/GetDispatch/${id}`);
-};
-
-export const updateDispatch = async (payload) => {
- return post(`${API_URL}/WorkOrder/UpdateDispatch`, payload);
-};
-
-export const addDispatchComment = async (dispatchId, text, sendEmail = true) => {
- return post(`${API_URL}/WorkOrder/AddDispatchComment`, {
- DispatchId: dispatchId,
- Text: text,
- SendEmail: sendEmail,
- });
-};
-
-export const dispatchToVendor = async (payload) => {
- return post(`${API_URL}/WorkOrder/DispatchToVendor`, payload);
-};
-
-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);
- return true;
- } catch (error) {
- console.error("Error deleting work order:", error);
- throw error;
- }
-};
diff --git a/src/pages/workorders/form/WorkOrderFormPage.css b/src/pages/workorders/form/WorkOrderFormPage.css
deleted file mode 100644
index 46a9c999..00000000
--- a/src/pages/workorders/form/WorkOrderFormPage.css
+++ /dev/null
@@ -1,297 +0,0 @@
-/* Work Order Form Page - Matching Employee Form Style */
-.workOrderFormPage {
- background: #f5f5f5;
- min-height: 100vh;
- padding: 0;
-}
-
-.panel {
- margin-bottom: 20px;
- background-color: #fff;
- border: 1px solid transparent;
- border-radius: 4px;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-
-.panel-default {
- border-color: #ddd;
-}
-
-.panel-connected {
- border-radius: 0;
- border: none;
- box-shadow: none;
-}
-
-.panel-body {
- padding: 15px;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 15px;
- margin-bottom: 20px;
- list-style: none;
- background-color: #f5f5f5;
- border-radius: 4px;
-}
-
-.breadcrumb-alt {
- background-color: white;
- border-bottom: 1px solid #e5e5e5;
- padding: 10px 20px;
- margin: 0 0 20px 0;
- border-radius: 0;
-}
-
-.breadcrumb > li {
- display: inline-block;
-}
-
-.breadcrumb > li + li:before {
- content: ">\00a0";
- padding: 0 5px;
- color: #ccc;
-}
-
-.breadcrumb > li a {
- color: #0088cc;
- text-decoration: none;
-}
-
-.breadcrumb > li a:hover {
- text-decoration: underline;
-}
-
-.breadcrumb > .active {
- color: #00a0d1;
- font-weight: bold;
- cursor: default;
-}
-
-/* Container */
-.container {
- padding-right: 15px;
- padding-left: 15px;
- margin-right: auto;
- margin-left: auto;
-}
-
-/* Form Header */
-.header {
- margin-bottom: 15px;
-}
-
-.heading {
- margin: 0 0 10px 0;
- font-weight: 300;
- line-height: 1.1;
- color: #333;
-}
-
-.heading.thin {
- font-weight: 300;
- font-size: 24px;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.heading.text-uppercase {
- text-transform: uppercase;
- font-size: 14px;
- margin-bottom: 15px;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle;
-}
-
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center;
-}
-
-.fa-stack-2x {
- font-size: 2em;
-}
-
-.fa-stack-1x {
- font-size: 1em;
- line-height: inherit;
-}
-
-.fa-inverse {
- color: #fff;
-}
-
-/* Form Groups */
-.form-group {
- margin-bottom: 15px;
- position: relative;
-}
-
-/* Row and Columns */
-.row {
- margin-right: -15px;
- margin-left: -15px;
- display: flex;
- flex-wrap: wrap;
-}
-
-.row::after {
- content: "";
- display: table;
- clear: both;
-}
-
-[class*="col-"] {
- position: relative;
- min-height: 1px;
- padding-right: 15px;
- padding-left: 15px;
-}
-
-.col-sm-3 {
- width: 25%;
-}
-
-.col-sm-4 {
- width: 33.33333333%;
-}
-
-.col-sm-6 {
- width: 50%;
-}
-
-.col-sm-12 {
- width: 100%;
-}
-
-@media (min-width: 992px) {
- .col-md-4 {
- width: 33.33333333%;
- }
-}
-
-/* Labels */
-label {
- display: inline-block;
- max-width: 100%;
- margin-bottom: 5px;
- font-weight: 600;
- font-size: 13px;
- color: #333;
-}
-
-.required-field::after {
- content: " *";
- color: #d9534f;
-}
-
-/* Form Controls */
-.form-control {
- display: block;
- width: 100%;
- height: 34px;
- padding: 6px 12px;
- font-size: 14px;
- line-height: 1.42857143;
- color: #555;
- background-color: #fff;
- background-image: none;
- border: 1px solid #ccc;
- border-radius: 4px;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
- box-sizing: border-box;
-}
-
-.form-control:focus {
- border-color: #66afe9;
- outline: 0;
- box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
-}
-
-select.form-control {
- height: 34px;
- padding: 6px 12px;
-}
-
-textarea.form-control {
- height: auto;
- min-height: 100px;
-}
-
-/* Form Actions */
-.form-actions {
- padding: 19px 20px 20px;
- margin-top: 20px;
- margin-bottom: 20px;
- border-top: 1px solid #e5e5e5;
- text-align: right;
-}
-
-/* Buttons */
-.btn {
- display: inline-block;
- padding: 6px 12px;
- margin-bottom: 0;
- font-size: 14px;
- font-weight: 400;
- line-height: 1.42857143;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- cursor: pointer;
- border: 1px solid transparent;
- border-radius: 4px;
- user-select: none;
-}
-
-.btn-default {
- color: #333;
- background-color: #fff;
- border-color: #ccc;
-}
-
-.btn-default:hover {
- background-color: #e6e6e6;
- border-color: #adadad;
-}
-
-.btn-primary {
- color: #fff;
- background-color: #0088cc;
- border-color: #0088cc;
-}
-
-.btn-primary:hover {
- background-color: #006699;
- border-color: #005580;
-}
-
-.btn:disabled {
- cursor: not-allowed;
- opacity: 0.65;
-}
-
-/* Responsive */
-@media (max-width: 767px) {
- [class*="col-sm-"] {
- width: 100%;
- }
-}
-
diff --git a/src/pages/workorders/form/WorkOrderFormPage.js b/src/pages/workorders/form/WorkOrderFormPage.js
deleted file mode 100644
index 2991703d..00000000
--- a/src/pages/workorders/form/WorkOrderFormPage.js
+++ /dev/null
@@ -1,341 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { getWorkOrderById, addWorkOrder, editWorkOrder, fetchDropdownOptions } from "../api";
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-import "./WorkOrderFormPage.css";
-
-export default function WorkOrderFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- internalWONumber: "",
- workOrderNumber: "",
- title: "",
- locationId: "",
- priority: "",
- 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 () => {
- try {
- const wo = await getWorkOrderById(id);
- if (wo) {
- setFormData(prev => {
- const merged = { ...prev };
- for (const key of Object.keys(merged)) {
- if (wo[key] != null) merged[key] = wo[key];
- }
- return merged;
- });
- }
- } catch (err) {
- console.error("Failed to load work order", err);
- toast.error("Failed to load work order");
- }
- };
- loadWorkOrder();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData(prev => {
- const updated = { ...prev, [name]: value };
- if (name === "trade") updated.subTrade = "";
- return updated;
- });
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- if (isEdit) {
- await editWorkOrder({ ...formData, id });
- toast.success("Work Order updated successfully!");
- } else {
- await addWorkOrder(formData);
- toast.success("Work Order created successfully!");
- }
- navigate(isEdit ? `/workorders/${id}` : "/workorders");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save work order");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate(isEdit ? `/workorders/${id}` : "/workorders");
- };
-
- return (
-
-
-
-
- -
- { e.preventDefault(); navigate("/workorders"); }}>
- Work Orders
-
-
- - {isEdit ? "Edit" : "Create"}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/pages/workorders/form/WorkOrderFormPage.js.backup b/src/pages/workorders/form/WorkOrderFormPage.js.backup
deleted file mode 100644
index a4d85d97..00000000
--- a/src/pages/workorders/form/WorkOrderFormPage.js.backup
+++ /dev/null
@@ -1,262 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import { getWorkOrderById, addWorkOrder, editWorkOrder } from "../api";
-
-export default function WorkOrderFormPage() {
- const { id } = useParams();
- const navigate = useNavigate();
- const isEdit = !!id;
-
- const [formData, setFormData] = useState({
- workOrderNumber: "",
- title: "",
- location: "",
- priority: "Medium",
- status: "Open",
- createdDate: new Date().toISOString().split("T")[0],
- dueDate: "",
- assignedTo: "",
- description: "",
- estimatedHours: 1,
- });
-
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (isEdit && id) {
- const loadWorkOrder = async () => {
- try {
- const wo = await getWorkOrderById(id);
- if (wo) {
- setFormData(wo);
- }
- } catch (err) {
- console.error("Failed to load work order", err);
- toast.error("Failed to load work order");
- }
- };
- loadWorkOrder();
- }
- }, [id, isEdit]);
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormData(prev => ({
- ...prev,
- [name]: value,
- }));
- };
-
- const handleSave = async (e) => {
- e.preventDefault();
- setLoading(true);
-
- try {
- if (isEdit) {
- await editWorkOrder(formData);
- toast.success("Work Order updated successfully!");
- } else {
- await addWorkOrder(formData);
- toast.success("Work Order created successfully!");
- }
- navigate("/workorders");
- } catch (err) {
- console.error(err);
- toast.error("Failed to save work order");
- } finally {
- setLoading(false);
- }
- };
-
- const handleCancel = () => {
- navigate("/workorders");
- };
-
- return (
-
-
- {isEdit ? "Edit Work Order" : "Create Work Order"}
-
-
-
-
- );
-}
diff --git a/src/pages/workorders/list/List.js b/src/pages/workorders/list/List.js
deleted file mode 100644
index f3ed313a..00000000
--- a/src/pages/workorders/list/List.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router";
-import { toast } from "react-toastify";
-import usePaginatedList from "../../../hooks/usePaginatedList";
-import { fetchWorkOrdersPaged } from "./api";
-import { fetchUsers } from "../api";
-import Table from "./Table";
-import DeleteModal from "../../../components/DeleteModal";
-import "./styles.css";
-
-export default function WorkOrdersList() {
- const navigate = useNavigate();
- const [statusFilter, setStatusFilter] = useState("");
- const [assigneeFilter, setAssigneeFilter] = useState("");
-
- const {
- rows, search, setSearch, page, setPage, pageSize, setPageSize,
- totalCount, loading, err, sortBy, setSortBy, sortDir, setSortDir
- } = usePaginatedList(fetchWorkOrdersPaged, { status: statusFilter, assignee: assigneeFilter });
-
- const [deleteModal, setDeleteModal] = useState({
- isOpen: false,
- item: null,
- loading: false
- });
-
- const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
-
- const handleDeleteClick = (item) => {
- setDeleteModal({ isOpen: true, item, loading: false });
- };
-
- const handleDeleteConfirm = async () => {
- setDeleteModal((prev) => ({ ...prev, loading: true }));
- try {
- await new Promise((resolve) => setTimeout(resolve, 500));
- toast.success("Work Order deleted successfully!");
- setDeleteModal({ isOpen: false, item: null, loading: false });
- } catch {
- toast.error("Failed to delete work order");
- setDeleteModal((prev) => ({ ...prev, loading: false }));
- }
- };
-
- const [users, setUsers] = useState([]);
- const statuses = ["Open", "In Progress", "On Hold", "Done", "Cancelled"];
-
- useEffect(() => {
- fetchUsers().then(setUsers).catch(() => {});
- }, []);
-
- return (
- <>
-
-
-
-
Work Orders
- {totalCount} total
-
-
- { setSearch(e.target.value); setPage(1); }}
- placeholder="Search..."
- />
-
-
-
-
-
-
- {statuses.map((s) => (
-
- ))}
-
-
-
-
- navigate(`/workorders/${item.id}`)}
- onDelete={handleDeleteClick}
- page={page}
- setPage={setPage}
- pageSize={pageSize}
- setPageSize={setPageSize}
- totalCount={totalCount}
- totalPages={totalPages}
- sortBy={sortBy}
- setSortBy={setSortBy}
- sortDir={sortDir}
- setSortDir={setSortDir}
- />
-
-
- setDeleteModal({ isOpen: false, item: null, loading: false })
- }
- onConfirm={handleDeleteConfirm}
- title="Delete Work Order"
- message={`Are you sure you want to delete "${deleteModal.item?.title}"? This action cannot be undone.`}
- loading={deleteModal.loading}
- />
-
- >
- );
-}
diff --git a/src/pages/workorders/list/Table.js b/src/pages/workorders/list/Table.js
deleted file mode 100644
index 4e840c57..00000000
--- a/src/pages/workorders/list/Table.js
+++ /dev/null
@@ -1,199 +0,0 @@
-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, sortBy, setSortBy, sortDir, setSortDir }) {
- 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)
- );
- };
-
- 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", render: (val, row) => (
-
- )},
- { key: "createdDate", label: "Created", render: (val) => {
- if (!val) return "";
- const d = new Date(val.endsWith("Z") ? val : val + "Z");
- if (isNaN(d)) return val;
- const date = d.toLocaleDateString("en-CA");
- const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
- const tz = d.toLocaleTimeString(undefined, { timeZoneName: "short" }).split(" ").pop();
- return `${date} ${time} ${tz}`;
- }},
- { key: "assignedTo", label: "Assigned To", render: (val, row) => (
-
- )},
- { key: "lastUpdated", label: "Last Updated", render: (val) => {
- if (!val) return "—";
- const d = new Date(val.endsWith("Z") ? val : val + "Z");
- if (isNaN(d)) return val;
- const date = d.toLocaleDateString("en-CA");
- const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
- const tz = d.toLocaleTimeString(undefined, { timeZoneName: "short" }).split(" ").pop();
- return `${date} ${time} ${tz}`;
- }},
- ];
-
- return (
-
- );
-}
diff --git a/src/pages/workorders/list/api.js b/src/pages/workorders/list/api.js
deleted file mode 100644
index f6fc23ff..00000000
--- a/src/pages/workorders/list/api.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { get } from "../../../apiUtil";
-import { API_URL } from "../../../constants";
-
-export const fetchWorkOrdersPaged = async ({ search = "", page = 1, pageSize = 12, sortBy = "", sortDir = "", status = "", assignee = "" }) => {
- const params = new URLSearchParams({ search, page, pageSize });
- if (sortBy) params.append("sortBy", sortBy);
- if (sortDir) params.append("sortDir", sortDir);
- if (status) params.append("status", status);
- if (assignee) params.append("assignee", assignee);
- const res = await get(`${API_URL}/WorkOrder/GetWorkOrderList?${params.toString()}`);
- const rows = (res?.data || res?.Data || []).map(x => ({
- id: x.id ?? x.Id,
- number: x.internalWONumber ?? x.number ?? x.workOrderNumber ?? "",
- title: x.title ?? x.Title ?? "",
- location: x.location ?? x.Location ?? x.locationName ?? x.LocationName ?? "",
- priority: x.priority ?? x.Priority ?? "",
- status: x.status ?? x.Status ?? "",
- createdDate: x.createdDate ?? x.CreatedDate ?? "",
- dueDate: x.dueDate ?? x.DueDate ?? "",
- assignedTo: x.assignedTo ?? x.AssignedTo ?? x.employeeName ?? x.EmployeeName ?? "",
- lastUpdated: x.lastUpdated ?? x.LastUpdated ?? "",
- }));
- return {
- rows,
- page: res?.pageNumber ?? 1,
- pageSize: res?.pageSize ?? pageSize,
- totalCount: res?.totalCount ?? rows.length,
- totalPages: res?.totalPages ?? 1,
- };
-};
-
-export const deleteWorkOrder = (id) =>
- get(`${API_URL}/WorkOrder/DeleteWorkOrder?id=${id}`);
-
diff --git a/src/pages/workorders/list/styles.css b/src/pages/workorders/list/styles.css
deleted file mode 100644
index e08af143..00000000
--- a/src/pages/workorders/list/styles.css
+++ /dev/null
@@ -1,450 +0,0 @@
-/* Modal styles for Work Orders module */
-.modalRoot {
- position: fixed;
- inset: 0;
- z-index: 1000;
-}
-
-.modalScrim {
- position: absolute;
- inset: 0;
- background: rgba(0, 0, 0, 0.3);
-}
-
-.modalCard {
- position: relative;
- margin: 10% auto;
- padding: 20px;
- background: #fff;
- width: 400px;
-}
-
-/* Override listPage padding for this module */
-.listPage {
- padding: 0 !important;
- background: white !important;
- min-height: auto !important;
-}
-
-/* Breadcrumb */
-.breadcrumb {
- background: white;
- padding: 10px 20px;
- margin: 0;
- border-bottom: 1px solid #e5e5e5;
- font-size: 13px;
-}
-
-.breadcrumbItem {
- color: #333;
-}
-
-.breadcrumbItem.active {
- cursor: default;
- font-weight: bold;
- color: #00a0d1;
-}
-
-/* Search Section Styles */
-.searchSection {
- background: white;
- padding: 20px;
- margin-bottom: 0;
- border-bottom: 1px solid #e5e5e5;
- position: relative;
- z-index: 1;
-}
-
-.searchHeader {
- margin-bottom: 20px;
-}
-
-.searchHeading {
- font-size: 24px;
- font-weight: 300;
- margin: 0 0 8px 0;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.iconStack {
- font-size: 0.9em;
-}
-
-.text-primary {
- color: #0088cc;
-}
-
-.fa-inverse {
- color: white;
-}
-
-.searchSubheading {
- color: #666;
- font-size: 14px;
- display: block;
- margin-left: 45px;
-}
-
-.searchControls {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 20px;
-}
-
-.searchLeft {
- display: flex;
- align-items: center;
- gap: 15px;
- flex: 1;
-}
-
-.searchInputGroup {
- display: flex;
- align-items: center;
- gap: 0;
- flex: 1;
- max-width: 600px;
-}
-
-.searchDropdown {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-right: none;
- border-radius: 4px 0 0 4px;
- background: white;
- font-size: 14px;
- min-width: 120px;
-}
-
-.searchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- flex: 1;
- font-size: 14px;
- outline: none;
-}
-
-.searchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.woSearchInput {
- height: 38px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-size: 14px;
- outline: none;
- width: 100%;
- box-sizing: border-box;
-}
-
-.woSearchInput:focus {
- border-color: #66afe9;
- box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
-}
-
-.searchButton {
- height: 38px;
- padding: 6px 20px;
- background: #0088cc;
- color: white;
- border: 1px solid #0088cc;
- border-radius: 0 4px 4px 0;
- font-size: 14px;
- cursor: pointer;
- white-space: nowrap;
- margin-left: 20px;
-}
-
-.searchButton:hover {
- background: #0077b3;
- border-color: #0077b3;
-}
-
-.viewToggle {
- display: flex;
- border: 1px solid #0088cc;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.viewBtn {
- width: 40px;
- height: 38px;
- background: white;
- border: none;
- border-right: 1px solid #0088cc;
- color: #0088cc;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.viewBtn:last-child {
- border-right: none;
-}
-
-.viewBtn.active {
- background: #0088cc;
- color: white;
-}
-
-.viewBtn:hover:not(.active) {
- background: #e6f2f8;
-}
-
-.searchRight {
- display: flex;
- align-items: center;
-}
-
-.paginationTop {
- display: flex;
- align-items: center;
- gap: 5px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 0;
-}
-
-.pageBtn {
- width: 35px;
- height: 36px;
- background: white;
- border: none;
- cursor: pointer;
- font-size: 14px;
- color: #0088cc;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.pageBtn:hover:not(:disabled) {
- background: #e6f2f8;
-}
-
-.pageBtn:disabled {
- color: #ccc;
- cursor: not-allowed;
-}
-
-.pageNumber {
- min-width: 35px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #0088cc;
- color: white;
- font-size: 14px;
- border-left: 1px solid #ddd;
- 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;
-}
-
-.woListToolbar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- border-bottom: 1px solid #e5e5e5;
-}
-
-.woListToolbarLeft {
- display: flex;
- align-items: baseline;
- gap: 12px;
-}
-
-.woListTitle {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- color: #333;
-}
-
-.woListCount {
- font-size: 13px;
- color: #999;
-}
-
-.woListToolbarRight {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.woListToolbarRight .woSearchInput {
- width: 260px;
-}
-
-.woNewBtn {
- padding: 8px 16px;
- background: #007bff;
- color: #fff;
- border: none;
- border-radius: 4px;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
-}
-
-.woNewBtn:hover {
- background: #0069d9;
-}
-
-.woFilterBar {
- display: flex;
- gap: 6px;
- padding: 10px 20px;
- border-bottom: 1px solid #e5e5e5;
- background: #fafafa;
-}
-
-.woFilterChip {
- padding: 5px 14px;
- border: 1px solid #ddd;
- border-radius: 16px;
- background: #fff;
- font-size: 12px;
- color: #666;
- cursor: pointer;
-}
-
-.woFilterChip:hover {
- border-color: #007bff;
- color: #007bff;
-}
-
-.woFilterChip.active {
- background: #007bff;
- color: #fff;
- border-color: #007bff;
-}
-
-.woFilterDivider {
- width: 1px;
- height: 20px;
- background: #ddd;
- margin: 0 4px;
-}
-
-.woFilterSelect {
- height: 28px;
- padding: 0 10px;
- border: 1px solid #ddd;
- border-radius: 16px;
- font-size: 12px;
- color: #666;
- background: #fff;
- cursor: pointer;
- outline: none;
-}
-
-.woFilterSelect:focus {
- border-color: #007bff;
-}
-
-/* Table wrapper */
-.tableContainer {
- background: white;
- border: 1px solid #ddd;
- border-radius: 0;
- margin: 0;
-}
diff --git a/src/pages/workorders/view/WorkOrderViewPage.css b/src/pages/workorders/view/WorkOrderViewPage.css
deleted file mode 100644
index 6940db92..00000000
--- a/src/pages/workorders/view/WorkOrderViewPage.css
+++ /dev/null
@@ -1,1122 +0,0 @@
-.woViewPage {
- padding: 0;
-}
-
-.woViewHeader {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- padding: 0 20px 20px;
- border-bottom: 1px solid #e5e5e5;
- margin-bottom: 20px;
-}
-
-.woViewTitle h2 {
- margin: 0 0 8px 0;
- font-size: 22px;
- font-weight: 600;
- color: #333;
-}
-
-.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, .woStatusBadge.completed { background: #28a745; }
-.woStatusBadge.cancelled { background: #dc3545; }
-
-.woStatusClickable {
- cursor: pointer;
- user-select: none;
- border: none;
-}
-
-.woStatusClickable:hover {
- filter: brightness(0.9);
-}
-
-.woStatusCaret {
- font-size: 10px;
- margin-left: 4px;
-}
-
-.woStatusMenu {
- 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: 170px;
- padding: 4px 0;
-}
-
-.woStatusOption {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 100%;
- padding: 8px 14px;
- border: none;
- background: #ffffff;
- font-size: 13px;
- color: #333;
- cursor: pointer;
- text-align: left;
-}
-
-.woStatusOption:hover {
- background: #f0f0f0;
-}
-
-.woStatusOption.active {
- font-weight: 600;
- background: #e6f0ff;
-}
-
-.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;
- flex-shrink: 0;
-}
-
-.woViewGrid {
- padding: 0 20px;
-}
-
-.woViewSection {
- margin-bottom: 24px;
-}
-
-.woViewSection h4 {
- font-size: 14px;
- font-weight: 600;
- text-transform: uppercase;
- color: #666;
- margin: 0 0 12px 0;
- padding-bottom: 8px;
- 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;
- gap: 12px;
-}
-
-.woField {
- display: flex;
- flex-direction: column;
-}
-
-.woFieldLabel {
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- color: #999;
- margin-bottom: 2px;
-}
-
-.woFieldValue {
- font-size: 14px;
- color: #333;
-}
-
-.woDescription {
- font-size: 14px;
- color: #333;
- line-height: 1.6;
- margin: 0;
- white-space: pre-wrap;
-}
-
-.woFieldPlaceholder {
- color: #ccc;
- font-style: italic;
-}
-
-.woTimeline {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.woTimelineItem {
- padding: 12px;
- background: #f8f9fa;
- border-radius: 6px;
- border-left: 3px solid #007bff;
-}
-
-.woTimelineHeader {
- display: flex;
- justify-content: space-between;
- margin-bottom: 6px;
-}
-
-.woTimelineAuthor {
- font-weight: 600;
- font-size: 13px;
- color: #333;
-}
-
-.woTimelineDate {
- font-size: 12px;
- color: #999;
-}
-
-.woTimelineBody {
- font-size: 13px;
- color: #555;
- line-height: 1.5;
-}
-
-.woTabContainer {
- display: flex;
- flex-direction: column;
- height: 400px;
- border: 1px solid #e5e5e5;
- border-radius: 0 0 6px 6px;
-}
-
-.woTabScroll {
- flex: 1;
- overflow-y: auto;
- padding: 16px;
-}
-
-.woCommentTabs {
- display: flex;
- gap: 0;
- border-bottom: 2px solid #e5e5e5;
-}
-
-.woCommentTab {
- padding: 8px 20px;
- border: none;
- background: none;
- font-size: 13px;
- font-weight: 600;
- color: #999;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- margin-bottom: -2px;
-}
-
-.woCommentTab:hover {
- color: #333;
-}
-
-.woCommentTab.active {
- color: #007bff;
- border-bottom-color: #007bff;
-}
-
-.woNoComments {
- font-size: 13px;
- color: #999;
- padding: 20px 0;
- text-align: center;
-}
-
-.woAuditItem {
- border-left-color: #6c757d;
-}
-
-.woAuditField {
- font-weight: 600;
- color: #333;
- margin-right: 8px;
-}
-
-.woAuditOld {
- color: #dc3545;
- text-decoration: line-through;
- margin-right: 4px;
-}
-
-.woAuditArrow {
- color: #999;
- margin: 0 4px;
-}
-
-.woAuditNew {
- color: #28a745;
- font-weight: 500;
-}
-
-.woCommentInput {
- display: flex;
- gap: 8px;
- padding: 12px 16px;
- border-top: 1px solid #e5e5e5;
- background: #f8f9fa;
- flex-shrink: 0;
-}
-
-.woCommentTextarea {
- flex: 1;
- border: 1px solid #ddd;
- border-radius: 4px;
- padding: 8px 12px;
- font-size: 13px;
- font-family: inherit;
- resize: none;
- line-height: 1.4;
-}
-
-.woCommentTextarea:focus {
- outline: none;
- border-color: #007bff;
-}
-
-.woCommentSend {
- align-self: flex-end;
- padding: 8px 16px;
- font-size: 13px;
-}
-
-.btn-success {
- background: #28a745;
- color: #fff;
- border: none;
- border-radius: 4px;
- padding: 8px 16px;
- font-size: 13px;
- font-weight: 600;
- cursor: pointer;
-}
-
-.btn-success:hover {
- background: #218838;
-}
-
-.woDispatchOverlay {
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.4);
- z-index: 99999;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.woDispatchModal {
- background: #fff;
- border-radius: 8px;
- width: 560px;
- max-height: 80vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 12px 40px rgba(0,0,0,0.2);
-}
-
-.woDispatchHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 20px;
- border-bottom: 1px solid #eee;
-}
-
-.woDispatchHeader h3 {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
-}
-
-.woDispatchClose {
- background: none;
- border: none;
- font-size: 22px;
- color: #999;
- cursor: pointer;
-}
-
-.woDispatchBody {
- padding: 20px;
- overflow-y: auto;
- flex: 1;
-}
-
-.woDispatchField {
- margin-bottom: 16px;
-}
-
-.woDispatchField label {
- display: block;
- font-size: 13px;
- font-weight: 600;
- margin-bottom: 6px;
- color: #333;
-}
-
-.woDispatchSearch {
- width: 100%;
- height: 36px;
- padding: 6px 12px;
- border: 1px solid #ccc;
- border-radius: 4px;
- font-size: 13px;
- margin-bottom: 8px;
- box-sizing: border-box;
-}
-
-.woDispatchSearch:focus {
- border-color: #66afe9;
- outline: none;
-}
-
-.woDispatchSelected {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-top: 8px;
-}
-
-.woDispatchSelectedChip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 3px 10px;
- background: #e6f0ff;
- border-radius: 12px;
- font-size: 12px;
- color: #007bff;
-}
-
-.woDispatchSelectedChip button {
- background: none;
- border: none;
- color: #007bff;
- cursor: pointer;
- font-size: 14px;
- padding: 0;
- line-height: 1;
-}
-
-.woDispatchFieldRow {
- display: flex;
- gap: 12px;
-}
-
-.woDispatchFieldHalf {
- flex: 1;
-}
-
-.woDispatchVendorList {
- max-height: 200px;
- overflow-y: auto;
- border: 1px solid #ddd;
- border-radius: 4px;
-}
-
-.woDispatchVendorItem {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- cursor: pointer;
- border-bottom: 1px solid #f0f0f0;
- flex-wrap: nowrap;
-}
-
-.woDispatchVendorItem:last-child {
- border-bottom: none;
-}
-
-.woDispatchVendorItem:hover {
- background: #f8f9fa;
-}
-
-.woDispatchVendorItem.selected {
- background: #e6f0ff;
-}
-
-.woDispatchVendorItem input[type="radio"] {
- flex-shrink: 0;
- width: 16px;
- height: 16px;
-}
-
-.woDispatchVendorInfo {
- display: flex;
- flex-direction: column;
- gap: 1px;
- min-width: 0;
- flex: 1;
-}
-
-.woDispatchVendorName {
- font-size: 13px;
- font-weight: 500;
-}
-
-.woDispatchVendorTrades {
- font-size: 11px;
- color: #007bff;
-}
-
-.woDispatchVendorAddr {
- font-size: 11px;
- color: #999;
-}
-
-.woDispatchVendorDistBadge {
- display: flex;
- flex-direction: column;
- align-items: center;
- flex-shrink: 0;
- min-width: 60px;
- padding: 4px 8px;
- background: #f0f7ff;
- border-radius: 6px;
-}
-
-.woDispatchDistNum {
- font-size: 16px;
- font-weight: 700;
- color: #007bff;
- line-height: 1.1;
-}
-
-.woDispatchDistLabel {
- font-size: 10px;
- color: #999;
- text-transform: uppercase;
-}
-
-.woDispatchLoading {
- padding: 20px;
- text-align: center;
- color: #999;
- font-size: 13px;
-}
-
-.woDispatchFooter {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding: 16px 20px;
- border-top: 1px solid #eee;
-}
-
-.woDispatchTable {
- width: 100%;
- border-collapse: collapse;
- font-size: 13px;
-}
-
-.woDispatchTable th {
- text-align: left;
- padding: 8px 12px;
- font-weight: 600;
- color: #666;
- border-bottom: 1px solid #ddd;
- font-size: 11px;
- text-transform: uppercase;
-}
-
-.woDispatchTable td {
- padding: 8px 12px;
- border-bottom: 1px solid #eee;
-}
-
-.woDispatchStatus {
- display: inline-block;
- padding: 2px 10px;
- border-radius: 10px;
- font-size: 11px;
- font-weight: 500;
- color: #fff;
- background: #6c757d;
-}
-
-.woDispatchStatus.sent { background: #007bff; }
-.woDispatchStatus.acknowledged { background: #ffc107; color: #333; }
-.woDispatchStatus.completed { background: #28a745; }
-.woDispatchStatus.cancelled { background: #dc3545; }
-.woDispatchStatus.verified { background: #155724; }
-
-.dispatchClickRow {
- cursor: pointer;
-}
-
-.dispatchClickRow:hover {
- background: #f0f7ff;
-}
-
-.dispatchDetailModal {
- background: #fff;
- border-radius: 8px;
- width: 800px;
- height: 90vh;
- display: flex;
- flex-direction: column;
- box-shadow: 0 12px 40px rgba(0,0,0,0.2);
-}
-
-.dispatchDetailModal .woDispatchHeader {
- padding: 14px 20px;
-}
-
-.dispatchDetailModal .woDispatchHeader h3 {
- font-size: 15px;
-}
-
-.dispatchDetailSub {
- font-size: 12px;
- color: #999;
- display: block;
- margin-top: 2px;
-}
-
-.dispatchDetailBody {
- overflow-y: auto;
- flex: 1;
- padding: 0;
- display: flex;
- flex-direction: column;
-}
-
-.dispatchDetailSection {
- padding: 16px 20px;
- border-bottom: 1px solid #eee;
-}
-
-.dispatchDetailSection:last-child {
- flex: 1;
- display: flex;
- flex-direction: column;
- border-bottom: none;
- overflow: hidden;
-}
-
-.dispatchDetailSection h4 {
- margin: 0 0 12px 0;
- font-size: 13px;
- font-weight: 600;
- text-transform: uppercase;
- color: #666;
-}
-
-.dispatchDetailGrid {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 10px;
-}
-
-.dispatchDetailField {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.dispatchDetailField label {
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- color: #999;
- margin-bottom: 0;
-}
-
-.dispatchDetailField span {
- font-size: 13px;
- color: #333;
-}
-
-.dispatchDetailField .woDispatchStatus {
- font-size: 11px;
- width: fit-content;
-}
-
-.dispatchDetailFieldFull {
- grid-column: 1 / -1;
-}
-
-.dispatchDetailActions {
- grid-column: 1 / -1;
- display: flex;
- gap: 8px;
- justify-content: flex-end;
- padding-top: 4px;
-}
-
-.dispatchWaiting {
- font-weight: 500;
- font-size: 13px;
-}
-
-.dispatchWait-green { color: #28a745; }
-.dispatchWait-yellow { color: #ffc107; }
-.dispatchWait-red { color: #dc3545; }
-
-.dispatchWaitDot {
- font-size: 12px;
- font-weight: 600;
-}
-
-.dispatchWOChips {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
-}
-
-.dispatchWOChip {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 4px 10px;
- background: #e9ecef;
- border-radius: 4px;
- font-size: 12px;
- color: #333;
-}
-
-.dispatchWOChip button {
- background: none;
- border: none;
- color: #999;
- cursor: pointer;
- font-size: 14px;
- padding: 0;
- line-height: 1;
-}
-
-.dispatchWOChip button:hover {
- color: #dc3545;
-}
-
-.dispatchWODropdown {
- border: 1px solid #ddd;
- border-radius: 4px;
- max-height: 160px;
- overflow-y: auto;
- margin-top: 4px;
-}
-
-.dispatchWOOption {
- padding: 8px 12px;
- font-size: 12px;
- cursor: pointer;
- border-bottom: 1px solid #f0f0f0;
-}
-
-.dispatchWOOption:hover {
- background: #f0f7ff;
-}
-
-.dispatchWOOption:last-child {
- border-bottom: none;
-}
-
-.dispatchChecklistAdd {
- margin-top: 8px;
-}
-
-.dispatchChecklistAdd .form-control {
- font-size: 13px;
-}
-
-.dispatchCustomItems {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-top: 6px;
-}
-
-.dispatchCustomItem {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 4px 8px;
- background: #f8f9fa;
- border-radius: 4px;
- font-size: 12px;
-}
-
-.dispatchCustomItem button {
- background: none;
- border: none;
- color: #999;
- cursor: pointer;
- font-size: 14px;
-}
-
-.dispatchCustomItem button:hover {
- color: #dc3545;
-}
-
-.dispatchCancelBtn {
- padding: 6px 14px;
- background: #fff;
- color: #dc3545;
- border: 1px solid #dc3545;
- border-radius: 4px;
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- margin-right: auto;
-}
-
-.dispatchCancelBtn:hover {
- background: #dc3545;
- color: #fff;
-}
-
-.dispatchVerifiedBanner {
- padding: 12px 20px;
- background: #d4edda;
- color: #155724;
- font-size: 13px;
- font-weight: 600;
- text-align: center;
- border-top: 1px solid #c3e6cb;
- flex-shrink: 0;
-}
-
-.dispatchVerifyFooter {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 12px;
- padding: 12px 20px;
- border-top: 1px solid #eee;
- background: #fafafa;
- flex-shrink: 0;
-}
-
-.dispatchVerifyMissing {
- font-size: 12px;
- color: #999;
- flex: 1;
-}
-
-.dispatchSignoffs {
- display: flex;
- gap: 16px;
- margin-bottom: 16px;
-}
-
-.dispatchSignoffSlot {
- flex: 1;
- border: 1px solid #eee;
- border-radius: 6px;
- padding: 12px;
- text-align: center;
-}
-
-.dispatchSignoffSlot.signed {
- border-color: #28a745;
- background: #f0fff4;
-}
-
-.dispatchSignoffLabel {
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- color: #999;
- margin-bottom: 8px;
-}
-
-.dispatchSignoffDone {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6px;
-}
-
-.dispatchSignoffImg {
- max-width: 160px;
- max-height: 60px;
- border: 1px solid #eee;
- border-radius: 4px;
-}
-
-.dispatchSignoffTyped {
- font-family: "Brush Script MT", "Segoe Script", cursive;
- font-size: 24px;
- color: #333;
-}
-
-.dispatchSignoffMeta {
- font-size: 11px;
- color: #28a745;
-}
-
-.dispatchSignoffBtn {
- font-size: 12px;
-}
-
-.dispatchSignoffBadge {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 8px;
- background: #28a745;
- color: #fff;
- border-radius: 6px;
- font-size: 13px;
- font-weight: 600;
- min-width: 100px;
-}
-
-.dispatchChecklist {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-bottom: 16px;
-}
-
-.dispatchChecklistItem {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 6px 8px;
- border-radius: 4px;
- font-size: 13px;
-}
-
-.dispatchChecklistItem:hover {
- background: #f8f9fa;
-}
-
-.dispatchChecklistItem.completed .dispatchChecklistText {
- text-decoration: line-through;
- color: #999;
-}
-
-.dispatchChecklistText {
- flex: 1;
- color: #333;
-}
-
-.dispatchChecklistDone {
- font-size: 11px;
- color: #28a745;
-}
-
-.dispatchChecklistItem input[type="checkbox"] {
- width: 16px;
- height: 16px;
- flex-shrink: 0;
- cursor: pointer;
-}
-
-.dispatchThreadContainer {
- display: flex;
- flex-direction: column;
- flex: 1;
- min-height: 200px;
- border: 1px solid #e5e5e5;
- border-radius: 6px;
- overflow: hidden;
-}
-
-.dispatchThreadScroll {
- flex: 1;
- overflow-y: auto;
- padding: 12px;
-}
-
-.woTimelineInbound {
- border-left-color: #28a745;
-}
-
-.dispatchThreadInput {
- border-top: 1px solid #e5e5e5;
- padding: 10px 12px;
- background: #f8f9fa;
-}
-
-.dispatchThreadInput .woCommentTextarea {
- width: 100%;
- box-sizing: border-box;
-}
-
-.dispatchThreadControls {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 6px;
-}
-
-.dispatchEmailToggle {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #666;
- cursor: pointer;
-}
-
-.loading {
- padding: 40px;
- text-align: center;
- color: #999;
-}
-
-.dispatchUpliftPanel {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-bottom: 12px;
-}
-
-.dispatchUpliftRow {
- padding: 12px;
- border: 1px solid #e4e7eb;
- border-radius: 8px;
- background: #fafbfc;
-}
-
-.dispatchUpliftHeader {
- display: flex;
- align-items: center;
- gap: 10px;
- flex-wrap: wrap;
- margin-bottom: 6px;
-}
-
-.dispatchUpliftAmount {
- font-weight: 600;
- color: #1f2937;
-}
-
-.dispatchUpliftDelta {
- color: #059669;
- font-weight: 500;
-}
-
-.dispatchUpliftTier {
- font-size: 12px;
- padding: 2px 8px;
- border-radius: 999px;
- background: #fef3c7;
- color: #92400e;
- font-weight: 600;
-}
-
-.dispatchUpliftPill {
- display: inline-block;
- padding: 3px 9px;
- border-radius: 999px;
- font-size: 12px;
- font-weight: 600;
-}
-
-.dispatchUpliftPill-pending { background: #fef3c7; color: #92400e; }
-.dispatchUpliftPill-approved { background: #d1fae5; color: #065f46; }
-.dispatchUpliftPill-denied { background: #fee2e2; color: #991b1b; }
-.dispatchUpliftPill-cancelled { background: #e5e7eb; color: #4b5563; }
-
-.dispatchUpliftReason {
- font-size: 13px;
- color: #374151;
- margin-top: 4px;
-}
-
-.dispatchUpliftMeta {
- font-size: 12px;
- color: #6b7280;
- margin-top: 2px;
-}
-
-.dispatchUpliftActions {
- display: flex;
- gap: 8px;
- margin-top: 10px;
-}
diff --git a/src/pages/workorders/view/WorkOrderViewPage.js b/src/pages/workorders/view/WorkOrderViewPage.js
deleted file mode 100644
index 332bff69..00000000
--- a/src/pages/workorders/view/WorkOrderViewPage.js
+++ /dev/null
@@ -1,1264 +0,0 @@
-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, fetchUsers, changeWorkOrderAssignment, fetchVendorsDropdown, dispatchToVendor, getDispatchById, updateDispatch, addDispatchComment, fetchTaskTemplates, updateChecklistItem, addChecklistItem, addDispatchSignoff, verifyDispatch } from "../api";
-import { upliftApi } from "../../../services/api";
-import { getCurrentUser } from "../../../services/authService";
-import { timeSince, timeBetween, waitTimeColor } from "../../../utils/timeSince";
-import SignatureCapture from "../../../components/SignatureCapture";
-import "../../../components/SignatureCapture.css";
-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 (
- <>
-
- {open && ReactDOM.createPortal(
-
-
- {users.map((u) => (
-
- ))}
-
,
- document.body
- )}
- >
- );
-}
-
-function CollapsibleSection({ title, defaultOpen = false, children }) {
- const [open, setOpen] = useState(defaultOpen);
- return (
-
-
setOpen(!open)}>
- ▸
- {title}
-
- {open && children}
-
- );
-}
-
-function formatDateTime(value) {
- const d = new Date(value.endsWith("Z") ? value : value + "Z");
- if (isNaN(d)) return value;
- const date = d.toLocaleDateString("en-CA");
- const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", hour12: false });
- const tz = d.toLocaleTimeString(undefined, { timeZoneName: "short" }).split(" ").pop();
- return `${date} ${time} ${tz}`;
-}
-
-function CommentTimeline({ comments }) {
- const bottomRef = useRef(null);
- const sorted = [...(comments || [])].sort((a, b) =>
- new Date(a.createdDate) - new Date(b.createdDate)
- );
-
- useEffect(() => {
- bottomRef.current?.scrollIntoView();
- }, [sorted.length]);
-
- if (!sorted.length) {
- return ;
- }
-
- return (
-
-
- {sorted.map((c) => (
-
-
- {c.userName || c.commenter || "System"}
-
- {c.createdDate ? formatDateTime(c.createdDate) : ""}
-
-
-
{c.commenttext}
-
- ))}
-
-
-
- );
-}
-
-function AuditTimeline({ entries }) {
- const bottomRef = useRef(null);
- const sorted = [...(entries || [])].sort((a, b) =>
- new Date(a.createdAt) - new Date(b.createdAt)
- );
-
- useEffect(() => {
- bottomRef.current?.scrollIntoView();
- }, [sorted.length]);
-
- if (!sorted.length) {
- return ;
- }
-
- return (
-
-
- {sorted.map((e) => (
-
-
- {e.userName || "System"}
-
- {e.createdAt ? formatDateTime(e.createdAt) : ""}
-
-
-
- {e.fieldName}
- {e.oldValue && {e.oldValue}}
- →
- {e.newValue}
-
-
- ))}
-
-
-
- );
-}
-
-const DISPATCH_STATUSES = ["Sent", "Acknowledged", "In Progress", "Completed", "Verified", "Cancelled"];
-
-function DispatchDetailModal({ dispatchId, onClose, onUpdated }) {
- const [dispatch, setDispatch] = useState(null);
- const [loading, setLoading] = useState(true);
- const [editing, setEditing] = useState(false);
- const [newChecklistItem, setNewChecklistItem] = useState("");
- const [form, setForm] = useState({});
- const [saving, setSaving] = useState(false);
- const [commentText, setCommentText] = useState("");
- const [sendEmail, setSendEmail] = useState(true);
- const [sendingComment, setSendingComment] = useState(false);
- const [sigCapture, setSigCapture] = useState(null);
- const [uplifts, setUplifts] = useState([]);
- const bottomRef = useRef(null);
-
- const loadUplifts = async () => {
- try {
- const res = await upliftApi.listForDispatch(dispatchId);
- setUplifts(res?.data ?? res ?? []);
- } catch {
- setUplifts([]);
- }
- };
-
- const loadDispatch = async () => {
- try {
- const data = await getDispatchById(dispatchId);
- setDispatch(data);
- setForm({
- status: data.status || "",
- nteAmount: data.nteAmount != null ? String(data.nteAmount) : "",
- scheduledDate: data.scheduledDate ? data.scheduledDate.split("T")[0] : "",
- completedDate: data.completedDate ? data.completedDate.split("T")[0] : "",
- description: data.description || "",
- });
- await loadUplifts();
- } catch {
- toast.error("Failed to load dispatch");
- } finally {
- setLoading(false);
- }
- };
-
- const handleApproveUplift = async (req) => {
- const note = window.prompt("Optional approval note:", "") || "";
- if (!window.confirm(`Approve uplift to $${Number(req.requestedNTE).toFixed(2)}?`)) return;
- try {
- await upliftApi.approve(req.id, note);
- toast.success("Uplift approved — NTE updated");
- await loadDispatch();
- onUpdated();
- } catch (err) {
- toast.error(err.message || "Failed to approve uplift");
- }
- };
-
- const handleDenyUplift = async (req) => {
- const note = window.prompt("Denial reason (required):", "");
- if (!note || !note.trim()) {
- toast.error("A reason is required when denying.");
- return;
- }
- try {
- await upliftApi.deny(req.id, note.trim());
- toast.success("Uplift denied");
- await loadDispatch();
- onUpdated();
- } catch (err) {
- toast.error(err.message || "Failed to deny uplift");
- }
- };
-
- useEffect(() => { loadDispatch(); }, [dispatchId]);
- useEffect(() => { bottomRef.current?.scrollIntoView(); }, [dispatch?.comments?.length]);
-
- const handleSave = async () => {
- setSaving(true);
- try {
- await updateDispatch({
- Id: dispatchId,
- Status: form.status,
- NTEAmount: form.nteAmount ? parseFloat(form.nteAmount) : null,
- ScheduledDate: form.scheduledDate || null,
- CompletedDate: form.completedDate || null,
- Description: form.description,
- });
- toast.success("Dispatch updated");
- setEditing(false);
- loadDispatch();
- onUpdated();
- } catch {
- toast.error("Failed to update dispatch");
- } finally {
- setSaving(false);
- }
- };
-
- const handleComment = async (e) => {
- e.preventDefault();
- if (!commentText.trim()) return;
- setSendingComment(true);
- try {
- const newComment = await addDispatchComment(dispatchId, commentText.trim(), sendEmail);
- setCommentText("");
- setDispatch((prev) => ({
- ...prev,
- comments: [...(prev.comments || []), newComment],
- }));
- } catch {
- toast.error("Failed to send message");
- } finally {
- setSendingComment(false);
- }
- };
-
- if (loading) {
- return ReactDOM.createPortal(
- ,
- document.body
- );
- }
-
- if (!dispatch) return null;
-
- return ReactDOM.createPortal(
-
-
e.stopPropagation()}>
-
-
-
{dispatch.dispatchNumber || dispatch.poNumber} · {dispatch.vendorName}
-
- PO: {dispatch.poNumber} · {dispatch.workOrders?.length > 0 ? `${dispatch.workOrders.length} WO(s): ${dispatch.workOrders.map(w => w.woNumber).join(", ")}` : `WO: ${dispatch.woNumber}`} · {dispatch.vendorEmail}
-
-
-
-
-
-
-
-
- {editing ? (
- <>
-
-
-
-
-
-
- setForm(f => ({ ...f, nteAmount: e.target.value }))} step="0.01" />
-
-
-
- setForm(f => ({ ...f, scheduledDate: e.target.value }))} />
-
-
-
- setForm(f => ({ ...f, completedDate: e.target.value }))} />
-
-
-
-
-
-
-
-
- >
- ) : (
- <>
-
-
- {dispatch.status}
-
-
-
- {dispatch.nteAmount != null ? `$${parseFloat(dispatch.nteAmount).toFixed(2)}` : "—"}
-
-
-
- {dispatch.scheduledDate ? dispatch.scheduledDate.split("T")[0] : "—"}
-
-
-
- {dispatch.completedDate ? dispatch.completedDate.split("T")[0] : "—"}
-
-
-
- {dispatch.dispatchedAt ? formatDateTime(dispatch.dispatchedAt) : "—"}
-
-
-
- {dispatch.acknowledgedAt ? (
- {formatDateTime(dispatch.acknowledgedAt)} (after {timeBetween(dispatch.dispatchedAt, dispatch.acknowledgedAt)})
- ) : dispatch.status === "Sent" ? (
- Awaiting acceptance · {timeSince(dispatch.dispatchedAt)}
- ) : (
- —
- )}
-
-
-
- {dispatch.vendorEmail}
-
- {dispatch.description && (
-
-
- {dispatch.description}
-
- )}
-
- {!dispatch.verifiedAt && dispatch.status !== "Cancelled" && (
-
- )}
- {!dispatch.verifiedAt && dispatch.status !== "Cancelled" && }
-
- >
- )}
-
-
-
-
- {((dispatch.checklistItems && dispatch.checklistItems.length > 0) || !dispatch.verifiedAt) && (
- <>
-
Checklist ({(dispatch.checklistItems || []).filter(i => i.isCompleted).length}/{(dispatch.checklistItems || []).length})
-
- {(dispatch.checklistItems || []).map((item) => (
-
- {
- const currentUser = getCurrentUser();
- const userName = currentUser?.fullname || "User";
- await updateChecklistItem(item.id, !item.isCompleted, userName);
- setDispatch((prev) => ({
- ...prev,
- checklistItems: prev.checklistItems.map((ci) =>
- ci.id === item.id ? { ...ci, isCompleted: !ci.isCompleted, completedBy: !ci.isCompleted ? userName : null, completedAt: !ci.isCompleted ? new Date().toISOString() : null } : ci
- ),
- }));
- }}
- />
- {item.itemText}
- {item.isCompleted && (
-
- {item.completedBy}{item.completedAt ? ` · ${formatDateTime(item.completedAt)}` : ""}
-
- )}
-
- ))}
-
- {!dispatch.verifiedAt && (
-
- setNewChecklistItem(e.target.value)}
- onKeyDown={async (e) => {
- if (e.key === "Enter" && newChecklistItem.trim()) {
- e.preventDefault();
- const result = await addChecklistItem(dispatchId, newChecklistItem.trim());
- setDispatch((prev) => ({
- ...prev,
- checklistItems: [...(prev.checklistItems || []), { ...result, isCompleted: false }],
- }));
- setNewChecklistItem("");
- }
- }}
- placeholder="Add checklist item..."
- />
-
- )}
- >
- )}
-
Sign-offs
-
- {["Customer", "Vendor"].map((type) => {
- const signoff = (dispatch.signoffs || []).find((s) => s.signoffType === type);
- return (
-
-
{type}
- {signoff ? (
-
- {signoff.signatureMethod === "drawn" && signoff.signature && (
-

- )}
- {signoff.signatureMethod === "typed" && (
-
{signoff.signature}
- )}
-
{signoff.name} · {signoff.signedAt ? formatDateTime(signoff.signedAt) : ""}
-
- ) : (
-
- )}
-
- );
- })}
- {(dispatch.signoffs || []).length >= 2 &&
All Signed ✓
}
-
-
- {sigCapture && (
-
setSigCapture(null)}
- onSave={async (signature, method, name) => {
- try {
- const result = await addDispatchSignoff(dispatchId, sigCapture, name, signature, method);
- setDispatch((prev) => ({
- ...prev,
- signoffs: [...(prev.signoffs || []), { ...result, signature, signatureMethod: method }],
- }));
- setSigCapture(null);
- toast.success(`${sigCapture} sign-off collected`);
- } catch {
- toast.error("Failed to save sign-off");
- }
- }}
- />
- )}
-
- NTE Uplift Requests
-
- {uplifts.length === 0 &&
No uplift requests.
}
- {uplifts.map((u) => {
- const locked = dispatch.status === "Verified" || dispatch.status === "Cancelled";
- const canDecide = u.canDecide && u.status === "Pending" && !locked;
- return (
-
-
- {u.status}
- {u.requiredTier === 2 && Tier 2 · Manager approval}
-
- ${Number(u.currentNTE ?? 0).toFixed(2)} → ${Number(u.requestedNTE).toFixed(2)}
- (+${Number(u.delta).toFixed(2)})
-
-
- {u.vendorReason &&
Reason: {u.vendorReason}
}
-
- Requested {u.requestedAt ? formatDateTime(u.requestedAt) : ""}
- {u.requestedByVendorName ? ` by ${u.requestedByVendorName}` : ""}
-
- {u.decidedAt && (
-
- Decided {formatDateTime(u.decidedAt)}
- {u.decidedByName ? ` by ${u.decidedByName}` : ""}
-
- )}
- {u.decisionNote &&
Note: {u.decisionNote}
}
- {u.status === "Pending" && (
-
-
-
-
- )}
-
- );
- })}
-
-
- Vendor Communication
-
-
- {(!dispatch.comments || dispatch.comments.length === 0) ? (
-
No messages yet.
- ) : (
-
- {dispatch.comments.map((c) => (
-
-
- {c.userName || c.commenter || "System"}
- {c.createdDate ? formatDateTime(c.createdDate) : ""}
-
-
{c.commenttext}
-
- ))}
-
-
- )}
-
-
-
-
-
- {dispatch.verifiedAt ? (
-
- ✓ Verified by {dispatch.verifiedBy} on {formatDateTime(dispatch.verifiedAt)}
-
- ) : (
-
- {(() => {
- const checklistTotal = (dispatch.checklistItems || []).length;
- const checklistDone = (dispatch.checklistItems || []).filter(i => i.isCompleted).length;
- const signoffCount = (dispatch.signoffs || []).length;
- const missing = [];
- if (checklistTotal > 0 && checklistDone < checklistTotal) missing.push(`${checklistTotal - checklistDone} checklist item(s)`);
- if (signoffCount < 2) {
- const types = (dispatch.signoffs || []).map(s => s.signoffType);
- if (!types.includes("Customer")) missing.push("Customer sign-off");
- if (!types.includes("Vendor")) missing.push("Vendor sign-off");
- }
- const canVerify = missing.length === 0 && (checklistTotal > 0 || signoffCount >= 2);
- return (
- <>
- {missing.length > 0 && Missing: {missing.join(", ")}}
-
- >
- );
- })()}
-
- )}
-
-
-
,
- document.body
- );
-}
-
-function DispatchModal({ wo, onClose, onDispatched }) {
- const [vendors, setVendors] = useState([]);
- const [selectedId, setSelectedId] = useState(null);
- const [nteAmount, setNteAmount] = useState(wo.vendorNTE || "");
- const [scheduledDate, setScheduledDate] = useState("");
- const [description, setDescription] = useState(wo.description || "");
- const [vendorSearch, setVendorSearch] = useState("");
- const [sending, setSending] = useState(false);
- const [taskTemplates, setTaskTemplates] = useState([]);
- const [selectedTemplateId, setSelectedTemplateId] = useState("");
- const [customItems, setCustomItems] = useState([]);
- const [newCustomItem, setNewCustomItem] = useState("");
- const [loaded, setLoaded] = useState(false);
- const [selectedWOs, setSelectedWOs] = useState([{ id: wo.id, number: wo.internalWONumber, title: wo.title }]);
- const [siteWOs, setSiteWOs] = useState([]);
- const [woSearch, setWoSearch] = useState("");
-
- useEffect(() => {
- fetchVendorsDropdown(wo.trade, wo.locationZip).then((data) => {
- setVendors(data);
- setLoaded(true);
- }).catch(() => setLoaded(true));
- fetchTaskTemplates().then(setTaskTemplates).catch(() => {});
- if (wo.locationId) {
- const { get: apiGet } = require("../../../apiUtil");
- const { API_URL } = require("../../../constants");
- apiGet(`${API_URL}/WorkOrder/GetWorkOrderList?search=&page=1&pageSize=500&sortBy=number&sortDir=asc&locationId=${wo.locationId}`).then((res) => {
- setSiteWOs((res?.data || []).filter(w => w.id !== wo.id));
- }).catch(() => {});
- }
- }, [wo.trade]);
-
- const selectVendor = (id) => {
- setSelectedId(prev => prev === id ? null : id);
- };
-
- const filteredVendors = vendors.filter((v) =>
- (v.companyName || "").toLowerCase().includes(vendorSearch.toLowerCase()) ||
- (v.tradeSpecialties || "").toLowerCase().includes(vendorSearch.toLowerCase())
- );
-
- const handleSubmit = async () => {
- if (!selectedId) { toast.error("Select a vendor"); return; }
- setSending(true);
- try {
- await dispatchToVendor({
- WorkOrderIds: selectedWOs.map(w => w.id),
- VendorIds: [selectedId],
- NTEAmount: nteAmount ? parseFloat(nteAmount) : null,
- ScheduledDate: scheduledDate || null,
- Description: description,
- TaskListTemplateId: selectedTemplateId ? parseInt(selectedTemplateId) : null,
- CustomChecklistItems: customItems.length > 0 ? customItems : null,
- });
- toast.success("Dispatched successfully");
- onDispatched();
- onClose();
- } catch {
- toast.error("Failed to dispatch");
- } finally {
- setSending(false);
- }
- };
-
- return ReactDOM.createPortal(
-
-
e.stopPropagation()}>
-
-
Dispatch {selectedWOs.length > 1 ? `${selectedWOs.length} Work Orders` : `Work Order ${wo.internalWONumber}`}
-
-
-
-
-
-
- {selectedWOs.map((w) => (
-
- {w.number} — {w.title}
- {w.id !== wo.id && }
-
- ))}
-
- {siteWOs.length > 0 && (
- <>
-
setWoSearch(e.target.value)}
- placeholder="Search to filter..."
- style={{ marginTop: 8 }}
- />
-
- {siteWOs
- .filter(w => !selectedWOs.find(s => s.id === w.id))
- .filter(w => !woSearch || (w.number || "").includes(woSearch) || (w.title || "").toLowerCase().includes(woSearch.toLowerCase()))
- .slice(0, 10)
- .map(w => (
-
{
- setSelectedWOs(prev => [...prev, { id: w.id, number: w.number, title: w.title }]);
- setWoSearch("");
- }}>
- {w.number} — {w.title}
-
- ))
- }
- {siteWOs.filter(w => !selectedWOs.find(s => s.id === w.id)).length === 0 && (
-
All work orders at this site are selected
- )}
-
- >
- )}
-
-
-
-
setVendorSearch(e.target.value)}
- placeholder="Search vendors..."
- />
- {!loaded ?
Loading vendors...
: filteredVendors.length === 0 ?
No vendors found
: (
-
- {filteredVendors.map((v) => (
-
selectVendor(v.id)}>
-
-
- {v.companyName}
- {v.tradeSpecialties && {v.tradeSpecialties}}
- {v.address && {v.address}}
-
- {v.distanceMiles != null && (
-
- {v.distanceMiles}
- miles
-
- )}
-
- ))}
-
- )}
- {selectedId && (
-
-
- {vendors.find((x) => x.id === selectedId)?.companyName}
-
-
-
- )}
-
-
-
-
-
- {customItems.length > 0 && (
-
- {customItems.map((item, idx) => (
-
- {item}
-
-
- ))}
-
- )}
-
- setNewCustomItem(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter" && newCustomItem.trim()) {
- e.preventDefault();
- setCustomItems(prev => [...prev, newCustomItem.trim()]);
- setNewCustomItem("");
- }
- }}
- placeholder="Add custom checklist item..."
- />
-
-
-
-
-
-
-
-
-
-
-
-
,
- document.body
- );
-}
-
-function CommentInput({ workOrderId, commentType, onCommentAdded }) {
- const [text, setText] = useState("");
- const [sending, setSending] = useState(false);
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!text.trim()) return;
- setSending(true);
- try {
- const newComment = await addComment(workOrderId, text.trim(), commentType);
- setText("");
- if (onCommentAdded) onCommentAdded(newComment);
- } catch (err) {
- toast.error("Failed to add comment");
- } finally {
- setSending(false);
- }
- };
-
- const handleKeyDown = (e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- handleSubmit(e);
- }
- };
-
- return (
-
- );
-}
-
-const STATUS_OPTIONS = ["Open", "In Progress", "On Hold", "Done", "Cancelled"];
-
-function StatusDropdown({ status, onStatusChange }) {
- const [open, setOpen] = useState(false);
- const btnRef = useRef(null);
- const menuRef = useRef(null);
- const [menuPos, setMenuPos] = useState({ top: 0, left: 0 });
- const statusClass = (status || "").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);
- };
-
- return (
- <>
-
- {open && ReactDOM.createPortal(
-
- {STATUS_OPTIONS.map((s) => (
-
- ))}
-
,
- document.body
- )}
- >
- );
-}
-
-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 [dispatchOpen, setDispatchOpen] = useState(false);
- const [dispatchDetailId, setDispatchDetailId] = useState(null);
-
- const handleStatusChange = async (newStatus) => {
- if (newStatus === wo?.status) return;
- setStatusUpdating(true);
- try {
- await changeWorkOrderStatus(id, newStatus);
- const oldStatus = wo.status;
- setWo((prev) => ({
- ...prev,
- status: newStatus,
- auditLog: [
- {
- id: Date.now(),
- fieldName: "Status",
- oldValue: oldStatus,
- newValue: newStatus,
- action: "status_change",
- createdAt: new Date().toISOString(),
- userName: "You",
- },
- ...(prev.auditLog || []),
- ],
- }));
- toast.success(`Status changed to ${newStatus}`);
- } catch (err) {
- toast.error("Failed to update status");
- } finally {
- setStatusUpdating(false);
- }
- };
-
- useEffect(() => {
- const load = async () => {
- try {
- const data = await getWorkOrderById(id);
- setWo(data);
- } catch (err) {
- toast.error("Failed to load work order");
- } finally {
- setLoading(false);
- }
- };
- load();
- }, [id]);
-
- const handleCommentAdded = (newComment) => {
- setWo((prev) => ({
- ...prev,
- comments: [
- ...prev.comments,
- {
- id: newComment.id,
- createdDate: newComment.createdDate,
- commenttext: newComment.commenttext,
- commentType: newComment.commentType,
- userName: newComment.userName,
- commenter: null,
- recordType: null,
- },
- ],
- }));
- };
-
- if (loading) return ;
- if (!wo) return ;
-
- const allComments = wo.comments || [];
- const customerComments = allComments.filter((c) => c.commentType === "customer");
- const internalComments = allComments.filter((c) => c.commentType === "internal");
- const auditLog = wo.auditLog || [];
-
- const tabs = [
- { key: "customer", label: "Customer", count: customerComments.length },
- { key: "internal", label: "Internal", count: internalComments.length },
- { key: "audit", label: "Audit Log", count: auditLog.length },
- ];
-
- const showInput = activeTab === "internal";
-
- return (
-
-
-
-
- -
- { e.preventDefault(); navigate("/workorders"); }}>
- Work Orders
-
-
- - {wo.internalWONumber}
-
-
-
-
-
{wo.title}
-
-
-
-
-
-
-
-
-
-
-
-
Details
-
-
- Internal WO
- {wo.internalWONumber}
-
-
- Customer WO
- {wo.workOrderNumber || "—"}
-
-
- Priority
- {wo.priority || "—"}
-
-
- Location
- {wo.location || "—"}
-
-
-
Assigned To
-
- {
- try {
- await changeWorkOrderAssignment(wo.id, userId);
- setWo((prev) => ({ ...prev, assignedTo: userName }));
- toast.success(`Assigned to ${userName || "Unassigned"}`);
- } catch { toast.error("Failed to update assignment"); }
- }}
- />
-
-
-
- Source
- {wo.source || "—"}
-
-
- TT
- {wo.tt || "—"}
-
-
-
-
-
-
-
- Problem
- {wo.problem || "—"}
-
-
- Trade
- {wo.trade || "—"}
-
-
- Sub-Trade
- {wo.subTrade || "—"}
-
-
-
-
-
-
-
- Created
-
- {wo.createdDateFull ? formatDateTime(wo.createdDateFull) : "—"}
-
-
-
- Due Date
- {wo.dueDate || "—"}
-
-
- Scheduled
- {wo.scheduledDate || "—"}
-
-
- Completed
- {wo.completedDate || "—"}
-
-
-
-
-
-
-
- Vendor NTE
- {wo.vendorNTE ? `$${parseFloat(wo.vendorNTE).toFixed(2)}` : "—"}
-
-
- Customer PO
- {wo.po || "—"}
-
-
- Customer NTE
- Not connected
-
-
-
-
-
- {wo.dispatches?.length ? (
-
-
- | Dispatch # | Vendor | NTE | Status | Date | Wait |
-
-
- {wo.dispatches.map(d => (
- setDispatchDetailId(d.id)}>
- | {d.dispatchNumber || d.poNumber} |
- {d.vendorName} |
- {d.nteAmount != null ? `$${parseFloat(d.nteAmount).toFixed(2)}` : "—"} |
- {d.status} |
- {d.dispatchedAt ? formatDateTime(d.dispatchedAt) : "—"} |
- {d.status === "Sent" ? {timeSince(d.dispatchedAt)} : "—"} |
-
- ))}
-
-
- ) : (
- No dispatches yet.
- )}
-
-
-
- {wo.description || "No description provided."}
-
-
-
-
Activity
-
- {tabs.map((tab) => (
-
- ))}
-
-
- {activeTab === "customer" &&
}
- {activeTab === "internal" &&
}
- {activeTab === "audit" &&
}
- {showInput && (
-
- )}
-
-
-
-
-
-
- {dispatchOpen && (
-
setDispatchOpen(false)}
- onDispatched={async () => {
- const data = await getWorkOrderById(id);
- setWo(data);
- }}
- />
- )}
-
- {dispatchDetailId && (
- setDispatchDetailId(null)}
- onUpdated={async () => {
- const data = await getWorkOrderById(id);
- setWo(data);
- }}
- />
- )}
-
- );
-}
diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js
deleted file mode 100644
index 5253d3ad..00000000
--- a/src/reportWebVitals.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const reportWebVitals = onPerfEntry => {
- if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
- }
-};
-
-export default reportWebVitals;
diff --git a/src/services/api.js b/src/services/api.js
deleted file mode 100644
index 4e4f3f58..00000000
--- a/src/services/api.js
+++ /dev/null
@@ -1,168 +0,0 @@
-// API Base Configuration
-import { getAccessToken } from "../tokenUtility";
-
-const API_BASE_URL = process.env.REACT_APP_API_URL || '';
-const API_PREFIX = '/api';
-
-// HTTP Methods Helper
-class ApiService {
- constructor(baseURL = API_BASE_URL, prefix = API_PREFIX) {
- this.baseURL = baseURL;
- this.prefix = prefix;
- }
-
- async request(endpoint, options = {}) {
- const url = `${this.baseURL}${this.prefix}${endpoint}`;
- const config = {
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': getAccessToken(),
- ...options.headers,
- },
- ...options,
- };
-
- try {
- const response = await fetch(url, config);
-
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.message || `HTTP Error: ${response.status}`);
- }
-
- // Handle 204 No Content
- if (response.status === 204) {
- return null;
- }
-
- return await response.json();
- } catch (error) {
- console.error('API Request Error:', error);
- throw error;
- }
- }
-
- async get(endpoint, params = {}) {
- const queryString = new URLSearchParams(params).toString();
- const url = queryString ? `${endpoint}?${queryString}` : endpoint;
- return this.request(url, { method: 'GET' });
- }
-
- async post(endpoint, data) {
- return this.request(endpoint, {
- method: 'POST',
- body: JSON.stringify(data),
- });
- }
-
- async put(endpoint, data) {
- return this.request(endpoint, {
- method: 'PUT',
- body: JSON.stringify(data),
- });
- }
-
- async delete(endpoint) {
- return this.request(endpoint, { method: 'DELETE' });
- }
-}
-
-// Create singleton instance
-const api = new ApiService();
-
-// Employee API
-export const employeeApi = {
- getAll: (params) => api.get('/employees', params),
- getById: (id) => api.get(`/employees/${id}`),
- create: (data) => api.post('/employees', data),
- update: (id, data) => api.put(`/employees/${id}`, data),
- delete: (id) => api.delete(`/employees/${id}`),
-};
-
-// Contact API
-export const contactApi = {
- getAll: (params) => api.get('/contacts', params),
- getById: (id) => api.get(`/contacts/${id}`),
- create: (data) => api.post('/contacts', data),
- update: (id, data) => api.put(`/contacts/${id}`, data),
- delete: (id) => api.delete(`/contacts/${id}`),
-};
-
-// Account API
-export const accountApi = {
- getAll: (params) => api.get('/accounts', params),
- getById: (id) => api.get(`/accounts/${id}`),
- create: (data) => api.post('/accounts', data),
- update: (id, data) => api.put(`/accounts/${id}`, data),
- delete: (id) => api.delete(`/accounts/${id}`),
-};
-
-// Location API
-export const locationApi = {
- getAll: (params) => api.get('/locations', params),
- getById: (id) => api.get(`/locations/${id}`),
- create: (data) => api.post('/locations', data),
- update: (id, data) => api.put(`/locations/${id}`, data),
- delete: (id) => api.delete(`/locations/${id}`),
-};
-
-// Work Order API
-export const workOrderApi = {
- getAll: (params) => api.get('/workorders', params),
- getById: (id) => api.get(`/workorders/${id}`),
- create: (data) => api.post('/workorders', data),
- update: (id, data) => api.put(`/workorders/${id}`, data),
- delete: (id) => api.delete(`/workorders/${id}`),
-};
-
-// PM Schedule API
-export const pmScheduleApi = {
- getAll: (params) => api.get('/pmschedules', params),
- getById: (id) => api.get(`/pmschedules/${id}`),
- create: (data) => api.post('/pmschedules', data),
- update: (id, data) => api.put(`/pmschedules/${id}`, data),
- delete: (id) => api.delete(`/pmschedules/${id}`),
-};
-
-// Follow-up API
-export const followupApi = {
- getAll: (params) => api.get('/followups', params),
- getById: (id) => api.get(`/followups/${id}`),
- create: (data) => api.post('/followups', data),
- update: (id, data) => api.put(`/followups/${id}`, data),
- delete: (id) => api.delete(`/followups/${id}`),
-};
-
-export const vendorApi = {
- getAll: (params) => api.get('/vendors', params),
- getById: (id) => api.get(`/vendors/${id}`),
- create: (data) => api.post('/vendors', data),
- update: (id, data) => api.put(`/vendors/${id}`, data),
- delete: (id) => api.delete(`/vendors/${id}`),
- getPortalToken: (id) => api.get(`/vendors/${id}/portal-token`),
- rotatePortalToken: (id) => api.post(`/vendors/${id}/portal-token/rotate`),
- revokePortalToken: (id) => api.post(`/vendors/${id}/portal-token/revoke`),
-};
-
-export const taskTemplateApi = {
- getAll: () => api.get('/tasklisttemplates'),
- getById: (id) => api.get(`/tasklisttemplates/${id}`),
- create: (data) => api.post('/tasklisttemplates', data),
- update: (id, data) => api.put(`/tasklisttemplates/${id}`, data),
- delete: (id) => api.delete(`/tasklisttemplates/${id}`),
-};
-
-export const upliftApi = {
- list: (params) => api.get('/uplifts', params),
- listForDispatch: (dispatchId) => api.get(`/uplifts/dispatch/${dispatchId}`),
- approve: (id, note) => api.post(`/uplifts/${id}/approve`, { note }),
- deny: (id, note) => api.post(`/uplifts/${id}/deny`, { note }),
-};
-
-export const dispatchApi = {
- list: (params) => api.get('/dispatches', params),
- getById: (id) => api.get(`/dispatches/${id}`),
-};
-
-export default api;
-
diff --git a/src/services/authService.js b/src/services/authService.js
deleted file mode 100644
index c08ab966..00000000
--- a/src/services/authService.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { getAccessToken, removeToken, getUser, isAuthenticated, getRawToken } from "../tokenUtility";
-
-const API_BASE_URL = process.env.REACT_APP_API_URL || "";
-
-export { isAuthenticated };
-
-export function getCurrentUser() {
- return getUser();
-}
-
-export function getToken() {
- return getRawToken();
-}
-
-export function logout() {
- removeToken();
-}
-
-export async function changePassword(currentPassword, newPassword, confirmPassword) {
- const response = await fetch(`${API_BASE_URL}/api/Authentication/ChangePassword`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": getAccessToken(),
- },
- body: JSON.stringify({
- Currentpassword: currentPassword,
- Newpassword: newPassword,
- Confirmpassword: confirmPassword,
- }),
- });
-
- if (!response.ok) {
- throw new Error("Failed to change password");
- }
-
- return await response.json();
-}
-
-export async function forgotPassword(email) {
- const response = await fetch(`${API_BASE_URL}/api/Authentication/ForgetPassword?Email=${encodeURIComponent(email)}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (!response.ok) {
- throw new Error("Failed to send password reset email");
- }
-
- return await response.json();
-}
-
-export async function verifyResetCode(code) {
- const response = await fetch(`${API_BASE_URL}/api/Authentication/VerificationCode?code=${encodeURIComponent(code)}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- if (!response.ok) {
- throw new Error("Invalid verification code");
- }
-
- return await response.json();
-}
-
-export async function resetPassword(email, code, password) {
- const response = await fetch(`${API_BASE_URL}/api/Authentication/ResetPassword`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- Email: email,
- Code: code,
- Password: password,
- }),
- });
-
- if (!response.ok) {
- throw new Error("Failed to reset password");
- }
-
- return await response.json();
-}
diff --git a/src/setupProxy.js b/src/setupProxy.js
deleted file mode 100644
index 7751a0c3..00000000
--- a/src/setupProxy.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const { createProxyMiddleware } = require('http-proxy-middleware');
-
-const API_TARGET = process.env.REACT_APP_API_TARGET || 'http://localhost:5141';
-
-module.exports = function(app) {
- app.use(
- createProxyMiddleware({
- target: API_TARGET,
- changeOrigin: true,
- secure: false,
- pathFilter: '/api',
- })
- );
-};
\ No newline at end of file
diff --git a/src/setupTests.js b/src/setupTests.js
deleted file mode 100644
index 8f2609b7..00000000
--- a/src/setupTests.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/src/site.css b/src/site.css
deleted file mode 100644
index 17058f35..00000000
--- a/src/site.css
+++ /dev/null
@@ -1,680 +0,0 @@
-@import url("https://fonts.googleapis.com/css2?family=Dosis:wght@200;300;400;500;600&family=Nunito:ital,wght@0,200;0,300;0,400;0,600;1,200;1,300;1,400;1,600&display=swap");
-
-* {
- margin: 0;
- padding: 0;
- z-index: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: "Nunito", sans-serif;
- background-color: #f5f5f5;
- color: #333;
- line-height: 1.5;
-}
-
-/* Page Layout */
-.listPage, .contactsPage {
- padding: 20px;
- background: #f5f5f5;
- min-height: 100vh;
-}
-
-.pageTitle {
- font-size: 28px;
- font-weight: 600;
- color: #333;
- margin-bottom: 20px;
-}
-
-.card, .controlsCard {
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
- margin-bottom: 20px;
-}
-
-/* Controls */
-.controlsTop {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 15px;
- border-bottom: 1px solid #eee;
-}
-
-.leftControls {
- flex: 1;
-}
-
-.filter {
- display: flex;
- gap: 10px;
- align-items: center;
-}
-
-.filter select, .filter input {
- padding: 8px 12px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
-}
-
-.filter input {
- flex: 1;
- min-width: 300px;
-}
-
-.searchBtn {
- padding: 8px 16px;
- background: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-weight: 600;
-}
-
-.searchBtn:hover {
- background: #0056b3;
-}
-
-.rightControls {
- display: flex;
- gap: 8px;
-}
-
-.iconSmall {
- width: 36px;
- height: 36px;
- padding: 0;
- border: 1px solid #ddd;
- background: white;
- border-radius: 4px;
- cursor: pointer;
- font-size: 18px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
-}
-
-.iconSmall:hover {
- background: #f0f0f0;
- border-color: #999;
-}
-
-/* Table Styles */
-.tableContainer {
- padding: 15px;
-}
-
-.tableResponsive {
- overflow-x: auto;
-}
-
-.dataTable {
- width: 100%;
- border-collapse: collapse;
- font-size: 14px;
-}
-
-.dataTable thead {
- background: #f9f9f9;
-}
-
-.dataTable th {
- padding: 12px;
- text-align: left;
- font-weight: 600;
- color: #333;
- border-bottom: 2px solid #ddd;
-}
-
-.dataTable td {
- padding: 12px;
- border-bottom: 1px solid #eee;
- color: #333;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.gridTable {
- display: grid;
- font-size: 14px;
- min-width: 100%;
-}
-
-.gridCell {
- padding: 12px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- display: flex;
- align-items: center;
- border-right: 1px solid #eee;
-}
-
-.gridCell:last-child {
- border-right: none;
-}
-
-.gridHeader {
- font-weight: 600;
- color: #333;
- background: #f5f6f8;
- border-bottom: 2px solid #ddd;
- border-right: 1px solid #ddd;
- position: relative;
-}
-
-.gridHeader:last-child {
- border-right: none;
-}
-
-.gridBody {
- border-bottom: 1px solid #eee;
- color: #333;
-}
-
-.gridBody:hover {
- background: #f9f9f9;
-}
-
-.gridCell a {
- color: #007bff;
- text-decoration: none;
- cursor: pointer;
-}
-
-.gridCell a:hover {
- text-decoration: underline;
-}
-
-.colConfigBtn {
- font-size: 15px;
- color: #999;
-}
-
-.colConfigPanel {
- position: fixed;
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
- z-index: 99999;
- width: 220px;
- overflow: hidden;
-}
-
-.colConfigTitle {
- padding: 10px 14px;
- font-size: 12px;
- font-weight: 600;
- color: #999;
- text-transform: uppercase;
- border-bottom: 1px solid #eee;
-}
-
-.colConfigList {
- max-height: 300px;
- overflow-y: auto;
-}
-
-.colConfigItem {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 14px;
- cursor: grab;
- border-bottom: 1px solid #f5f5f5;
- font-size: 13px;
-}
-
-.colConfigItem:hover {
- background: #f8f9fa;
-}
-
-.colConfigItem.dragging {
- opacity: 0.4;
-}
-
-.colConfigDragHandle {
- color: #ccc;
- font-size: 14px;
- cursor: grab;
- flex-shrink: 0;
-}
-
-.colConfigLabel {
- display: flex;
- align-items: center;
- gap: 6px;
- cursor: pointer;
- flex: 1;
-}
-
-.colConfigLabel input[type="checkbox"] {
- width: 14px;
- height: 14px;
-}
-
-.gridSortable {
- cursor: pointer;
- user-select: none;
-}
-
-.gridSortable:hover {
- background: #eef1f4;
-}
-
-.gridHeader.sorted {
- background: #e8edf2;
-}
-
-.sortArrow {
- font-size: 10px;
- color: #007bff;
- margin-left: 4px;
-}
-
-.colConfigBtn {
- margin-right: 8px;
-}
-
-.colConfigPanel {
- position: fixed;
- background: #fff;
- border: 1px solid #ddd;
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.15);
- z-index: 99999;
- width: 220px;
- overflow: hidden;
-}
-
-.colConfigTitle {
- padding: 10px 14px;
- font-size: 13px;
- font-weight: 600;
- color: #333;
- border-bottom: 1px solid #eee;
-}
-
-.colConfigList {
- max-height: 300px;
- overflow-y: auto;
-}
-
-.colConfigItem {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 14px;
- font-size: 13px;
- cursor: grab;
- border-bottom: 1px solid #f5f5f5;
-}
-
-.colConfigItem:hover {
- background: #f8f9fa;
-}
-
-.colConfigItem.dragging {
- opacity: 0.4;
-}
-
-.colConfigDragHandle {
- color: #ccc;
- font-size: 14px;
- cursor: grab;
-}
-
-.colConfigLabel {
- display: flex;
- align-items: center;
- gap: 6px;
- cursor: pointer;
- flex: 1;
-}
-
-.colResizeHandle {
- position: absolute;
- top: 0;
- right: 0;
- width: 5px;
- height: 100%;
- cursor: col-resize;
- background: transparent;
-}
-
-.colResizeHandle:hover {
- background: #007bff;
-}
-
-.dataTable tbody tr:hover {
- background: #f9f9f9;
-}
-
-.dataTable a {
- color: #007bff;
- text-decoration: none;
- cursor: pointer;
-}
-
-.dataTable a:hover {
- text-decoration: underline;
-}
-
-.actionButtons {
- display: flex;
- gap: 8px;
-}
-
-.btnIcon {
- padding: 4px 8px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 3px;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s;
-}
-
-.btnIcon:hover {
- background: #f0f0f0;
- border-color: #999;
-}
-
-.btnIcon.edit {
- color: #28a745;
-}
-
-.btnIcon.delete {
- color: #dc3545;
-}
-
-/* Pagination */
-.paginationBar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 16px;
- border-bottom: 1px solid #eee;
- font-size: 13px;
- background: #fafafa;
-}
-
-.paginationPerPage {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.paginationLabel {
- color: #999;
- font-size: 12px;
-}
-
-.paginationSizeGroup {
- display: flex;
- border: 1px solid #ddd;
- border-radius: 4px;
- overflow: hidden;
-}
-
-.paginationSizeBtn {
- padding: 4px 12px;
- border: none;
- border-right: 1px solid #ddd;
- background: #fff;
- font-size: 12px;
- cursor: pointer;
- color: #666;
-}
-
-.paginationSizeBtn:last-child {
- border-right: none;
-}
-
-.paginationSizeBtn:hover {
- background: #f0f0f0;
-}
-
-.paginationSizeBtn.active {
- background: #007bff;
- color: #fff;
-}
-
-.paginationNav {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.paginationInfo {
- color: #999;
- font-size: 12px;
-}
-
-.paginationButtons {
- display: flex;
- align-items: center;
- gap: 2px;
-}
-
-.paginationBtn {
- width: 30px;
- height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- border: 1px solid #ddd;
- background: #fff;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- color: #333;
-}
-
-.paginationBtn:hover:not(:disabled) {
- background: #f0f0f0;
-}
-
-.paginationBtn:disabled {
- opacity: 0.3;
- cursor: not-allowed;
-}
-
-.paginationPage {
- width: 30px;
- height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #007bff;
- color: #fff;
- border-radius: 4px;
- font-size: 12px;
- font-weight: 600;
-}
-
-/* Form Styles */
-.formWrapper {
- padding: 20px;
- background: #f5f5f5;
- min-height: 100vh;
-}
-
-.formCard {
- background: white;
- border: 1px solid #ddd;
- border-radius: 4px;
- max-width: 900px;
- margin: 0 auto;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
-}
-
-.formHeader {
- padding: 20px;
- border-bottom: 1px solid #ddd;
- background: #f9f9f9;
-}
-
-.formHeader h2 {
- margin: 0;
- font-size: 22px;
- color: #333;
-}
-
-.dataForm {
- padding: 20px;
-}
-
-.section {
- margin-bottom: 25px;
-}
-
-.sectionTitle {
- font-size: 16px;
- font-weight: 600;
- color: #333;
- margin-bottom: 15px;
- border-bottom: 2px solid #007bff;
- padding-bottom: 8px;
-}
-
-.formBody {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 15px;
- margin-bottom: 15px;
-}
-
-.formGroup {
- display: flex;
- flex-direction: column;
-}
-
-.formLabel {
- font-size: 14px;
- font-weight: 600;
- margin-bottom: 6px;
- color: #333;
-}
-
-.required {
- color: #dc3545;
-}
-
-.formControl {
- padding: 10px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- font-family: inherit;
-}
-
-.formControl:focus {
- outline: none;
- border-color: #007bff;
- box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
-}
-
-.formActions {
- display: flex;
- gap: 12px;
- padding-top: 20px;
- border-top: 1px solid #ddd;
-}
-
-.btnPrimary {
- padding: 10px 24px;
- background: #007bff;
- color: white;
- border: none;
- border-radius: 4px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.btnPrimary:hover {
- background: #0056b3;
-}
-
-.btnPrimary:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.btnSecondary {
- padding: 10px 24px;
- background: white;
- color: #333;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.btnSecondary:hover {
- background: #f0f0f0;
-}
-
-/* Loading State */
-.loading {
- padding: 40px;
- text-align: center;
- color: #666;
- font-size: 16px;
-}
-
-.error {
- padding: 15px;
- background: #f8d7da;
- border: 1px solid #f5c6cb;
- color: #721c24;
- border-radius: 4px;
- margin: 15px;
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .controlsTop {
- flex-direction: column;
- align-items: stretch;
- gap: 12px;
- }
-
- .filter {
- flex-direction: column;
- }
-
- .filter input, .filter select {
- width: 100%;
- }
-
- .dataTable {
- font-size: 12px;
- }
-
- .dataTable th, .dataTable td {
- padding: 8px;
- }
-
- .paginationContainer {
- flex-direction: column;
- gap: 12px;
- }
-
- .formBody {
- grid-template-columns: 1fr;
- }
-}
diff --git a/src/tokenUtility.js b/src/tokenUtility.js
deleted file mode 100644
index 83a339ec..00000000
--- a/src/tokenUtility.js
+++ /dev/null
@@ -1,50 +0,0 @@
-const tokenKey = "token";
-
-export const setUser = (user) => {
- if (user && user.token) {
- window.localStorage.setItem(tokenKey, JSON.stringify(user));
- }
- else {
- window.localStorage.removeItem(tokenKey);
- }
-}
-
-export const getAccessToken = () => {
- let result = window.localStorage.getItem(tokenKey);
- if (!result) {
- return "";
- }
- const user = JSON.parse(result);
- return `Bearer ${user.token}`;
-}
-
-export const getRawToken = () => {
- let result = window.localStorage.getItem(tokenKey);
- if (!result) {
- return null;
- }
- return JSON.parse(result).token || null;
-}
-
-export const getUser = () => {
- let result = window.localStorage.getItem(tokenKey);
- if (!result) {
- return null;
- }
- return JSON.parse(result);
-}
-
-export const removeToken = () => {
- window.localStorage.removeItem(tokenKey);
-}
-
-export const isAuthenticated = () => {
- const token = getRawToken();
- if (!token) return false;
- try {
- const payload = JSON.parse(atob(token.split(".")[1]));
- return new Date(payload.exp * 1000) > new Date();
- } catch (e) {
- return false;
- }
-}