chore: remove legacy CRA pages, Redux store and JS hooks

This commit is contained in:
Arthur Bassi 2026-06-12 11:57:07 -03:00
parent 7af4a56d04
commit fac35319e5
161 changed files with 0 additions and 23458 deletions

View file

@ -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 : <Navigate to="/login" replace />;
}
export default function App() {
const [navCollapsed, setNavCollapsed] = useState(false);
return (
<BrowserRouter>
<Routes>
{/* Public Routes */}
<Route path="/login" element={<LoginPage />} />
{/* Vendor Portal — public, token-authenticated via URL */}
<Route path="/v/:token" element={<VendorPortalLayout />}>
<Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={<VendorPortalDashboard />} />
<Route path="pos" element={<VendorPortalPos />} />
<Route path="dispatch/:id" element={<VendorPortalDispatchDetail />} />
</Route>
{/* Protected Routes */}
<Route
path="/*"
element={
<ProtectedRoute>
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
<Sidebar />
<div className="mainArea">
<Topbar onToggleNav={() => setNavCollapsed(v => !v)} />
<div className="contentWrapper">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<Dashboard />} />
{/* CONTACTS */}
<Route path="/contacts" element={<ContactsList />} />
<Route path="/contacts/new" element={<ContactFormPage />} />
<Route path="/contacts/edit/:id" element={<ContactFormPage />} />
{/* ACCOUNTS */}
<Route path="/accounts" element={<AccountList />} />
<Route path="/accounts/new" element={<AccountFormPage />} />
<Route path="/accounts/edit/:id" element={<AccountFormPage />} />
{/* EMPLOYEES */}
<Route path="/employees" element={<EmployeeList />} />
<Route path="/employees/new" element={<EmployeeFormPage />} />
<Route path="/employees/edit/:id" element={<EmployeeFormPage />} />
{/* LOCATIONS */}
<Route path="/locations" element={<LocationsList />} />
<Route path="/locations/new" element={<LocationFormPage />} />
<Route path="/locations/edit/:id" element={<LocationFormPage />} />
{/* WORK ORDERS */}
<Route path="/workorders" element={<WorkOrdersList />} />
<Route path="/workorders/new" element={<WorkOrderFormPage />} />
<Route path="/workorders/:id" element={<WorkOrderViewPage />} />
<Route path="/workorders/edit/:id" element={<WorkOrderFormPage />} />
{/* FOLLOW UPS */}
<Route path="/followups" element={<FollowUpList />} />
<Route path="/followups/new" element={<FollowUpFormPage />} />
<Route path="/followups/edit/:id" element={<FollowUpFormPage />} />
{/* PM SCHEDULES */}
<Route path="/pmschedules" element={<PMScheduleList />} />
<Route path="/pmschedules/new" element={<PMScheduleFormPage />} />
<Route path="/pmschedules/edit/:id" element={<PMScheduleFormPage />} />
{/* ASSETS */}
<Route path="/assets" element={<AssetsList />} />
<Route path="/assets/new" element={<AssetFormPage />} />
<Route path="/assets/edit/:id" element={<AssetFormPage />} />
{/* VENDORS */}
<Route path="/vendors" element={<VendorsList />} />
<Route path="/vendors/new" element={<VendorFormPage />} />
<Route path="/vendors/edit/:id" element={<VendorFormPage />} />
{/* CALENDAR */}
<Route path="/calendar" element={<CalendarPage />} />
<Route path="/calendar/new" element={<EventFormPage />} />
<Route path="/calendar/edit/:id" element={<EventFormPage />} />
{/* UPLIFTS */}
<Route path="/uplifts" element={<UpliftQueuePage />} />
{/* VENDOR POs */}
<Route path="/vendor-pos" element={<VendorPoList />} />
<Route path="/vendor-pos/:id" element={<VendorPoViewPage />} />
{/* SETTINGS */}
<Route path="/settings/dropdowns" element={<DropdownOptionsPage />} />
<Route path="/settings/task-templates" element={<TaskTemplatesPage />} />
</Routes>
</div>
</div>
</div>
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
}

View file

@ -1,8 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View file

@ -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 (
<ThemeProvider theme={THEME}>
<TopNav handleDrawerOpen={handleDrawerOpen}/>
<LeftNav handleDrawerClose= {handleDrawerClose} open={open}/>
<Main open={open} maxWidth="lg">{props.children}</Main>
</ThemeProvider>
)};
export default Layout;

View file

@ -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);
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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 (
<div className="actionBar">
<ul className="actionBarNav">
{/* Settings/Filter Button */}
<li>
<a href="#" className="actionBarLink" title="Settings">
<i className="fa fa-filter"></i>
</a>
</li>
{/* Workspace Selector */}
<li className={`actionBarDropdown ${workspaceOpen ? 'open' : ''}`}>
<a
href="#"
className="actionBarLink"
onClick={toggleWorkspace}
title="Change your workspace"
>
<i className="fa fa-inbox"></i>
<span className="actionBarText">{selectedWorkspace}</span>
<i className="fa fa-angle-down"></i>
</a>
{workspaceOpen && (
<ul className="actionBarDropdownMenu">
<li><a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("All"); }}>All</a></li>
<li><a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("Everyone else"); }}>Everyone else</a></li>
<li><a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("Only Me"); }}>Only Me</a></li>
<li className="divider"></li>
<li><a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("Custom"); }}>Specific people...</a></li>
</ul>
)}
</li>
{/* Module Actions (New, Edit, Import/Export) */}
<li className={`actionBarDropdown ${moduleOpen ? 'open' : ''}`}>
<a
href="#"
className="actionBarLink"
onClick={toggleModule}
title={moduleName}
>
<i className={`fa ${moduleIcon}`}></i>
<span className="actionBarText">{moduleName}</span>
<i className="fa fa-angle-down"></i>
</a>
{moduleOpen && (
<ul className="actionBarDropdownMenu">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); setModuleOpen(false); onNew && onNew(); }}>
New
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); setModuleOpen(false); onBulkEdit && onBulkEdit(); }}>
Edit <span className="countLabel">({selectedCount})</span>
</a>
</li>
<li className="divider"></li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); }}>
Import/Export <i className="fa fa-angle-right pull-right"></i>
</a>
</li>
</ul>
)}
</li>
{/* Search & Report */}
<li className={`actionBarDropdown ${searchOpen ? 'open' : ''}`}>
<a
href="#"
className="actionBarLink"
onClick={toggleSearch}
title="Search & Report"
>
<i className="fa fa-search"></i>
<span className="actionBarText">Search</span>
<i className="fa fa-angle-down"></i>
</a>
{searchOpen && (
<ul className="actionBarDropdownMenu">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); setSearchOpen(false); onSearch && onSearch('smart'); }}>
Smart Search
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); setSearchOpen(false); onSearch && onSearch('open'); }}>
Open
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); }}>
Recent <i className="fa fa-angle-right pull-right"></i>
</a>
</li>
</ul>
)}
</li>
</ul>
</div>
);
}

View file

@ -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 (
<div style={styles.overlay}>
<div style={styles.modal}>
<div style={styles.header}>
<h3 style={styles.title}>{title}</h3>
<button
onClick={onClose}
style={styles.closeBtn}
disabled={loading}
>
✕
</button>
</div>
<div style={styles.body}>
<p style={styles.message}>{message}</p>
</div>
<div style={styles.actions}>
<button
onClick={onClose}
style={styles.cancelBtn}
disabled={loading}
>
Cancel
</button>
<button
onClick={onConfirm}
style={styles.deleteBtn}
disabled={loading}
>
{loading ? "Deleting..." : "Delete"}
</button>
</div>
</div>
</div>
);
}
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",
},
};

View file

@ -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 (
<>
<button ref={btnRef} className="paginationBtn colConfigBtn" onClick={handleOpen} title="Configure columns">
⚙
</button>
{open && ReactDOM.createPortal(
<div ref={panelRef} className="colConfigPanel" style={{ top: menuPos.top, right: menuPos.right }}>
<div className="colConfigTitle">Columns</div>
<div className="colConfigList">
{ordered.map((col, idx) => (
<div
key={col.key}
className={`colConfigItem ${dragIdx === idx ? "dragging" : ""}`}
draggable
onDragStart={() => handleDragStart(idx)}
onDragOver={handleDragOver}
onDrop={() => handleDrop(idx)}
>
<span className="colConfigDragHandle">⠿</span>
<label className="colConfigLabel">
<input
type="checkbox"
checked={!hiddenColumns.includes(col.key)}
onChange={() => onToggle(col.key)}
/>
{col.label}
</label>
</div>
))}
</div>
</div>,
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 <div className="loading">Loading...</div>;
if (error) return <div className="error">{error}</div>;
const gridTemplate = colWidths
? ["40px", ...visibleColumns.map((col) => `${colWidths[col.key] || 100}px`), "80px"].join(" ")
: ["40px", ...visibleColumns.map(() => "1fr"), "80px"].join(" ");
const paginationBar = (
<div className="paginationBar">
<div className="paginationPerPage">
<span className="paginationLabel">Show</span>
<div className="paginationSizeGroup">
{[12, 24, 48, 96].map((size) => (
<button
key={size}
className={`paginationSizeBtn ${pageSize === size ? "active" : ""}`}
onClick={() => { setPageSize(size); setPage(1); }}
>
{size}
</button>
))}
</div>
<span className="paginationLabel">per page</span>
</div>
<div className="paginationNav">
<span className="paginationInfo">
{Math.min((page - 1) * pageSize + 1, totalCount)}–{Math.min(page * pageSize, totalCount)} of {totalCount}
</span>
<div className="paginationButtons">
<button className="paginationBtn" onClick={() => setPage(1)} disabled={page === 1} title="First">«</button>
<button className="paginationBtn" onClick={() => setPage(page - 1)} disabled={page === 1} title="Previous">‹</button>
<span className="paginationPage">{page}</span>
<button className="paginationBtn" onClick={() => setPage(page + 1)} disabled={page >= totalPages} title="Next">›</button>
<button className="paginationBtn" onClick={() => setPage(totalPages)} disabled={page >= totalPages} title="Last">»</button>
</div>
<ColumnConfigPanel
columns={columns}
columnOrder={columnOrder}
hiddenColumns={hiddenColumns}
onReorder={(order) => { setColumnOrder(order); setColWidths(null); }}
onToggle={handleToggleColumn}
/>
</div>
</div>
);
return (
<div className="tableContainer">
{paginationBar}
<div className="tableResponsive">
<div className="gridTable" ref={gridRef} style={{ gridTemplateColumns: gridTemplate }}>
<div className="gridCell gridHeader">
<input type="checkbox" />
</div>
{visibleColumns.map((col) => (
<div
key={col.key}
data-col={col.key}
className={`gridCell gridHeader gridSortable ${sortBy === col.key ? "sorted" : ""}`}
onClick={() => handleSort(col.key)}
>
{col.label}
{sortBy === col.key && <span className="sortArrow">{sortDir === "asc" ? " ▲" : " ▼"}</span>}
<span
className="colResizeHandle"
onMouseDown={(e) => { e.stopPropagation(); handleMouseDown(e, col.key); }}
/>
</div>
))}
<div className="gridCell gridHeader">Actions</div>
{rows.map((row, idx) => (
<React.Fragment key={row.id || idx}>
<div className="gridCell gridBody">
<input type="checkbox" />
</div>
{visibleColumns.map((col) => (
<div key={col.key} className="gridCell gridBody">
{col.render ? col.render(row[col.key], row) : row[col.key]}
</div>
))}
<div className="gridCell gridBody">
<div className="actionButtons">
{onEdit && (
<button className="btnIcon edit" title="Edit" onClick={() => onEdit(row)}>
✎
</button>
)}
{onDelete && (
<button className="btnIcon delete" title="Delete" onClick={() => onDelete(row)}>
🗑
</button>
)}
</div>
</div>
</React.Fragment>
))}
</div>
</div>
</div>
);
};
export default SharedTable;

View file

@ -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: <FaTachometerAlt />, path: "/" },
{ label: "Calendar", icon: <FaCalendarAlt />, path: "/calendar" },
{ label: "Reports", icon: <FaChartBar />, path: "/reports" },
],
},
{
title: "Operations",
items: [
{ label: "Work Orders", icon: <FaTools />, path: "/workorders" },
{ label: "Uplifts", icon: <FaDollarSign />, path: "/uplifts" },
{ label: "Follow Ups", icon: <FaBell />, path: "/followups" },
{ label: "PM Schedules", icon: <FaClipboardList />, path: "/pmschedules" },
],
},
{
title: "Vendors",
items: [
{ label: "Vendors", icon: <FaTruck />, path: "/vendors" },
{ label: "Vendor POs", icon: <FaFileInvoiceDollar />, path: "/vendor-pos" },
],
},
{
title: "Customers & Sites",
items: [
{ label: "Accounts", icon: <FaMoneyCheckAlt />, path: "/accounts" },
{ label: "Contacts", icon: <FaAddressBook />, path: "/contacts" },
{ label: "Locations", icon: <FaMapMarkerAlt />, path: "/locations" },
{ label: "Assets", icon: <FaCubes />, path: "/assets" },
],
},
{
title: "Internal",
items: [
{ label: "Employees", icon: <FaUserTie />, path: "/employees" },
{ label: "Documents", icon: <FaFileAlt />, path: "/documents" },
],
},
{
title: "System",
items: [
{ label: "Settings", icon: <FaCog />, path: "/settings/dropdowns" },
],
},
];
export default function Sidebar() {
return (
<aside className="sidebar">
<div className="logoRow">
<img
src="/logo.png"
alt="SeaHaven Industries"
className="sidebarLogo"
/>
</div>
<ul className="navList">
{navGroups.map((group, idx) => (
<li key={group.title} className="navGroup">
<div
className={`navGroupTitle ${idx === 0 ? "navGroupTitleFirst" : ""}`}
>
{group.title}
</div>
<ul className="navGroupList">
{group.items.map(({ label, icon, path }) => (
<li key={path}>
<NavLink
to={path}
end={path === "/"}
className={({ isActive }) =>
`navItem ${isActive ? "active" : ""}`
}
>
<span className="glyph">{icon}</span>
<span className="navLabel">{label}</span>
</NavLink>
</li>
))}
</ul>
</li>
))}
</ul>
</aside>
);
}

View file

@ -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;
}

View file

@ -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(
<div className="sigOverlay" onClick={onClose}>
<div className="sigModal" onClick={(e) => e.stopPropagation()}>
<div className="sigHeader">
<h3>{title || "Capture Signature"}</h3>
<button className="woDispatchClose" onClick={onClose}>&times;</button>
</div>
<div className="sigBody">
<div className="sigNameField">
<label>Full Name</label>
<input
className="form-control"
value={signerName}
onChange={(e) => setSignerName(e.target.value)}
placeholder="Enter signer's full name"
/>
</div>
<div className="sigModeTabs">
<button className={`sigModeTab ${mode === "draw" ? "active" : ""}`} onClick={() => setMode("draw")}>Draw</button>
<button className={`sigModeTab ${mode === "type" ? "active" : ""}`} onClick={() => setMode("type")}>Type</button>
</div>
{mode === "draw" ? (
<div className="sigCanvasWrap">
<canvas
ref={canvasRef}
width={460}
height={180}
className="sigCanvas"
onMouseDown={startDraw}
onMouseMove={draw}
onMouseUp={stopDraw}
onMouseLeave={stopDraw}
onTouchStart={startDraw}
onTouchMove={draw}
onTouchEnd={stopDraw}
/>
<button className="sigClearBtn" onClick={clearCanvas}>Clear</button>
</div>
) : (
<div className="sigTypeWrap">
<input
className="form-control sigTypeInput"
value={typedName}
onChange={(e) => setTypedName(e.target.value)}
placeholder="Type your name"
/>
{typedName && <div className="sigTypePreview">{typedName}</div>}
</div>
)}
</div>
<div className="sigFooter">
<button className="btn btn-default" onClick={onClose}>Cancel</button>
<button className="btn btn-primary" onClick={handleSave} disabled={!signerName.trim() || (mode === "type" && !typedName.trim())}>Save Signature</button>
</div>
</div>
</div>,
document.body
);
}

View file

@ -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 (
<>
<button ref={btnRef} className="topbarIconBtn" title="Notifications" onClick={handleOpen}>
<i className="fa fa-bell-o"></i>
</button>
{open && ReactDOM.createPortal(
<div ref={menuRef} className="notifDropdown" style={{ top: menuPos.top, right: menuPos.right }}>
<div className="notifHeader">Notifications</div>
<div className="notifEmpty">
<i className="fa fa-check-circle notifEmptyIcon"></i>
<span>No new notifications</span>
</div>
</div>,
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 (
<>
<button ref={btnRef} className="topbarIconBtn" onClick={handleOpen} title={currentUser?.name || "User"}>
<i className="fa fa-user-o"></i>
</button>
{open && ReactDOM.createPortal(
<div ref={menuRef} className="userDropdown" style={{ top: menuPos.top, right: menuPos.right }}>
<div className="userHeader">
<i className="fa fa-user-circle"></i>
<div className="userInfo">
<div className="userName">{currentUser?.name || "User"}</div>
<div className="userEmail">{currentUser?.email || ""}</div>
</div>
</div>
<div className="userDivider"></div>
<button
className="userMenuItem"
onClick={() => handleItemClick(() => onNavigate("/settings"))}
>
<i className="fa fa-cog"></i>
<span>Settings</span>
</button>
<button
className="userMenuItem"
onClick={() => handleItemClick(onLogout)}
>
<i className="fa fa-sign-out"></i>
<span>Logout</span>
</button>
</div>,
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 (
<header className="topbar">
<div className="topLeft">
<button
className="hamburger"
onClick={onToggleNav}
aria-label="Toggle navigation"
type="button"
>
☰
</button>
</div>
<div className="topRight">
<NotificationDropdown />
<div className="topbarDivider" />
<UserDropdown currentUser={user} onLogout={handleLogout} onNavigate={navigate} />
</div>
</header>
);
}

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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);
},
});
}

View file

@ -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);
},
});
}

View file

@ -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);
},
});
}

View file

@ -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,
};
}

View file

@ -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;
}

View file

@ -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,
};
}

View file

@ -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]);
}

View file

@ -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;
}

View file

@ -1 +0,0 @@
<div id="root"></div>

View file

@ -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(
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<App />
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
</Provider>
);

View file

@ -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;

View file

@ -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),
};

View file

@ -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;

View file

@ -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;
}

View file

@ -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 (
<section className="dashboardPage">
<div className="pageTitle">Dashboard</div>
<div className="kpiContainer">
{kpis.map((kpi, idx) => (
<div key={idx} className={`kpiCard bg-${kpi.color}`} onClick={kpi.onClick}>
<div className="kpiIcon">
<i className={`fa ${kpi.icon}`}></i>
</div>
<div className="kpiContent">
<h5>{kpi.title}</h5>
{kpi.stat !== undefined && <div className="kpiStat">{kpi.stat?.toLocaleString?.() ?? kpi.stat}</div>}
{kpi.desc && <p>{kpi.desc}</p>}
</div>
</div>
))}
</div>
<div className="card mt-4">
<div className="cardHeader">
<h3>Priority Analysis</h3>
<p>Age of open Work Orders by Priority</p>
</div>
<div className="tableWrapper">
<table className="dashTable">
<thead>
<tr>
<th>Priority</th>
<th className="text-right">Total</th>
<th className="text-right">0-14 Days</th>
<th className="text-right">15-29 Days</th>
<th className="text-right">30-59 Days</th>
<th className="text-right">60+ Days</th>
</tr>
</thead>
<tbody>
{[5, 4, 3, 2, 1].map((p) => (
<tr key={p}>
<td>{p}</td>
<td className="text-right">—</td>
<td className="text-right">—</td>
<td className="text-right">—</td>
<td className="text-right">—</td>
<td className="text-right">—</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="card mt-4">
<div className="cardHeader">
<h3>Invoice Analysis</h3>
</div>
<div className="tableWrapper">
<table className="dashTable">
<thead>
<tr>
<th>Status</th>
<th className="text-right">Count</th>
<th className="text-right">Amount</th>
</tr>
</thead>
<tbody>
{["Draft", "Challenged", "Pending Approval", "Approved", "Paid"].map((s) => (
<tr key={s}>
<td>{s}</td>
<td className="text-right">0</td>
<td className="text-right">$0.00</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
);
};
export default Dashboard;

View file

@ -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}`);
};

View file

@ -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);
};

View file

@ -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%;
}
}

View file

@ -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 (
<div className="pmScheduleFormPage">
<div className="panel panel-default panel-connected">
<div className="panel-body">
<ol className="breadcrumb breadcrumb-alt">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); navigate("/pmschedules"); }}>
PM Schedules
</a>
</li>
<li className="active">{isEdit ? "Edit" : "Create"}</li>
</ol>
<div className="container">
<form onSubmit={handleSave}>
{/* Page Header */}
<div className="form-group">
<div className="header">
<h3 className="heading thin">
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
</span>
<span>{isEdit ? "Edit a PM Schedule" : "Create a PM Schedule"}</span>
</h3>
</div>
</div>
{/* Asset Information Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Asset Information</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="asset">Asset ID</label>
<input
className="form-control"
type="text"
id="asset"
name="asset"
value={formData.asset}
onChange={handleChange}
placeholder="e.g., HVAC-001"
required
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="assetName">Asset Name</label>
<input
className="form-control"
type="text"
id="assetName"
name="assetName"
value={formData.assetName}
onChange={handleChange}
placeholder="Asset description"
required
/>
</div>
</div>
</div>
</div>
{/* Schedule Information Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Schedule Information</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-4">
<div className="form-group">
<label className="required-field" htmlFor="frequency">Frequency</label>
<select
className="form-control"
id="frequency"
name="frequency"
value={formData.frequency}
onChange={handleChange}
required
>
<option value="Daily">Daily</option>
<option value="Weekly">Weekly</option>
<option value="Monthly">Monthly</option>
<option value="Quarterly">Quarterly</option>
<option value="Semi-Annual">Semi-Annual</option>
<option value="Annual">Annual</option>
</select>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label className="required-field" htmlFor="lastServiceDate">Last Service Date</label>
<input
className="form-control"
type="date"
id="lastServiceDate"
name="lastServiceDate"
value={formData.lastServiceDate}
onChange={handleChange}
required
/>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label className="required-field" htmlFor="nextServiceDate">Next Service Date</label>
<input
className="form-control"
type="date"
id="nextServiceDate"
name="nextServiceDate"
value={formData.nextServiceDate}
onChange={handleChange}
required
/>
</div>
</div>
</div>
</div>
{/* Assignment Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Assignment</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="assignedTo">Assigned To</label>
<select
className="form-control"
id="assignedTo"
name="assignedTo"
value={formData.assignedTo}
onChange={handleChange}
>
<option value="">Select Employee</option>
<option value="John Anderson">John Anderson</option>
<option value="Sarah Martinez">Sarah Martinez</option>
<option value="Mike Johnson">Mike Johnson</option>
</select>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="status">Status</label>
<select
className="form-control"
id="status"
name="status"
value={formData.status}
onChange={handleChange}
required
>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="Suspended">Suspended</option>
</select>
</div>
</div>
</div>
</div>
{/* Notes Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Notes</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-12">
<div className="form-group">
<label htmlFor="notes">Notes</label>
<textarea
className="form-control"
id="notes"
name="notes"
value={formData.notes}
onChange={handleChange}
rows="4"
placeholder="PM schedule notes"
/>
</div>
</div>
</div>
</div>
{/* Form Actions */}
<div className="form-actions">
<button
type="button"
className="btn btn-default"
onClick={handleCancel}
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
disabled={loading}
>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<>
<ActionBar
moduleName="PM Schedules"
moduleIcon="fa-calendar-check-o"
onNew={() => navigate("/pmschedules/new")}
onBulkEdit={() => console.log("Bulk edit")}
selectedCount={0}
/>
<section className="listPage">
<div className="breadcrumb">
<span className="breadcrumbItem active">PM Schedules</span>
</div>
<div className="searchSection">
<div className="searchHeader">
<h3 className="searchHeading">
<span className="fa-stack iconStack">
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className="fa fa-calendar-check-o fa-stack-1x fa-inverse"></i>
</span>
<span>Search for PM Schedules</span>
</h3>
<span className="searchSubheading">Use the settings menu to adjust how to filter the list, and which columns to display.</span>
</div>
<div className="searchControls">
<div className="searchLeft">
<div className="searchInputGroup">
<select className="searchDropdown" defaultValue="Asset">
<option>Asset</option>
<option>Frequency</option>
<option>Status</option>
</select>
<input
className="searchInput"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Enter search criteria here..."
/>
<button className="searchButton">Search!</button>
</div>
<div className="viewToggle">
<button
className={`viewBtn ${viewMode === "list" ? "active" : ""}`}
title="List view"
onClick={() => setViewMode("list")}
>
<i className="fa fa-navicon"></i>
</button>
<button
className={`viewBtn ${viewMode === "card" ? "active" : ""}`}
title="Card view"
onClick={() => setViewMode("card")}
>
<i className="fa fa-th-large"></i>
</button>
</div>
</div>
<div className="searchRight">
<div className="paginationTop">
<button
className="pageBtn"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
«
</button>
<span className="pageNumber">{page}</span>
<button
className="pageBtn"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
»
</button>
</div>
</div>
</div>
</div>
<Table
loading={isLoading}
error={error?.message}
rows={rows}
onEdit={(item) => navigate(`/pmschedules/edit/${item.id}`)}
onDelete={handleDeleteClick}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
totalPages={totalPages}
/>
<DeleteModal
isOpen={deleteModal.isOpen}
onClose={deleteModal.close}
onConfirm={handleDeleteConfirm}
title="Delete PM Schedule"
message={`Are you sure you want to delete "${deleteModal.data?.name}"? This action cannot be undone.`}
loading={deleteMutation.isLoading}
/>
</section>
</>
);
}

View file

@ -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) => <strong><a href="#" onClick={(e) => { e.preventDefault(); onEdit && onEdit(row); }}>{val}</a></strong> },
{ key: "frequency", label: "Frequency" },
{ key: "lastDate", label: "Last Date" },
{ key: "nextDate", label: "Next Date" },
{ key: "status", label: "Status" },
{ key: "assignedTo", label: "Assigned To" },
];
return (
<SharedTable rows={rows} columns={columns} onEdit={onEdit} onDelete={onDelete} page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize} totalCount={totalCount} loading={loading} error={error} />
);
}

View file

@ -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)),
};
};

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}
};

View file

@ -1,5 +0,0 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const editAccount = (data) =>
post(`${API_URL}/Account/EditAccount`, data);

View file

@ -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 (
<div className="contactFormWrapper">
<div className="formCard">
<div className="formHeader">
<h2>{isEdit ? "Edit Account" : "Create an Account"}</h2>
</div>
<form className="contactForm" onSubmit={handleSubmit} noValidate>
{/* GENERAL DETAILS */}
<section className="section">
<h5 className="sectionTitle">General Details</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">
Account Name <span className="required">*</span>
</label>
<input
name="AccountName"
className="formControl"
placeholder="Enter account name"
value={account.AccountName || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Account Number</label>
<input
name="AccountNumber"
className="formControl"
placeholder="Enter account number"
value={account.AccountNumber || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Industry</label>
<input
name="Industry"
className="formControl"
placeholder="Enter industry"
value={account.Industry || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Website</label>
<input
name="Website"
className="formControl"
placeholder="https://example.com"
value={account.Website || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup fullWidth">
<label className="formLabel">Notes</label>
<input
name="Notes"
className="formControl"
placeholder="Enter account notes or description"
value={account.Notes || ""}
onChange={handleChange}
/>
</div>
</div>
</section>
<hr />
{/* ADDRESS */}
<section className="section">
<h5 className="sectionTitle">Address</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">Address 1</label>
<input
name="Address1"
className="formControl"
placeholder="Enter street address"
value={account.Address1 || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Address 2</label>
<input
name="Address2"
className="formControl"
placeholder="Suite/Apt/Unit"
value={account.Address2 || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">City</label>
<input
name="City"
className="formControl"
placeholder="Enter city"
value={account.City || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">State / Province</label>
<input
name="State"
className="formControl"
placeholder="State"
value={account.State || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Zip/Postal Code</label>
<input
name="Zip"
className="formControl"
placeholder="Zip code"
value={account.Zip || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Country</label>
<select
name="Country"
className="formControl"
value={account.Country || "US"}
onChange={handleChange}
>
<option value="">Select Country</option>
<option value="US">United States</option>
</select>
</div>
</div>
</section>
<hr />
{/* SUBMIT */}
<div className="formActions">
<div className="formNote">
All fields marked with <span className="required">*</span> are required.
</div>
<button className="btnPrimary" type="submit" disabled={loading}>
{loading ? "Saving..." : isEdit ? "Update Account" : "Create Account"}
</button>
</div>
</form>
</div>
</div>
);
};
export default AccountFormPage;

View file

@ -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 (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<>
<ActionBar
moduleName="Accounts"
moduleIcon="fa-building"
onNew={() => navigate("/accounts/new")}
onBulkEdit={() => console.log("Bulk edit")}
selectedCount={0}
/>
<section className="listPage">
<div className="breadcrumb">
<span className="breadcrumbItem active">Accounts</span>
</div>
<div className="searchSection">
<div className="searchHeader">
<h3 className="searchHeading">
<span className="fa-stack iconStack">
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className="fa fa-building fa-stack-1x fa-inverse"></i>
</span>
<span>Search for Accounts</span>
</h3>
<span className="searchSubheading">Use the settings menu to adjust how to filter the list, and which columns to display.</span>
</div>
<div className="searchControls">
<div className="searchLeft">
<div className="searchInputGroup">
<select className="searchDropdown" defaultValue="Name">
<option>Name</option>
<option>Account Owner</option>
<option>Email</option>
<option>Phone</option>
</select>
<input
className="searchInput"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Enter search criteria here..."
/>
<button className="searchButton">Search!</button>
</div>
<div className="viewToggle">
<button
className={`viewBtn ${viewMode === "list" ? "active" : ""}`}
title="List view"
onClick={() => setViewMode("list")}
>
<i className="fa fa-navicon"></i>
</button>
<button
className={`viewBtn ${viewMode === "card" ? "active" : ""}`}
title="Card view"
onClick={() => setViewMode("card")}
>
<i className="fa fa-th-large"></i>
</button>
</div>
</div>
<div className="searchRight">
<div className="paginationTop">
<button
className="pageBtn"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
«
</button>
<span className="pageNumber">{page}</span>
<button
className="pageBtn"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
»
</button>
</div>
</div>
</div>
</div>
<Table loading={loading} error={err} rows={rows}
onEdit={(item) => navigate(`/accounts/edit/${item.id}`)}
onDelete={handleDeleteClick}
page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize}
totalCount={totalCount} totalPages={totalPages} />
<DeleteModal
isOpen={deleteModal.isOpen}
onClose={() => 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}
/>
</section>
</>
);
}

View file

@ -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) => <strong><a href="#" onClick={(e) => { e.preventDefault(); onEdit && onEdit(row); }}>{val}</a></strong> },
{ key: "email", label: "Email" },
{ key: "phone", label: "Phone" },
{ key: "owner", label: "Owner" },
{ key: "status", label: "Status" },
];
return (
<SharedTable
rows={rows}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
loading={loading}
error={error}
/>
);
}

View file

@ -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}`);

View file

@ -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;
}

View file

@ -1,5 +0,0 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const addAccount = (data) =>
post(`${API_URL}/Account/AddAccount`, data);

View file

@ -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 (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
<Button
startIcon={<ArrowBackIcon />}
onClick={() => navigate("/assets")}
sx={{ mb: 2 }}
>
Back to Assets
</Button>
<Paper sx={{ p: 3 }}>
<Typography variant="h4" component="h1" gutterBottom>
{isEdit ? "Edit Asset" : "Create New Asset"}
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
required
label="Asset Code"
name="assetCode"
value={formData.assetCode}
onChange={handleChange}
placeholder="Enter asset code"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
required
label="Name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Enter asset name"
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
multiline
rows={3}
label="Description"
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Enter asset description"
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>Account</InputLabel>
<Select
name="accountId"
value={formData.accountId}
onChange={handleChange}
label="Account"
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{accounts.map((account) => (
<MenuItem key={account.id} value={account.id}>
{account.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>Location</InputLabel>
<Select
name="locationId"
value={formData.locationId}
onChange={handleChange}
label="Location"
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{locations.map((location) => (
<MenuItem key={location.id} value={location.id}>
{location.title} - {location.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={
<Switch
checked={formData.isActive}
onChange={handleChange}
name="isActive"
color="primary"
/>
}
label="Active"
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: "flex", gap: 2, justifyContent: "flex-end" }}>
<Button
variant="outlined"
onClick={() => navigate("/assets")}
disabled={loading}
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
color="primary"
startIcon={<SaveIcon />}
disabled={loading}
>
{loading ? "Saving..." : isEdit ? "Update Asset" : "Create Asset"}
</Button>
</Box>
</Grid>
</Grid>
</Box>
</Paper>
</Box>
);
};
export default AssetFormPage;

View file

@ -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 (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete the asset <strong>{itemName}</strong>?
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="inherit">
Cancel
</Button>
<Button onClick={onConfirm} color="error" variant="contained">
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default DeleteDialog;

View file

@ -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 (
<Box sx={{ p: 3 }}>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 3 }}>
<Typography variant="h4" component="h1">
Assets
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={() => navigate("/assets/new")}
>
New Asset
</Button>
</Box>
<Box component="form" onSubmit={handleSearchSubmit} sx={{ mb: 3 }}>
<TextField
fullWidth
placeholder="Search assets by name, code, or description..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 4 }}>
<CircularProgress />
</Box>
) : (
<>
<AssetsTable items={items} onDelete={handleDeleteClick} />
{totalPages > 1 && (
<Box sx={{ display: "flex", justifyContent: "center", mt: 3 }}>
<Pagination
count={totalPages}
page={page}
onChange={(e, value) => handlePageChange(value)}
color="primary"
/>
</Box>
)}
</>
)}
<DeleteDialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
onConfirm={handleDeleteConfirm}
itemName={itemToDelete?.name || ""}
/>
</Box>
);
};
export default AssetsList;

View file

@ -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 (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell><strong>Asset Code</strong></TableCell>
<TableCell><strong>Name</strong></TableCell>
<TableCell><strong>Description</strong></TableCell>
<TableCell><strong>Account</strong></TableCell>
<TableCell><strong>Location</strong></TableCell>
<TableCell><strong>Status</strong></TableCell>
<TableCell align="right"><strong>Actions</strong></TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center">
No assets found
</TableCell>
</TableRow>
) : (
items.map((item) => (
<TableRow key={item.id} hover>
<TableCell>{item.assetCode}</TableCell>
<TableCell>{item.name}</TableCell>
<TableCell>{item.description || "-"}</TableCell>
<TableCell>{item.accountName || "-"}</TableCell>
<TableCell>{item.locationName || "-"}</TableCell>
<TableCell>
<Chip
label={item.isActive ? "Active" : "Inactive"}
color={item.isActive ? "success" : "default"}
size="small"
/>
</TableCell>
<TableCell align="right">
<IconButton
color="primary"
size="small"
onClick={() => handleEdit(item.id)}
title="Edit"
>
<EditIcon />
</IconButton>
<IconButton
color="error"
size="small"
onClick={() => onDelete(item.id)}
title="Delete"
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
);
};
export default AssetsTable;

View file

@ -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}`);

View file

@ -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,
};
};

View file

@ -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;
}

View file

@ -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 (
<div
className={styles.page}
style={{
backgroundImage: `url(${process.env.PUBLIC_URL}/BrandingImage.png)`
}}
>
<div className={styles.wrapper}>
<div className={styles.container}>
<div className={styles.panel}>
{/* Logo Area */}
<div
className={styles.branding}
style={{
backgroundImage: `url(${process.env.PUBLIC_URL}/logo.png)`
}}
></div>
{/* Header */}
<div className={styles.header}>
<h5>User Login</h5>
</div>
{/* Login Form */}
<div className={styles.body}>
<form onSubmit={handleSubmit} className={styles.form} autoComplete="off">
<div className={styles.formGroup}>
<input
type="email"
className={styles.formControl}
id="username"
name="username"
placeholder="Username"
value={formData.username}
onChange={handleChange}
required
autoComplete="off"
/>
</div>
<div className={styles.formGroup}>
<input
type="password"
className={styles.formControl}
id="password"
name="password"
placeholder="Password"
value={formData.password}
onChange={handleChange}
required
autoComplete="new-password"
/>
</div>
{(error || authError) && <div className={styles.errorMessage}>{error || authError}</div>}
<div className={styles.buttonContent}>
<button
type="submit"
className={styles.button}
disabled={loading}
>
{loading ? "Logging in..." : "Log in"}
</button>
</div>
</form>
<p className={styles.forgotPassword}>
<a href="/forgot-password">Forgot your password?</a>
</p>
</div>
</div>
</div>
{/* Footer Links */}
<p className={styles.footer}>
<a href="https://www.officetrax.com" target="_blank" rel="noopener noreferrer">
www.officetrax.com
</a>
{" · "}
<a href="https://www.officetrax.com/Legal/TermsOfService" target="_blank" rel="noopener noreferrer">
Terms of Service
</a>
{" · "}
<a href="https://www.officetrax.com/Legal/PrivacyPolicy" target="_blank" rel="noopener noreferrer">
Privacy Policy
</a>
</p>
</div>
{/* Copyright */}
<p className={styles.copyright}>
©2026 <em>Core 7 Technologies Inc.</em> All rights reserved.
</p>
</div>
);
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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 (
<div className="calendarActionBar">
<ul className="calActionBarNav">
{/* Workspace Selector */}
<li className={`calActionBarDropdown ${workspaceOpen ? "open" : ""}`}>
<a
href="#"
className="calActionBarLink"
onClick={toggleWorkspace}
title="Change your workspace"
>
<i className="fa fa-inbox"></i>
<span className="calActionBarText">{selectedWorkspace}</span>
<i className="fa fa-angle-down"></i>
</a>
{workspaceOpen && (
<ul className="calActionBarDropdownMenu">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("All", "All"); }}>
All
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("Everyone else", "EveryoneElse"); }}>
Everyone else
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("Only Me", "OnlyMe"); }}>
Only Me
</a>
</li>
<li className="divider"></li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleWorkspaceSelect("Custom", "Custom"); }}>
Specific people...
</a>
</li>
</ul>
)}
</li>
{/* Event Type Filter */}
<li className={`calActionBarDropdown ${eventTypeOpen ? "open" : ""}`}>
<a
href="#"
className="calActionBarLink"
onClick={toggleEventType}
title="Filter events"
>
<i className="fa fa-filter"></i>
<span className="calActionBarText">{selectedEventType}</span>
<i className="fa fa-angle-down"></i>
</a>
{eventTypeOpen && (
<ul className="calActionBarDropdownMenu">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleEventTypeSelect("Events", "EventsOnly"); }}>
Events
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleEventTypeSelect("Follow Ups", "FollowUpsOnly"); }}>
Follow Ups
</a>
</li>
<li>
<a href="#" onClick={(e) => { e.preventDefault(); handleEventTypeSelect("Work Orders", "WorkOrdersOnly"); }}>
Work Orders
</a>
</li>
</ul>
)}
</li>
{/* New Event Button */}
<li>
<a
href="#"
className="calActionBarLink"
onClick={(e) => {
e.preventDefault();
navigate("/calendar/new");
}}
>
<i className="fa fa-plus"></i>
<span className="calActionBarText">New Event</span>
</a>
</li>
</ul>
{/* Right Side Navigation */}
<ul className="calActionBarNav calActionBarNavRight">
<li>
<a href="#" className="calActionBarLink" onClick={(e) => { e.preventDefault(); onTodayClick && onTodayClick(); }}>
Today
</a>
</li>
<li>
<a href="#" className="calActionBarLink" onClick={(e) => { e.preventDefault(); onPrevClick && onPrevClick(); }}>
<i className="fa fa-angle-double-left"></i>
</a>
</li>
<li>
<a href="#" className="calActionBarLink" onClick={(e) => { e.preventDefault(); onNextClick && onNextClick(); }}>
<i className="fa fa-angle-double-right"></i>
</a>
</li>
<li>
<a href="#" className="calActionBarLink" onClick={(e) => { e.preventDefault(); onViewChange && onViewChange("dayGridMonth"); }}>
Month
</a>
</li>
<li>
<a href="#" className="calActionBarLink" onClick={(e) => { e.preventDefault(); onViewChange && onViewChange("timeGridWeek"); }}>
Week
</a>
</li>
<li>
<a href="#" className="calActionBarLink" onClick={(e) => { e.preventDefault(); onViewChange && onViewChange("timeGridDay"); }}>
Day
</a>
</li>
</ul>
</div>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<div className="eventFormPage">
<div className="container">
<div className="panel panel-default panel-connected">
<div className="panel-body">
<ol className="breadcrumb breadcrumb-alt">
<li>
<a href="/calendar">Calendar</a>
</li>
<li className="active">{isEdit ? "Edit" : "Create"}</li>
</ol>
<form onSubmit={handleSave}>
<div className="form-group">
<div className="header">
<h3 className="heading thin">
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
</span>
<span>{isEdit ? "Edit Event" : "Create an Event"}</span>
</h3>
</div>
</div>
{/* General Details */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>General Details</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-5 col-md-6">
<div className="form-group">
<span className="input-counter">{titleCounter}</span>
<label className="required-field" htmlFor="title">
Title
</label>
<input
className="form-control"
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Enter Event title"
maxLength={100}
required
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="recordName">Related to...</label>
<div className="panel-placeholder text-center">
<div className="panel-body">
<p>
<strong>
{formData.recordName ? (
<span className="text-primary">{formData.recordName}</span>
) : (
<span className="text-muted">Click here to search records</span>
)}
</strong>
</p>
</div>
</div>
<input
type="hidden"
name="recordID"
value={formData.recordID || ""}
/>
</div>
</div>
</div>
<div className="form-group">
<label htmlFor="description">Description</label>
<textarea
className="form-control"
id="description"
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Enter details about the Event"
rows={3}
/>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">{locationCounter}</span>
<label htmlFor="location">Location</label>
<input
className="form-control"
type="text"
id="location"
name="location"
value={formData.location}
onChange={handleChange}
placeholder="Enter location"
maxLength={150}
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="reminderMinutes">Remind Attendees</label>
<select
className="form-control"
id="reminderMinutes"
name="reminderMinutes"
value={formData.reminderMinutes}
onChange={handleChange}
required
>
{reminderOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
<div className="form-group">
<label htmlFor="eventColor">Select a Calendar Color</label>
<div className="theme-picker">
<ul className="theme-colors">
<li className={formData.eventColor === "primary" ? "active" : ""}>
<a
href="#"
className="primary"
onClick={(e) => {
e.preventDefault();
handleColorSelect("primary");
}}
></a>
</li>
<li className={formData.eventColor === "success" ? "active" : ""}>
<a
href="#"
className="success"
onClick={(e) => {
e.preventDefault();
handleColorSelect("success");
}}
></a>
</li>
<li className={formData.eventColor === "info" ? "active" : ""}>
<a
href="#"
className="info"
onClick={(e) => {
e.preventDefault();
handleColorSelect("info");
}}
></a>
</li>
<li className={formData.eventColor === "warning" ? "active" : ""}>
<a
href="#"
className="warning"
onClick={(e) => {
e.preventDefault();
handleColorSelect("warning");
}}
></a>
</li>
<li className={formData.eventColor === "danger" ? "active" : ""}>
<a
href="#"
className="danger"
onClick={(e) => {
e.preventDefault();
handleColorSelect("danger");
}}
></a>
</li>
</ul>
</div>
</div>
</div>
<hr />
{/* Schedule */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Schedule</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-5" style={{ display: formData.allDay ? "none" : "block" }}>
<div className="form-group">
<input
className="form-control"
type="date"
id="startDate"
name="startDate"
value={formData.startDate}
onChange={handleChange}
required
/>
</div>
</div>
<div className="col-sm-5" style={{ display: formData.allDay ? "none" : "block" }}>
<div className="form-group">
<select
className="form-control"
id="startTime"
name="startTime"
value={formData.startTime}
onChange={handleChange}
required={!formData.allDay}
>
{timeOptions.map((time) => (
<option key={time} value={time}>
{time}
</option>
))}
</select>
</div>
</div>
<div className="col-sm-2" style={{ display: formData.allDay ? "none" : "block" }}>
<h5>to</h5>
</div>
</div>
<div className="row">
<div className="col-sm-5" style={{ display: formData.allDay ? "none" : "block" }}>
<div className="form-group">
<input
className="form-control"
type="date"
id="endDate"
name="endDate"
value={formData.endDate}
onChange={handleChange}
required
/>
</div>
</div>
<div className="col-sm-5" style={{ display: formData.allDay ? "none" : "block" }}>
<div className="form-group">
<select
className="form-control"
id="endTime"
name="endTime"
value={formData.endTime}
onChange={handleChange}
required={!formData.allDay}
>
{timeOptions.map((time) => (
<option key={time} value={time}>
{time}
</option>
))}
</select>
</div>
</div>
</div>
<div className="checkbox stylized">
<label>
<input
className="check-box"
type="checkbox"
id="allDay"
name="allDay"
checked={formData.allDay}
onChange={handleChange}
/>
<span className="helper"></span>
<span>All day Event</span>
</label>
</div>
</div>
<hr />
{/* Action Buttons */}
<div className="form-group text-right">
<span className="text-pad-right text-sm">
All fields marked with a <span className="required-field"></span> are required.
</span>
<button
type="button"
className="btn btn-lg btn-default"
onClick={handleCancel}
disabled={loading}
style={{ marginRight: "10px" }}
>
Cancel
</button>
<button
type="submit"
className="btn btn-lg btn-success"
disabled={loading}
>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<>
<CalendarActionBar
onWorkspaceChange={(type) => 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}
/>
<section className="calendarPage">
{/* Calendar Container */}
<div className="calendarContainer">
{loading && (
<div className="calendarLoading">
<i className="fa fa-spinner fa-spin fa-2x"></i>
<p>Loading events...</p>
</div>
)}
{/* Custom Title Display */}
<div className="calendarTitle">
<h2>{currentTitle}</h2>
</div>
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
headerToolbar={false}
events={events}
editable={false}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
weekends={true}
datesSet={handleDatesSet}
eventClick={handleEventClick}
dateClick={handleDateClick}
height="auto"
fixedWeekCount={false}
showNonCurrentDates={true}
eventTimeFormat={{
hour: "2-digit",
minute: "2-digit",
meridiem: "short",
}}
/>
</div>
<DeleteModal
isOpen={deleteModal.isOpen}
onClose={() => 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}
/>
</section>
</>
);
}

View file

@ -1,163 +0,0 @@
export default function Table({
loading,
error,
rows,
onEdit,
onDelete,
page,
setPage,
pageSize,
setPageSize,
totalCount,
totalPages,
}) {
if (loading) {
return (
<div className="tableContainer">
<div className="loadingState">
<i className="fa fa-spinner fa-spin fa-3x"></i>
<p>Loading events...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="tableContainer">
<div className="errorState">
<i className="fa fa-exclamation-triangle fa-3x"></i>
<p>{error}</p>
</div>
</div>
);
}
if (rows.length === 0) {
return (
<div className="tableContainer">
<div className="emptyState">
<i className="fa fa-calendar fa-3x"></i>
<p>No events found</p>
<small>Create your first event to get started</small>
</div>
</div>
);
}
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 (
<div className="tableContainer">
<table className="dataTable">
<thead>
<tr>
<th>Title</th>
<th>Start Date</th>
<th>End Date</th>
<th>Location</th>
<th>Color</th>
<th>All Day</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<td>
<strong>{row.title}</strong>
{row.description && (
<div className="description">{row.description.substring(0, 100)}</div>
)}
</td>
<td>
{formatDate(row.startDate)}
{row.startTime && <div className="timeText">{row.startTime}</div>}
</td>
<td>
{formatDate(row.endDate)}
{row.endTime && <div className="timeText">{row.endTime}</div>}
</td>
<td>{row.location || "-"}</td>
<td>
<span className={getColorBadgeClass(row.eventColor)}>
{row.eventColor || "primary"}
</span>
</td>
<td>{row.allDay ? "Yes" : "No"}</td>
<td>
<div className="actionButtons">
<button
className="actionBtn editBtn"
onClick={() => onEdit(row)}
title="Edit"
>
<i className="fa fa-edit"></i>
</button>
<button
className="actionBtn deleteBtn"
onClick={() => onDelete(row)}
title="Delete"
>
<i className="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
<div className="pagination">
<div className="paginationInfo">
Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, totalCount)} of{" "}
{totalCount} events
</div>
<div className="paginationControls">
<label htmlFor="pageSize">Rows per page:</label>
<select
id="pageSize"
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setPage(1);
}}
>
<option value={12}>12</option>
<option value={24}>24</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
<button className="pageBtn" disabled={page === 1} onClick={() => setPage(page - 1)}>
Previous
</button>
<span className="pageInfo">
Page {page} of {totalPages}
</span>
<button
className="pageBtn"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
Next
</button>
</div>
</div>
</div>
);
}

View file

@ -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;

View file

@ -1,4 +0,0 @@
import {post } from "../../../apiUtil";
import { API_URL } from '../../../constants';
export const editContact = (item) => post(`${API_URL}/Contact/EditContacts`, item)

View file

@ -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 (
<div className="formPage">
<div className="panel panel-default panel-connected">
<div className="panel-body">
<ol className="breadcrumb breadcrumb-alt">
<li>
<a
href="#"
onClick={(e) => {
e.preventDefault();
navigate("/contacts");
}}
>
Contacts
</a>
</li>
<li className="active">{isEdit ? "Edit" : "Create"}</li>
</ol>
<div className="container">
<form onSubmit={handleSave}>
{/* Page Header */}
<div className="form-group">
<div className="header">
<h3 className="heading thin">
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i
className={`fa fa-${
isEdit ? "edit" : "plus"
} fa-stack-1x fa-inverse`}
></i>
</span>
<span>
{isEdit ? "Edit a Contact" : "Create a Contact"}
</span>
</h3>
</div>
</div>
{/* General Details Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>General Details</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-5">
<div className="form-group">
<span className="input-counter">50</span>
<label className="required-field" htmlFor="firstName">
First Name
</label>
<input
className="form-control"
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Enter first name"
required
/>
</div>
</div>
<div className="col-sm-2">
<div className="form-group">
<span className="input-counter">50</span>
<label htmlFor="middleName">Middle Name</label>
<input
className="form-control"
type="text"
id="middleName"
name="middleName"
value={formData.middleName}
onChange={handleChange}
placeholder="Enter middle name"
/>
</div>
</div>
<div className="col-sm-5">
<div className="form-group">
<span className="input-counter">50</span>
<label className="required-field" htmlFor="lastName">
Last Name
</label>
<input
className="form-control"
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Enter last name"
required
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">50</span>
<label htmlFor="title">Title</label>
<input
className="form-control"
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Enter a title"
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="contactType">Contact Type</label>
<select
className="form-control"
id="contactType"
name="contactType"
value={formData.contactType}
onChange={handleChange}
>
<option value="">Select a Contact Type</option>
<option value="Corporate">Corporate</option>
<option value="Store Mgr">Store Mgr</option>
<option value="Facilities">Facilities</option>
<option value="Area Leader">Area Leader</option>
</select>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="gender">Gender</label>
<select
className="form-control"
id="gender"
name="gender"
value={formData.gender}
onChange={handleChange}
>
<option value="">Select a gender</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Not specified">Not specified</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="dateOfBirth">Date of Birth</label>
<input
className="form-control"
type="date"
id="dateOfBirth"
name="dateOfBirth"
value={formData.dateOfBirth}
onChange={handleChange}
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label className="required-field" htmlFor="territory">
Territory
</label>
<select
className="form-control"
id="territory"
name="territory"
value={formData.territory}
onChange={handleChange}
required
>
<option value="">Select a Territory</option>
<option value="-1">No Territory</option>
</select>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label className="required-field" htmlFor="owner">
Owner
</label>
<select
className="form-control"
id="owner"
name="owner"
value={formData.owner}
onChange={handleChange}
required
>
<option value="">Select an Owner</option>
<option value="1">Employee 1</option>
<option value="2">Employee 2</option>
</select>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label className="required-field" htmlFor="privacySetting">
Privacy Setting
</label>
<select
className="form-control"
id="privacySetting"
name="privacySetting"
value={formData.privacySetting}
onChange={handleChange}
required
>
<option value="PublicReadWrite">Public read/write</option>
<option value="PublicReadOnly">Public read only</option>
<option value="Private">Private</option>
</select>
</div>
</div>
</div>
<div className="form-group">
<label htmlFor="about">About</label>
<input
className="form-control"
type="text"
id="about"
name="about"
value={formData.about}
onChange={handleChange}
placeholder="Enter details about the contact"
/>
</div>
<div className="form-group">
<label htmlFor="tags">Tags</label>
<input
className="form-control"
type="text"
id="tags"
name="tags"
value={formData.tags}
onChange={handleChange}
/>
<span className="form-control-label">
Press <code>Enter</code> after each tag to complete it.
</span>
</div>
</div>
<hr />
{/* Address Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Address</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">250</span>
<label htmlFor="street">Address 1</label>
<input
className="form-control"
type="text"
id="street"
name="street"
value={formData.street || ""}
onChange={handleChange}
placeholder="Enter street address or P.O. box"
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">250</span>
<label htmlFor="streetLineTwo">Address 2</label>
<input
className="form-control"
type="text"
id="streetLineTwo"
name="streetLineTwo"
value={formData.streetLineTwo || ""}
onChange={handleChange}
placeholder="Enter apt/unit/suite number (ex. Unit 1)"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">100</span>
<label htmlFor="city">City</label>
<input
className="form-control"
type="text"
id="city"
name="city"
value={formData.city || ""}
onChange={handleChange}
placeholder="Enter city name"
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">20</span>
<label htmlFor="zip">Zip/Postal Code</label>
<input
className="form-control"
type="text"
id="zip"
name="zip"
value={formData.zip || ""}
onChange={handleChange}
placeholder="Enter code"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="country">Country</label>
<select
className="form-control"
id="country"
name="country"
value={formData.country || "US"}
onChange={handleChange}
>
<option value="">Select a Country</option>
<option value="US">United States</option>
</select>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="state">State / Province</label>
<select
className="form-control"
id="state"
name="state"
value={formData.state || ""}
onChange={handleChange}
>
<option value="">Select a State/Province</option>
<option value="AL">Alabama</option>
<option value="AK">Alaska</option>
<option value="AZ">Arizona</option>
<option value="CA">California</option>
<option value="FL">Florida</option>
<option value="NY">New York</option>
<option value="TX">Texas</option>
</select>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="county">County</label>
<select
className="form-control"
id="county"
name="county"
value={formData.county || ""}
onChange={handleChange}
>
<option value="">Select a County</option>
</select>
</div>
</div>
</div>
</div>
<hr />
{/* Phone & Email Communication Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Phone &amp; Email Communication</strong>
</h5>
</div>
<div className="form-group">
<label>Phone Number</label>
<p className="text-muted text-sm">
Add phone numbers for this contact
</p>
</div>
<div className="form-group">
<label>Email Addresses</label>
<p className="text-muted text-sm">
Add email addresses for this contact
</p>
</div>
</div>
<hr />
{/* Additional Details Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Additional Details</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="timeZone">Time Zone</label>
<select
className="form-control"
id="timeZone"
name="timeZone"
value={formData.timeZone}
onChange={handleChange}
>
<option value="">Select a time zone</option>
<option value="Eastern Standard Time">
(UTC-05:00) Eastern Time (US &amp; Canada)
</option>
<option value="Central Standard Time">
(UTC-06:00) Central Time (US &amp; Canada)
</option>
<option value="Mountain Standard Time">
(UTC-07:00) Mountain Time (US &amp; Canada)
</option>
<option value="Pacific Standard Time">
(UTC-08:00) Pacific Time (US &amp; Canada)
</option>
</select>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="smsAddress">SMS Address</label>
<div className="input-group">
<input
className="form-control"
type="email"
id="smsAddress"
name="smsAddress"
value={formData.smsAddress || ""}
onChange={handleChange}
placeholder="13625550100@providerdomain.com"
/>
<span className="input-group-btn">
<button
type="button"
title="Test SMS Text Message"
className="btn btn-primary"
>
<i className="fa fa-comment"></i>
</button>
</span>
</div>
<span className="form-control-label">
Do not include brackets or dashes for the number.
</span>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<span className="input-counter">2048</span>
<label htmlFor="website">Website</label>
<input
className="form-control"
type="url"
id="website"
name="website"
value={formData.website || ""}
onChange={handleChange}
placeholder="http://"
/>
<span className="form-control-label">
Make sure to include the protocol (http://)
</span>
</div>
</div>
</div>
</div>
<hr />
{/* Social Media Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Social Media</strong>
</h5>
</div>
<p className="text-muted text-sm">
Enter the full URL of the Social Media profile you wish to
link.
</p>
<div className="row">
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="facebookUrl">Facebook</label>
<input
className="form-control"
type="text"
id="facebookUrl"
name="facebookUrl"
value={formData.facebookUrl || ""}
onChange={handleChange}
placeholder="Enter Facebook URL"
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="linkedInUrl">LinkedIn</label>
<input
className="form-control"
type="text"
id="linkedInUrl"
name="linkedInUrl"
value={formData.linkedInUrl || ""}
onChange={handleChange}
placeholder="Enter LinkedIn URL"
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="twitterUrl">Twitter</label>
<input
className="form-control"
type="text"
id="twitterUrl"
name="twitterUrl"
value={formData.twitterUrl || ""}
onChange={handleChange}
placeholder="Enter Twitter URL"
/>
</div>
</div>
</div>
</div>
<hr />
{/* Action Buttons */}
<div className="form-group text-right">
<span className="text-pad-right text-sm">
All fields marked with a <span className="required-field"></span> are
required.
</span>
<button
type="submit"
className="btn btn-lg btn-success"
disabled={loading}
>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
};
export default ContactFormPage;

View file

@ -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 (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<>
<ActionBar
moduleName="Contacts"
moduleIcon="fa-address-card"
onNew={() => navigate("/contacts/new")}
onBulkEdit={() => console.log("Bulk edit")}
selectedCount={0}
/>
<section className="listPage">
<div className="breadcrumb">
<span className="breadcrumbItem active">Contacts</span>
</div>
<div className="searchSection">
<div className="searchHeader">
<h3 className="searchHeading">
<span className="fa-stack iconStack">
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className="fa fa-address-card fa-stack-1x fa-inverse"></i>
</span>
<span>Search for Contacts</span>
</h3>
<span className="searchSubheading">Use the settings menu to adjust how to filter the list, and which columns to display.</span>
</div>
<div className="searchControls">
<div className="searchLeft">
<div className="searchInputGroup">
<select className="searchDropdown" defaultValue="Name">
<option>Name</option>
<option>Account</option>
<option>Owner</option>
<option>Email</option>
<option>Phone</option>
<option>Title</option>
</select>
<input
className="searchInput"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Enter search criteria here..."
/>
<button className="searchButton">Search!</button>
</div>
<div className="viewToggle">
<button
className={`viewBtn ${viewMode === "list" ? "active" : ""}`}
title="List view"
onClick={() => setViewMode("list")}
>
<i className="fa fa-navicon"></i>
</button>
<button
className={`viewBtn ${viewMode === "card" ? "active" : ""}`}
title="Card view"
onClick={() => setViewMode("card")}
>
<i className="fa fa-th-large"></i>
</button>
</div>
</div>
<div className="searchRight">
<div className="paginationTop">
<button
className="pageBtn"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
«
</button>
<span className="pageNumber">{page}</span>
<button
className="pageBtn"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
»
</button>
</div>
</div>
</div>
</div>
<Table loading={loading} error={err} rows={rows}
onEdit={(item) => navigate(`/contacts/edit/${item.id}`)}
onDelete={(item) => console.log("Delete", item)}
page={page} setPage={setPage}
pageSize={pageSize} setPageSize={setPageSize}
totalCount={totalCount} totalPages={totalPages} />
</section>
</>
);
}

View file

@ -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) => <strong><a href="#" onClick={(e) => { e.preventDefault(); onEdit && onEdit(row); }}>{val}</a></strong> },
{ key: "account", label: "Account" },
{ key: "title", label: "Title" },
{ key: "email", label: "Email" },
{ key: "phoneNumber", label: "Phone" },
{ key: "owner", label: "Owner" },
];
return (
<SharedTable
rows={rows}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
loading={loading}
error={error}
/>
);
}

View file

@ -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}`);

View file

@ -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;
}

View file

@ -1,4 +0,0 @@
import {post } from "../../../apiUtil";
import { API_URL } from '../../../constants';
export const addContact = (item) => post(`${API_URL}/Contact/AddContacts`, item);

View file

@ -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);
}
}

View file

@ -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 <div className="loading">Loading dashboard...</div>;
return (
<section className="listPage">
<div className="pageTitle">Dashboard</div>
{/* KPI Cards */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))", gap: "20px", marginBottom: "30px" }}>
<div className="card" style={{ padding: "20px", textAlign: "center" }}>
<div style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}>Total Accounts</div>
<div style={{ fontSize: "32px", fontWeight: "bold", color: "#003A5D" }}>{kpis?.totalAccounts}</div>
</div>
<div className="card" style={{ padding: "20px", textAlign: "center" }}>
<div style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}>Active Work Orders</div>
<div style={{ fontSize: "32px", fontWeight: "bold", color: "#007bff" }}>{kpis?.activeWorkOrders}</div>
</div>
<div className="card" style={{ padding: "20px", textAlign: "center" }}>
<div style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}>Completed Orders</div>
<div style={{ fontSize: "32px", fontWeight: "bold", color: "#28a745" }}>{kpis?.completedOrders}</div>
</div>
<div className="card" style={{ padding: "20px", textAlign: "center" }}>
<div style={{ fontSize: "12px", color: "#666", marginBottom: "8px" }}>Pending Follow-ups</div>
<div style={{ fontSize: "32px", fontWeight: "bold", color: "#ffc107" }}>{kpis?.pendingFollowUps}</div>
</div>
</div>
{/* Analytics Tables */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px" }}>
{/* Priority Analysis */}
<div className="card" style={{ padding: "20px" }}>
<h3 style={{ margin: "0 0 20px 0", fontSize: "16px", fontWeight: 600 }}>Priority Analysis</h3>
<table className="dataTable" style={{ width: "100%" }}>
<thead>
<tr>
<th>Priority</th>
<th>Count</th>
<th>%</th>
</tr>
</thead>
<tbody>
{priorityData.map((row, idx) => (
<tr key={idx}>
<td>
<span style={{
padding: "4px 12px",
borderRadius: "12px",
fontSize: "12px",
fontWeight: 500,
backgroundColor: row.priority === "Urgent" ? "#dc3545" : row.priority === "High" ? "#ff9800" : row.priority === "Medium" ? "#2196f3" : "#4caf50",
color: "#fff"
}}>
{row.priority}
</span>
</td>
<td style={{ fontWeight: 600 }}>{row.count}</td>
<td>{row.percentage}%</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Invoice Analysis */}
<div className="card" style={{ padding: "20px" }}>
<h3 style={{ margin: "0 0 20px 0", fontSize: "16px", fontWeight: 600 }}>Invoice Analysis</h3>
<table className="dataTable" style={{ width: "100%" }}>
<thead>
<tr>
<th>Status</th>
<th>Amount</th>
<th>Count</th>
</tr>
</thead>
<tbody>
{invoiceData.map((row, idx) => (
<tr key={idx}>
<td>
<span style={{
padding: "4px 12px",
borderRadius: "12px",
fontSize: "12px",
fontWeight: 500,
backgroundColor: row.status === "Paid" ? "#4caf50" : row.status === "Pending" ? "#2196f3" : "#dc3545",
color: "#fff"
}}>
{row.status}
</span>
</td>
<td style={{ fontWeight: 600 }}>${row.amount.toLocaleString()}</td>
<td>{row.count}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
);
}

View file

@ -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 },
];
};

View file

@ -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;
}
};

View file

@ -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);
};

View file

@ -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;
}
}

View file

@ -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%;
}
}

View file

@ -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 (
<div className="employeeFormPage">
<div className="panel panel-default panel-connected">
<div className="panel-body">
<ol className="breadcrumb breadcrumb-alt">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); navigate("/employees"); }}>
Employees
</a>
</li>
<li className="active">{isEdit ? "Edit" : "Create"}</li>
</ol>
<div className="container">
<form onSubmit={handleSave}>
{/* Page Header */}
<div className="form-group">
<div className="header">
<h3 className="heading thin">
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
</span>
<span>{isEdit ? "Edit an Employee" : "Create an Employee"}</span>
</h3>
</div>
</div>
{/* General Details Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>General Details</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-5">
<div className="form-group">
<span className="input-counter">46</span>
<label className="required-field" htmlFor="firstName">First Name</label>
<input
className="form-control"
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
placeholder="Enter first name"
required
/>
</div>
</div>
<div className="col-sm-2">
<div className="form-group">
<span className="input-counter">50</span>
<label htmlFor="middleName">Middle Name</label>
<input
className="form-control"
type="text"
id="middleName"
name="middleName"
value={formData.middleName}
onChange={handleChange}
placeholder="Enter middle name"
/>
</div>
</div>
<div className="col-sm-5">
<div className="form-group">
<span className="input-counter">44</span>
<label className="required-field" htmlFor="lastName">Last Name</label>
<input
className="form-control"
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
placeholder="Enter last name"
required
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="department">Department</label>
<select
className="form-control"
id="department"
name="department"
value={formData.department}
onChange={handleChange}
>
<option value="">Select a Department</option>
<option value="Administration">Administration</option>
<option value="Sales">Sales</option>
<option value="Service">Service</option>
</select>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label className="required-field" htmlFor="timeZone">Time Zone</label>
<select
className="form-control"
id="timeZone"
name="timeZone"
value={formData.timeZone}
onChange={handleChange}
required
>
<option value="">Select a time zone</option>
<option value="Eastern Standard Time">(UTC-05:00) Eastern Time (US & Canada)</option>
<option value="Central Standard Time">(UTC-06:00) Central Time (US & Canada)</option>
<option value="Mountain Standard Time">(UTC-07:00) Mountain Time (US & Canada)</option>
<option value="Pacific Standard Time">(UTC-08:00) Pacific Time (US & Canada)</option>
</select>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="gender">Gender</label>
<select
className="form-control"
id="gender"
name="gender"
value={formData.gender}
onChange={handleChange}
>
<option value="">Select a gender</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Not specified">Not specified</option>
<option value="Other">Other</option>
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="dateOfBirth">Date of Birth</label>
<input
className="form-control"
type="date"
id="dateOfBirth"
name="dateOfBirth"
value={formData.dateOfBirth}
onChange={handleChange}
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="jobTitle">Job Title</label>
<select
className="form-control"
id="jobTitle"
name="jobTitle"
value={formData.jobTitle}
onChange={handleChange}
>
<option value="">Select a Job Title</option>
<option value="Technician">Technician</option>
<option value="Service Associate">Service Associate</option>
<option value="Accountant">Accountant</option>
<option value="Salesperson">Salesperson</option>
<option value="President">President</option>
</select>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<span className="input-counter">50</span>
<label htmlFor="employeeNumber">Employee #</label>
<input
className="form-control"
type="text"
id="employeeNumber"
name="employeeNumber"
value={formData.employeeNumber}
onChange={handleChange}
placeholder="Enter Employee number"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="hireDate">Hire Date</label>
<input
className="form-control"
type="date"
id="hireDate"
name="hireDate"
value={formData.hireDate}
onChange={handleChange}
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="regionName">Region Name</label>
<select
className="form-control"
id="regionName"
name="regionName"
value={formData.regionName}
onChange={handleChange}
>
<option value="">Select a Region Name</option>
<option value="Region 1">Region 1</option>
<option value="Region 2">Region 2</option>
</select>
</div>
</div>
</div>
</div>
<hr />
{/* Login & Role Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Login &amp; Role</strong>
</h5>
<p className="sub-heading">
{isEdit
? "If the Username is changed, the Employee's account will have to be confirmed again before they can log in. A confirmation email will automatically be sent."
: "A confirmation email will be sent to activate the Employee's account, as well as a temporary password. The Employee's account must be activated before they will be able to log in."}
</p>
</div>
<div className="row">
<div className="col-sm-4">
<div className="form-group">
<label className="required-field" htmlFor="role">Role</label>
<select
className="form-control"
id="role"
name="role"
value={formData.role}
onChange={handleChange}
required
>
<option value="">Select a role</option>
<option value="Admin">Admin</option>
<option value="Administrator User">Administrator User</option>
<option value="Dispatcher">Dispatcher</option>
<option value="Editor User">Editor User</option>
<option value="Employee">Employee</option>
<option value="Read Only User">Read Only User</option>
</select>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label className="required-field" htmlFor="userName">User Name</label>
<input
className="form-control"
type="email"
id="userName"
name="userName"
value={formData.userName || formData.email}
onChange={handleChange}
placeholder="address@email.com"
required
/>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label className="required-field" htmlFor="confirmUserName">Confirm User Name</label>
<div className="input-group">
<input
className="form-control"
type="email"
id="confirmUserName"
name="confirmUserName"
value={formData.confirmUserName || formData.email}
onChange={handleChange}
placeholder="address@email.com"
required
/>
<span className="input-group-btn">
<button type="button" title="Send Test Email" className="btn btn-primary">
<i className="fa fa-envelope"></i>
</button>
</span>
</div>
</div>
</div>
</div>
</div>
<hr />
{/* Address Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Address</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">250</span>
<label htmlFor="street">Address 1</label>
<input
className="form-control"
type="text"
id="street"
name="street"
value={formData.street || ""}
onChange={handleChange}
placeholder="Enter street address or P.O. box"
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">250</span>
<label htmlFor="streetLineTwo">Address 2</label>
<input
className="form-control"
type="text"
id="streetLineTwo"
name="streetLineTwo"
value={formData.streetLineTwo || ""}
onChange={handleChange}
placeholder="Enter apt/unit/suite number (ex. Unit 1)"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">100</span>
<label htmlFor="city">City</label>
<input
className="form-control"
type="text"
id="city"
name="city"
value={formData.city || ""}
onChange={handleChange}
placeholder="Enter city name"
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">20</span>
<label htmlFor="zip">Zip/Postal Code</label>
<input
className="form-control"
type="text"
id="zip"
name="zip"
value={formData.zip || ""}
onChange={handleChange}
placeholder="Enter code"
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="country">Country</label>
<select
className="form-control"
id="country"
name="country"
value={formData.country || "US"}
onChange={handleChange}
>
<option value="">Select a Country</option>
<option value="US">United States</option>
</select>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="state">State / Province</label>
<select
className="form-control"
id="state"
name="state"
value={formData.state || ""}
onChange={handleChange}
>
<option value="">Select a State/Province</option>
<option value="AL">Alabama</option>
<option value="AK">Alaska</option>
<option value="AZ">Arizona</option>
<option value="CA">California</option>
<option value="FL">Florida</option>
<option value="NY">New York</option>
<option value="TX">Texas</option>
</select>
</div>
</div>
<div className="col-sm-4">
<div className="form-group">
<label htmlFor="county">County</label>
<select
className="form-control"
id="county"
name="county"
value={formData.county || ""}
onChange={handleChange}
>
<option value="">Select a County</option>
</select>
</div>
</div>
</div>
</div>
<hr />
{/* Phone & Email Communication Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Phone &amp; Email Communication</strong>
</h5>
</div>
<div className="form-group">
<label>Phone Number</label>
<p className="text-muted text-sm">Add phone numbers for this employee</p>
</div>
<div className="form-group">
<label>Email Addresses</label>
<p className="text-muted text-sm">Add email addresses for this employee</p>
</div>
</div>
<hr />
{/* Additional Details Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Additional Details</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="smsAddress">SMS Address</label>
<div className="input-group">
<input
className="form-control"
type="email"
id="smsAddress"
name="smsAddress"
value={formData.smsAddress || ""}
onChange={handleChange}
placeholder="13625550100@providerdomain.com"
/>
<span className="input-group-btn">
<button type="button" title="Test SMS Text Message" className="btn btn-primary">
<i className="fa fa-comment"></i>
</button>
</span>
</div>
<span className="form-control-label">
Do not include brackets or dashes for the number. Contact your cell provider or visit their support website to find your email to SMS address.
</span>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<span className="input-counter">2048</span>
<label htmlFor="website">Website</label>
<input
className="form-control"
type="url"
id="website"
name="website"
value={formData.website || ""}
onChange={handleChange}
placeholder="http://"
/>
<span className="form-control-label">Make sure to include the protocol (http://)</span>
</div>
</div>
</div>
</div>
<hr />
{/* Social Media Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Social Media</strong>
</h5>
</div>
<p className="text-muted text-sm">
Enter the full URL of the Social Media profile you wish to link.
</p>
<div className="row">
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="facebookUrl">Facebook</label>
<input
className="form-control"
type="text"
id="facebookUrl"
name="facebookUrl"
value={formData.facebookUrl || ""}
onChange={handleChange}
placeholder="Enter Facebook URL"
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="linkedInUrl">LinkedIn</label>
<input
className="form-control"
type="text"
id="linkedInUrl"
name="linkedInUrl"
value={formData.linkedInUrl || ""}
onChange={handleChange}
placeholder="Enter LinkedIn URL"
/>
</div>
</div>
<div className="col-sm-6 col-md-4">
<div className="form-group">
<label htmlFor="twitterUrl">Twitter</label>
<input
className="form-control"
type="text"
id="twitterUrl"
name="twitterUrl"
value={formData.twitterUrl || ""}
onChange={handleChange}
placeholder="Enter Twitter URL"
/>
</div>
</div>
</div>
</div>
<hr />
{/* Action Buttons */}
<div className="form-group text-right">
<span className="text-pad-right text-sm">
All fields marked with a <span className="required-field"></span> are required.
</span>
<button
type="submit"
className="btn btn-lg btn-success"
disabled={loading}
>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<>
<ActionBar
moduleName="Employees"
moduleIcon="fa-user"
onNew={() => navigate("/employees/new")}
onBulkEdit={() => console.log("Bulk edit")}
selectedCount={0}
/>
<section className="listPage">
<div className="breadcrumb">
<span className="breadcrumbItem active">Employees</span>
</div>
<div className="searchSection">
<div className="searchHeader">
<h3 className="searchHeading">
<span className="fa-stack iconStack">
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className="fa fa-user fa-stack-1x fa-inverse"></i>
</span>
<span>Search for Employees</span>
</h3>
<span className="searchSubheading">Use the settings menu to adjust how to filter the list, and which columns to display.</span>
</div>
<div className="searchControls">
<div className="searchLeft">
<div className="searchInputGroup">
<select className="searchDropdown" defaultValue="Name">
<option>Name</option>
<option>Email</option>
<option>Phone</option>
<option>Role</option>
</select>
<input
className="searchInput"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Enter search criteria here..."
/>
<button className="searchButton">Search!</button>
</div>
<div className="viewToggle">
<button
className={`viewBtn ${viewMode === "list" ? "active" : ""}`}
title="List view"
onClick={() => setViewMode("list")}
>
<i className="fa fa-navicon"></i>
</button>
<button
className={`viewBtn ${viewMode === "card" ? "active" : ""}`}
title="Card view"
onClick={() => setViewMode("card")}
>
<i className="fa fa-th-large"></i>
</button>
</div>
</div>
<div className="searchRight">
<div className="paginationTop">
<button
className="pageBtn"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
«
</button>
<span className="pageNumber">{page}</span>
<button
className="pageBtn"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
»
</button>
</div>
</div>
</div>
</div>
<Table loading={loading} error={err} rows={rows}
onEdit={(item) => navigate(`/employees/edit/${item.id}`)}
onDelete={handleDeleteClick}
page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize}
totalCount={totalCount} totalPages={totalPages} />
<DeleteModal
isOpen={deleteModal.isOpen}
onClose={() => 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}
/>
</section>
</>
);
}

View file

@ -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) => <strong><a href="#" onClick={(e) => { e.preventDefault(); onEdit && onEdit(row); }}>{val}</a></strong> },
{ 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 (
<SharedTable
rows={rows}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
loading={loading}
error={error}
/>
);
}

View file

@ -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}`);

View file

@ -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;
}

View file

@ -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);
};

View file

@ -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}`);

View file

@ -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);
};

View file

@ -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%;
}
}

View file

@ -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 (
<div className="followUpFormPage">
<div className="panel panel-default panel-connected">
<div className="panel-body">
<ol className="breadcrumb breadcrumb-alt">
<li>
<a
href="#"
onClick={(e) => {
e.preventDefault();
navigate("/followups");
}}
>
Follow-ups
</a>
</li>
<li className="active">{isEdit ? "Edit" : "Create"}</li>
</ol>
<div className="container">
<form onSubmit={handleSave}>
<div className="form-group">
<div className="header">
<h3 className="heading thin">
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i
className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}
></i>
</span>
<span>{isEdit ? "Edit a Follow-up" : "Create a Follow-up"}</span>
</h3>
</div>
</div>
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Follow-up Details</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="reason">
Reason
</label>
<input
className="form-control"
type="text"
id="reason"
name="reason"
value={formData.reason}
onChange={handleChange}
placeholder="What is this follow-up for?"
required
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="employeeId">
Assigned To
</label>
<select
className="form-control"
id="employeeId"
name="employeeId"
value={formData.employeeId}
onChange={handleChange}
required
>
<option value="">Select Employee</option>
{employees.map((e) => (
<option key={e.id} value={e.id}>
{e.name || `Employee #${e.id}`}
</option>
))}
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="scheduleDate">
Scheduled Date
</label>
<input
className="form-control"
type="date"
id="scheduleDate"
name="scheduleDate"
value={formData.scheduleDate}
onChange={handleChange}
required
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="scheduleStartTime">
Start Time
</label>
<input
className="form-control"
type="time"
id="scheduleStartTime"
name="scheduleStartTime"
value={formData.scheduleStartTime}
onChange={handleChange}
required
/>
</div>
</div>
</div>
</div>
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Related (optional)</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="accountId">Account</label>
<select
className="form-control"
id="accountId"
name="accountId"
value={formData.accountId}
onChange={handleChange}
>
<option value="">None</option>
{accounts.map((a) => (
<option key={a.id} value={a.id}>
{a.name || `Account #${a.id}`}
</option>
))}
</select>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="locationId">Location</label>
<select
className="form-control"
id="locationId"
name="locationId"
value={formData.locationId}
onChange={handleChange}
>
<option value="">None</option>
{locations.map((l) => (
<option key={l.id} value={l.id}>
{l.name || `Location #${l.id}`}
</option>
))}
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="workOrderId">Work Order ID</label>
<input
className="form-control"
type="number"
id="workOrderId"
name="workOrderId"
value={formData.workOrderId}
onChange={handleChange}
placeholder="Numeric WO ID (optional)"
/>
</div>
</div>
</div>
</div>
<div className="form-actions">
<button
type="button"
className="btn btn-default"
onClick={handleCancel}
disabled={loading}
>
Cancel
</button>
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show more