mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-06 12:22:03 +00:00
chore: remove legacy CRA pages, Redux store and JS hooks
This commit is contained in:
parent
7af4a56d04
commit
fac35319e5
161 changed files with 0 additions and 23458 deletions
179
src/App.js
179
src/App.js
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}>×</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
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
569
src/index.css
569
src/index.css
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<div id="root"></div>
|
||||
21
src/index.js
21
src/index.js
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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),
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}`);
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
|
|
@ -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)),
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { post } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const editAccount = (data) =>
|
||||
post(`${API_URL}/Account/EditAccount`, data);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { post } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const addAccount = (data) =>
|
||||
post(`${API_URL}/Account/AddAccount`, data);
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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}`);
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import {post } from "../../../apiUtil";
|
||||
import { API_URL } from '../../../constants';
|
||||
|
||||
export const editContact = (item) => post(`${API_URL}/Contact/EditContacts`, item)
|
||||
|
|
@ -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 & 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 & 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="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;
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import {post } from "../../../apiUtil";
|
||||
import { API_URL } from '../../../constants';
|
||||
|
||||
export const addContact = (item) => post(`${API_URL}/Contact/AddContacts`, item);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 },
|
||||
];
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 & 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 & 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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}`);
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue