mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 04:33:11 +00:00
UI changes checking in
This commit is contained in:
parent
8417d708ac
commit
65ec768e13
55 changed files with 6130 additions and 1132 deletions
4
.env
Normal file
4
.env
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Backend API Base URL (without /api suffix)
|
||||
# The /api prefix is added in the service files
|
||||
REACT_APP_API_URL=https://localhost:7195
|
||||
|
||||
13
.env.example
Normal file
13
.env.example
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# API Configuration
|
||||
# Copy this file to .env and update with your actual values
|
||||
|
||||
# Backend API Base URL (without /api suffix)
|
||||
# For local development, this should point to your ASP.NET Core API
|
||||
REACT_APP_API_URL=http://localhost:5000
|
||||
|
||||
# For production, update to your production API URL
|
||||
# REACT_APP_API_URL=https://api.seahavenindustries.com
|
||||
|
||||
# Note: The /api prefix is added in the service files
|
||||
# Example: ${REACT_APP_API_URL}/api/Authentication/login
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -25,3 +25,6 @@
|
|||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
db.txt
|
||||
seed-data.sql
|
||||
/backend
|
||||
115
package-lock.json
generated
115
package-lock.json
generated
|
|
@ -10,6 +10,10 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
"@fullcalendar/interaction": "^6.1.20",
|
||||
"@fullcalendar/react": "^6.1.20",
|
||||
"@fullcalendar/timegrid": "^6.1.20",
|
||||
"@mui/icons-material": "^7.3.1",
|
||||
"@mui/material": "^7.3.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
|
|
@ -2451,6 +2455,52 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/core": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
|
||||
"integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"preact": "~10.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/daygrid": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz",
|
||||
"integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/interaction": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz",
|
||||
"integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/react": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz",
|
||||
"integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.20",
|
||||
"react": "^16.7.0 || ^17 || ^18 || ^19",
|
||||
"react-dom": "^16.7.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/timegrid": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz",
|
||||
"integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==",
|
||||
"dependencies": {
|
||||
"@fullcalendar/daygrid": "~6.1.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
|
|
@ -12943,6 +12993,16 @@
|
|||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.12.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -15646,16 +15706,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
|
@ -18332,6 +18392,41 @@
|
|||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
|
||||
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="
|
||||
},
|
||||
"@fullcalendar/core": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
|
||||
"integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==",
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"preact": "~10.12.1"
|
||||
}
|
||||
},
|
||||
"@fullcalendar/daygrid": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz",
|
||||
"integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"@fullcalendar/interaction": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz",
|
||||
"integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@fullcalendar/react": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz",
|
||||
"integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@fullcalendar/timegrid": {
|
||||
"version": "6.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz",
|
||||
"integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==",
|
||||
"requires": {
|
||||
"@fullcalendar/daygrid": "~6.1.20"
|
||||
}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||
|
|
@ -25654,6 +25749,12 @@
|
|||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"preact": {
|
||||
"version": "10.12.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||
"peer": true
|
||||
},
|
||||
"prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -27628,9 +27729,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"peer": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
"@fullcalendar/interaction": "^6.1.20",
|
||||
"@fullcalendar/react": "^6.1.20",
|
||||
"@fullcalendar/timegrid": "^6.1.20",
|
||||
"@mui/icons-material": "^7.3.1",
|
||||
"@mui/material": "^7.3.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
|
|
|
|||
BIN
public/BrandingImage.png
Normal file
BIN
public/BrandingImage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
122
src/App.js
122
src/App.js
|
|
@ -1,10 +1,14 @@
|
|||
import React, { useState } from "react";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
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";
|
||||
|
|
@ -33,60 +37,96 @@ import FollowUpFormPage from "./pages/followup/form/FollowUpFormPage";
|
|||
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";
|
||||
|
||||
// Protected Route Component
|
||||
function ProtectedRoute({ children }) {
|
||||
return isAuthenticated() ? children : <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
<div className="mainArea">
|
||||
<Topbar onToggleNav={() => setNavCollapsed(v => !v)} />
|
||||
{/* Protected Routes */}
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
|
||||
<Sidebar />
|
||||
|
||||
<div className="contentWrapper">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<div className="mainArea">
|
||||
<Topbar onToggleNav={() => setNavCollapsed(v => !v)} />
|
||||
|
||||
{/* CONTACTS */}
|
||||
<Route path="/contacts" element={<ContactsList />} />
|
||||
<Route path="/contacts/new" element={<ContactFormPage />} />
|
||||
<Route path="/contacts/edit/:id" element={<ContactFormPage />} />
|
||||
<div className="contentWrapper">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
|
||||
{/* ACCOUNTS */}
|
||||
<Route path="/accounts" element={<AccountList />} />
|
||||
<Route path="/accounts/new" element={<AccountFormPage />} />
|
||||
<Route path="/accounts/edit/:id" element={<AccountFormPage />} />
|
||||
{/* CONTACTS */}
|
||||
<Route path="/contacts" element={<ContactsList />} />
|
||||
<Route path="/contacts/new" element={<ContactFormPage />} />
|
||||
<Route path="/contacts/edit/:id" element={<ContactFormPage />} />
|
||||
|
||||
{/* EMPLOYEES */}
|
||||
<Route path="/employees" element={<EmployeeList />} />
|
||||
<Route path="/employees/new" element={<EmployeeFormPage />} />
|
||||
<Route path="/employees/edit/:id" element={<EmployeeFormPage />} />
|
||||
{/* ACCOUNTS */}
|
||||
<Route path="/accounts" element={<AccountList />} />
|
||||
<Route path="/accounts/new" element={<AccountFormPage />} />
|
||||
<Route path="/accounts/edit/:id" element={<AccountFormPage />} />
|
||||
|
||||
{/* LOCATIONS */}
|
||||
<Route path="/locations" element={<LocationsList />} />
|
||||
<Route path="/locations/new" element={<LocationFormPage />} />
|
||||
<Route path="/locations/edit/:id" element={<LocationFormPage />} />
|
||||
{/* EMPLOYEES */}
|
||||
<Route path="/employees" element={<EmployeeList />} />
|
||||
<Route path="/employees/new" element={<EmployeeFormPage />} />
|
||||
<Route path="/employees/edit/:id" element={<EmployeeFormPage />} />
|
||||
|
||||
{/* WORK ORDERS */}
|
||||
<Route path="/workorders" element={<WorkOrdersList />} />
|
||||
<Route path="/workorders/new" element={<WorkOrderFormPage />} />
|
||||
<Route path="/workorders/edit/:id" element={<WorkOrderFormPage />} />
|
||||
{/* LOCATIONS */}
|
||||
<Route path="/locations" element={<LocationsList />} />
|
||||
<Route path="/locations/new" element={<LocationFormPage />} />
|
||||
<Route path="/locations/edit/:id" element={<LocationFormPage />} />
|
||||
|
||||
{/* FOLLOW UPS */}
|
||||
<Route path="/followups" element={<FollowUpList />} />
|
||||
<Route path="/followups/new" element={<FollowUpFormPage />} />
|
||||
<Route path="/followups/edit/:id" element={<FollowUpFormPage />} />
|
||||
{/* WORK ORDERS */}
|
||||
<Route path="/workorders" element={<WorkOrdersList />} />
|
||||
<Route path="/workorders/new" element={<WorkOrderFormPage />} />
|
||||
<Route path="/workorders/edit/:id" element={<WorkOrderFormPage />} />
|
||||
|
||||
{/* PM SCHEDULES */}
|
||||
<Route path="/pmschedules" element={<PMScheduleList />} />
|
||||
<Route path="/pmschedules/new" element={<PMScheduleFormPage />} />
|
||||
<Route path="/pmschedules/edit/:id" element={<PMScheduleFormPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 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 />} />
|
||||
|
||||
{/* CALENDAR */}
|
||||
<Route path="/calendar" element={<CalendarPage />} />
|
||||
<Route path="/calendar/new" element={<EventFormPage />} />
|
||||
<Route path="/calendar/edit/:id" element={<EventFormPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { logout, getCurrentUser } from "../services/authService";
|
||||
|
||||
export default function Topbar({ onToggleNav }) {
|
||||
const navigate = useNavigate();
|
||||
const currentUser = getCurrentUser();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
const getInitials = (fullname) => {
|
||||
if (!fullname) return "U";
|
||||
const parts = fullname.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
return fullname.substring(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="topLeft">
|
||||
|
|
@ -22,8 +41,23 @@ export default function Topbar({ onToggleNav }) {
|
|||
<div className="notif">
|
||||
🔔<span className="dot">1</span>
|
||||
</div>
|
||||
<div className="username">Adam Moussa</div>
|
||||
<div className="avatar">AM</div>
|
||||
<div className="username">{currentUser?.fullname || "User"}</div>
|
||||
<div className="avatar">{getInitials(currentUser?.fullname)}</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
style={{
|
||||
marginLeft: "15px",
|
||||
padding: "6px 12px",
|
||||
backgroundColor: "#dc3545",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
289
src/pages/PmSchedule/form/PmScheduleFormPage.css
Normal file
289
src/pages/PmSchedule/form/PmScheduleFormPage.css
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/* 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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ 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();
|
||||
|
|
@ -72,164 +73,225 @@ export default function PmScheduleFormPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="formPage">
|
||||
<div className="pageTitle">
|
||||
{isEdit ? "Edit PM Schedule" : "Create PM Schedule"}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSave} className="formWrapper">
|
||||
<div className="card formCard">
|
||||
<div className="formBody">
|
||||
{/* ASSET INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Asset Information</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Asset ID *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="asset"
|
||||
value={formData.asset}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="e.g., HVAC-001"
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Asset Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="assetName"
|
||||
value={formData.assetName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Asset description"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* SCHEDULE INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Schedule Information</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Frequency *</label>
|
||||
<select
|
||||
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>
|
||||
{/* Asset Information Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Asset Information</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Last Service Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="lastServiceDate"
|
||||
value={formData.lastServiceDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Next Service Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="nextServiceDate"
|
||||
value={formData.nextServiceDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* ASSIGNMENT SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Assignment</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Assigned To</label>
|
||||
<select
|
||||
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>
|
||||
{/* Schedule Information Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Schedule Information</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Status *</label>
|
||||
<select
|
||||
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 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>
|
||||
</div>
|
||||
|
||||
{/* NOTES SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Notes</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup fullWidth">
|
||||
<label>Notes</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
value={formData.notes}
|
||||
onChange={handleChange}
|
||||
placeholder="PM schedule notes"
|
||||
rows="4"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #ddd",
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
/>
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACTION BUTTONS */}
|
||||
<div className="formActions">
|
||||
<button
|
||||
type="button"
|
||||
className="btnSecondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btnPrimary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</button>
|
||||
{/* 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>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ export const fetchPmSchedulesPaged = async ({
|
|||
pageSize = 12,
|
||||
}) => {
|
||||
const res = await get(
|
||||
`${API_URL}/PmSchedule/GetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`
|
||||
`${API_URL}/PMSchedule/GetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`
|
||||
);
|
||||
|
||||
const rows = (res?.Data || []).map(x => ({
|
||||
id: x.Id,
|
||||
name: x.Name,
|
||||
trade: x.Trade,
|
||||
status: x.Status,
|
||||
nextWorkOrderDate: x.NextWorkOrderDate,
|
||||
owner: x.Owner,
|
||||
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||
id: x.id ?? x.Id,
|
||||
name: x.name ?? x.Name ?? "",
|
||||
trade: x.trade ?? x.Trade ?? "",
|
||||
status: x.status ?? x.Status ?? "",
|
||||
nextWorkOrderDate: x.nextWorkOrderDate ?? x.NextWorkOrderDate ?? "",
|
||||
owner: x.owner ?? x.Owner ?? "",
|
||||
}));
|
||||
|
||||
return {
|
||||
rows,
|
||||
page: res.PageNumber,
|
||||
pageSize: res.PageSize,
|
||||
totalCount: res.TotalCount,
|
||||
totalPages: res.TotalPages,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { fetchContactsPaged } from "./api";
|
||||
import { fetchPmSchedulesPaged } from "./api";
|
||||
|
||||
export default function useItems() {
|
||||
const [rows, setRows] = useState([]);
|
||||
|
|
@ -13,7 +13,7 @@ export default function useItems() {
|
|||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
setErr("");
|
||||
fetchContactsPaged({ search, page, pageSize })
|
||||
fetchPmSchedulesPaged({ search, page, pageSize })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { getPMSchedules } from "../api";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { fetchPmSchedulesPaged } from "./api";
|
||||
|
||||
const usePMSchedules = () => {
|
||||
const [rows, setRows] = useState([]);
|
||||
|
|
@ -7,35 +7,41 @@ const usePMSchedules = () => {
|
|||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const reload = async () => {
|
||||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const pmschedules = await getPMSchedules();
|
||||
let filtered = pmschedules;
|
||||
if (search) {
|
||||
filtered = pmschedules.filter((p) =>
|
||||
p.asset.toLowerCase().includes(search.toLowerCase()) ||
|
||||
p.frequency.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
setTotalCount(filtered.length);
|
||||
const start = (page - 1) * pageSize;
|
||||
setRows(filtered.slice(start, start + pageSize));
|
||||
} catch (error) {
|
||||
setErr(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
setErr("");
|
||||
fetchPmSchedulesPaged({ 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();
|
||||
}, [search, page, pageSize]);
|
||||
}, [reload]);
|
||||
|
||||
return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
|
||||
return useMemo(
|
||||
() => ({
|
||||
rows,
|
||||
search,
|
||||
setSearch,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
totalCount,
|
||||
loading,
|
||||
err,
|
||||
reload,
|
||||
}),
|
||||
[rows, search, page, pageSize, totalCount, loading, err, reload]
|
||||
);
|
||||
};
|
||||
|
||||
export default usePMSchedules;
|
||||
|
|
|
|||
|
|
@ -1,66 +1,49 @@
|
|||
// Mock accounts data
|
||||
const mockAccounts = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Acme Corp",
|
||||
email: "contact@acmecorp.com",
|
||||
phone: "(555) 123-4567",
|
||||
owner: "John Smith",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Global Industries",
|
||||
email: "info@globalind.com",
|
||||
phone: "(555) 234-5678",
|
||||
owner: "Sarah Johnson",
|
||||
status: "Active",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Tech Solutions LLC",
|
||||
email: "support@techsol.com",
|
||||
phone: "(555) 345-6789",
|
||||
owner: "Mike Chen",
|
||||
status: "Inactive",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Premier Services",
|
||||
email: "hello@premier.com",
|
||||
phone: "(555) 456-7890",
|
||||
owner: "Lisa Brown",
|
||||
status: "Active",
|
||||
},
|
||||
];
|
||||
import { accountApi } from "../../services/api";
|
||||
|
||||
export const getAccounts = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return [...mockAccounts];
|
||||
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) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return mockAccounts.find(a => a.id === parseInt(id));
|
||||
try {
|
||||
return await accountApi.getById(id);
|
||||
} catch (error) {
|
||||
console.error("Error fetching account:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const addAccount = async (account) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newAccount = { id: mockAccounts.length + 1, ...account };
|
||||
mockAccounts.push(newAccount);
|
||||
return newAccount;
|
||||
try {
|
||||
return await accountApi.create(account);
|
||||
} catch (error) {
|
||||
console.error("Error creating account:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const editAccount = async (account) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockAccounts.findIndex(a => a.id === account.id);
|
||||
if (idx >= 0) mockAccounts[idx] = account;
|
||||
return account;
|
||||
try {
|
||||
return await accountApi.update(account.id, account);
|
||||
} catch (error) {
|
||||
console.error("Error updating account:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAccount = async (id) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockAccounts.findIndex(a => a.id === id);
|
||||
if (idx >= 0) mockAccounts.splice(idx, 1);
|
||||
return true;
|
||||
try {
|
||||
await accountApi.delete(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting account:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
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 data = res?.data || res?.Data || [];
|
||||
|
||||
const rows = data.map((x) => ({
|
||||
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 ?? "",
|
||||
|
|
@ -24,13 +13,12 @@ export const fetchAccountsPaged = async ({
|
|||
phoneNumber: x.phoneNumber ?? x.Phone ?? x.PhoneNumber ?? "",
|
||||
owner: x.owner ?? x.Owner ?? "",
|
||||
}));
|
||||
|
||||
return {
|
||||
rows,
|
||||
page: res?.page ?? res?.PageNumber ?? page,
|
||||
pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
|
||||
totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
|
||||
totalPages: res?.totalPages ?? res?.TotalPages ?? 1,
|
||||
page: res?.pageNumber ?? 1,
|
||||
pageSize: res?.pageSize ?? pageSize,
|
||||
totalCount: res?.totalCount ?? rows.length,
|
||||
totalPages: res?.totalPages ?? 1,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +1,31 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { getAccounts } from "../api";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { fetchAccountsPaged } from "./api";
|
||||
|
||||
const useAccounts = () => {
|
||||
export default function useAccounts() {
|
||||
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(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const reload = async () => {
|
||||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const allAccounts = await getAccounts();
|
||||
let filtered = allAccounts;
|
||||
if (search) {
|
||||
filtered = allAccounts.filter((a) =>
|
||||
a.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.email.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.phone.includes(search)
|
||||
);
|
||||
}
|
||||
setTotalCount(filtered.length);
|
||||
const start = (page - 1) * pageSize;
|
||||
setRows(filtered.slice(start, start + pageSize));
|
||||
} catch (error) {
|
||||
setErr(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
setErr("");
|
||||
fetchAccountsPaged({ search, page, pageSize })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
})
|
||||
.catch((e) => setErr(e.message || "Failed to load"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [search, page, pageSize]);
|
||||
|
||||
return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
|
||||
};
|
||||
useEffect(() => { reload(); }, [reload]);
|
||||
|
||||
export default useAccounts;
|
||||
return useMemo(() => ({
|
||||
rows, search, setSearch, page, setPage, pageSize, setPageSize,
|
||||
totalCount, loading, err, reload
|
||||
}), [rows, search, page, pageSize, totalCount, loading, err, reload]);
|
||||
}
|
||||
|
|
|
|||
290
src/pages/assets/form/AssetFormPage.js
Normal file
290
src/pages/assets/form/AssetFormPage.js
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
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;
|
||||
|
||||
36
src/pages/assets/list/Delete.js
Normal file
36
src/pages/assets/list/Delete.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
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;
|
||||
|
||||
133
src/pages/assets/list/List.js
Normal file
133
src/pages/assets/list/List.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
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;
|
||||
|
||||
88
src/pages/assets/list/Table.js
Normal file
88
src/pages/assets/list/Table.js
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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;
|
||||
|
||||
28
src/pages/assets/list/api.js
Normal file
28
src/pages/assets/list/api.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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}`);
|
||||
|
||||
63
src/pages/assets/list/useItems.js
Normal file
63
src/pages/assets/list/useItems.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
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,
|
||||
};
|
||||
};
|
||||
|
||||
190
src/pages/auth/LoginPage.css
Normal file
190
src/pages/auth/LoginPage.css
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/* 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;
|
||||
}
|
||||
|
||||
143
src/pages/auth/LoginPage.js
Normal file
143
src/pages/auth/LoginPage.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { login } from "../../pages/user/api";
|
||||
import { setUser } from "../../tokenUtility";
|
||||
import "./LoginPage.css";
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
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("");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await login(formData);
|
||||
|
||||
// Store authentication data using tokenUtility
|
||||
setUser(response);
|
||||
|
||||
// Redirect to dashboard/home
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
setError("You have entered an invalid username or password");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="loginPage"
|
||||
style={{
|
||||
backgroundImage: `url(${process.env.PUBLIC_URL}/BrandingImage.png)`
|
||||
}}
|
||||
>
|
||||
<div className="login-wrapper">
|
||||
<div className="login-container">
|
||||
<div className="login-panel">
|
||||
{/* Logo Area */}
|
||||
<div
|
||||
className="login-branding"
|
||||
style={{
|
||||
backgroundImage: `url(${process.env.PUBLIC_URL}/logo.png)`
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="login-header">
|
||||
<h5>User Login</h5>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="login-body">
|
||||
<form onSubmit={handleSubmit} className="login-form" autoComplete="off">
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="button-content">
|
||||
<button
|
||||
type="submit"
|
||||
className="login-button"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Logging in..." : "Log in"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p className="forgot-password">
|
||||
<a href="/forgot-password">Forgot your password?</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Links */}
|
||||
<p className="login-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="copyright">
|
||||
©2026 <em>Core 7 Technologies Inc.</em> All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
114
src/pages/calendar/api.js
Normal file
114
src/pages/calendar/api.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { get, post } from "../../apiUtil";
|
||||
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: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete event");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
142
src/pages/calendar/components/CalendarActionBar.css
Normal file
142
src/pages/calendar/components/CalendarActionBar.css
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
185
src/pages/calendar/components/CalendarActionBar.js
Normal file
185
src/pages/calendar/components/CalendarActionBar.js
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
||||
344
src/pages/calendar/form/EventFormPage.css
Normal file
344
src/pages/calendar/form/EventFormPage.css
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
450
src/pages/calendar/form/EventFormPage.js
Normal file
450
src/pages/calendar/form/EventFormPage.js
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
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(event);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
178
src/pages/calendar/list/CalendarPage.css
Normal file
178
src/pages/calendar/list/CalendarPage.css
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
250
src/pages/calendar/list/CalendarPage.js
Normal file
250
src/pages/calendar/list/CalendarPage.js
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
163
src/pages/calendar/list/Table.js
Normal file
163
src/pages/calendar/list/Table.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
||||
48
src/pages/calendar/list/useEvents.js
Normal file
48
src/pages/calendar/list/useEvents.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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,94 +1,131 @@
|
|||
// Mock employees data
|
||||
const mockEmployees = [
|
||||
{
|
||||
id: 1,
|
||||
name: "John Anderson",
|
||||
email: "john.anderson@company.com",
|
||||
phone: "(555) 123-4567",
|
||||
jobTitle: "Senior Technician",
|
||||
role: "Technician",
|
||||
status: "Active",
|
||||
hireDate: "2020-03-15",
|
||||
address: "123 Main St, Springfield, IL 62701",
|
||||
department: "Field Service",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Sarah Martinez",
|
||||
email: "sarah.martinez@company.com",
|
||||
phone: "(555) 234-5678",
|
||||
jobTitle: "Service Manager",
|
||||
role: "Manager",
|
||||
status: "Active",
|
||||
hireDate: "2019-06-01",
|
||||
address: "456 Oak Ave, Springfield, IL 62702",
|
||||
department: "Operations",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Mike Johnson",
|
||||
email: "mike.johnson@company.com",
|
||||
phone: "(555) 345-6789",
|
||||
jobTitle: "Technician",
|
||||
role: "Technician",
|
||||
status: "Active",
|
||||
hireDate: "2021-01-20",
|
||||
address: "789 Pine Rd, Springfield, IL 62703",
|
||||
department: "Field Service",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Emily Chen",
|
||||
email: "emily.chen@company.com",
|
||||
phone: "(555) 456-7890",
|
||||
jobTitle: "HR Specialist",
|
||||
role: "Admin",
|
||||
status: "Active",
|
||||
hireDate: "2022-05-10",
|
||||
address: "321 Oak St, Springfield, IL 62704",
|
||||
department: "Admin",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "David Williams",
|
||||
email: "david.williams@company.com",
|
||||
phone: "(555) 567-8901",
|
||||
jobTitle: "Field Supervisor",
|
||||
role: "Supervisor",
|
||||
status: "Active",
|
||||
hireDate: "2018-11-30",
|
||||
address: "654 Pine Ave, Springfield, IL 62705",
|
||||
department: "Field Service",
|
||||
},
|
||||
];
|
||||
import { employeeApi } from "../../services/api";
|
||||
|
||||
export const getEmployees = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return [...mockEmployees];
|
||||
// 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) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return mockEmployees.find(e => e.id === parseInt(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) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newEmployee = { id: mockEmployees.length + 1, ...employee };
|
||||
mockEmployees.push(newEmployee);
|
||||
return newEmployee;
|
||||
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) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockEmployees.findIndex(e => e.id === employee.id);
|
||||
if (idx >= 0) mockEmployees[idx] = employee;
|
||||
return 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) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockEmployees.findIndex(e => e.id === id);
|
||||
if (idx >= 0) mockEmployees.splice(idx, 1);
|
||||
return true;
|
||||
try {
|
||||
await employeeApi.delete(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting employee:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,33 +1,23 @@
|
|||
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 || []).map((x) => ({
|
||||
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.name ?? ""} ${x.name ?? ""}`.trim(),
|
||||
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,
|
||||
page: res?.pageNumber ?? 1,
|
||||
pageSize: res?.pageSize ?? pageSize,
|
||||
totalCount: res?.totalCount ?? rows.length,
|
||||
totalPages: res?.totalPages ?? 1,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +1,31 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { getEmployees } from "../api";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { fetchEmployeesPaged } from "./api";
|
||||
|
||||
const useEmployees = () => {
|
||||
export default function useEmployees() {
|
||||
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(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const reload = async () => {
|
||||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const employees = await getEmployees();
|
||||
let filtered = employees;
|
||||
if (search) {
|
||||
filtered = employees.filter((e) =>
|
||||
e.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
e.email.toLowerCase().includes(search.toLowerCase()) ||
|
||||
e.phone.includes(search)
|
||||
);
|
||||
}
|
||||
setTotalCount(filtered.length);
|
||||
const start = (page - 1) * pageSize;
|
||||
setRows(filtered.slice(start, start + pageSize));
|
||||
} catch (error) {
|
||||
setErr(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
setErr("");
|
||||
fetchEmployeesPaged({ search, page, pageSize })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
})
|
||||
.catch((e) => setErr(e.message || "Failed to load"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [search, page, pageSize]);
|
||||
|
||||
return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
|
||||
};
|
||||
useEffect(() => { reload(); }, [reload]);
|
||||
|
||||
export default useEmployees;
|
||||
return useMemo(() => ({
|
||||
rows, search, setSearch, page, setPage, pageSize, setPageSize,
|
||||
totalCount, loading, err, reload
|
||||
}), [rows, search, page, pageSize, totalCount, loading, err, reload]);
|
||||
}
|
||||
|
|
|
|||
289
src/pages/followup/form/FollowUpFormPage.css
Normal file
289
src/pages/followup/form/FollowUpFormPage.css
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/* 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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { useParams, useNavigate } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { getFollowUpById, addFollowUp, editFollowUp } from "../api";
|
||||
import "./FollowUpFormPage.css";
|
||||
|
||||
export default function FollowUpFormPage() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -71,149 +72,202 @@ export default function FollowUpFormPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="formPage">
|
||||
<div className="pageTitle">
|
||||
{isEdit ? "Edit Follow-up" : "Create Follow-up"}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<form onSubmit={handleSave} className="formWrapper">
|
||||
<div className="card formCard">
|
||||
<div className="formBody">
|
||||
{/* BASIC INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Follow-up Details</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Work Order *</label>
|
||||
<select
|
||||
name="workOrder"
|
||||
value={formData.workOrder}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select Work Order</option>
|
||||
<option value="WO-001">WO-001 - HVAC System Maintenance</option>
|
||||
<option value="WO-002">WO-002 - Electrical Repair</option>
|
||||
<option value="WO-003">WO-003 - Plumbing Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Reason *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="reason"
|
||||
value={formData.reason}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Follow-up reason"
|
||||
/>
|
||||
<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 Follow-up" : "Create a Follow-up"}</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Scheduled Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="scheduledDate"
|
||||
value={formData.scheduledDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
{/* Follow-up Details Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Follow-up Details</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Status *</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Pending">Pending</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="workOrder">Work Order</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="workOrder"
|
||||
name="workOrder"
|
||||
value={formData.workOrder}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select Work Order</option>
|
||||
<option value="WO-001">WO-001 - HVAC System Maintenance</option>
|
||||
<option value="WO-002">WO-002 - Electrical Repair</option>
|
||||
<option value="WO-003">WO-003 - Plumbing Issue</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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="Follow-up reason"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="scheduledDate">Scheduled Date</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="date"
|
||||
id="scheduledDate"
|
||||
name="scheduledDate"
|
||||
value={formData.scheduledDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</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="Pending">Pending</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CREATOR INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Creator Information</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Created By *</label>
|
||||
<select
|
||||
name="createdBy"
|
||||
value={formData.createdBy}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<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>
|
||||
{/* Creator Information Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Creator Information</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Created Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="createdDate"
|
||||
value={formData.createdDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="createdBy">Created By</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="createdBy"
|
||||
name="createdBy"
|
||||
value={formData.createdBy}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<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="createdDate">Created Date</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="date"
|
||||
id="createdDate"
|
||||
name="createdDate"
|
||||
value={formData.createdDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NOTES SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Notes</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup fullWidth">
|
||||
<label>Notes</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
value={formData.notes}
|
||||
onChange={handleChange}
|
||||
placeholder="Additional notes or details"
|
||||
rows="5"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #ddd",
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
/>
|
||||
{/* 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="5"
|
||||
placeholder="Additional notes or details"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACTION BUTTONS */}
|
||||
<div className="formActions">
|
||||
<button
|
||||
type="button"
|
||||
className="btnSecondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btnPrimary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</button>
|
||||
{/* 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>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// src/pages/followups/followUpApi.js
|
||||
import { get, post } from "../../apiUtil";
|
||||
import { API_URL } from "../../constants";
|
||||
import { get, post } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const fetchFollowUpsPaged = async ({
|
||||
search = "",
|
||||
|
|
@ -9,30 +9,30 @@ export const fetchFollowUpsPaged = async ({
|
|||
status = "Open"
|
||||
}) => {
|
||||
const res = await get(
|
||||
`${API_URL}/FollowUp/GetFollowUps?search=${encodeURIComponent(
|
||||
`${API_URL}/FollowUp/GetFollowUpList?search=${encodeURIComponent(
|
||||
search
|
||||
)}&page=${page}&pageSize=${pageSize}&status=${status}`
|
||||
);
|
||||
|
||||
const rows = (res?.Data || []).map(x => ({
|
||||
id: x.Id,
|
||||
reason: x.Reason,
|
||||
scheduleDate: x.ScheduleDate,
|
||||
scheduleTime: x.ScheduleStartTime,
|
||||
account: x.AccountName,
|
||||
assignedTo: x.EmployeeName,
|
||||
locationNumber: x.LocationNumber,
|
||||
locationName: x.LocationName,
|
||||
workOrderCode: x.WorkOrderCode,
|
||||
status: x.Status
|
||||
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||
id: x.id ?? x.Id,
|
||||
reason: x.reason ?? x.Reason ?? "",
|
||||
scheduleDate: x.scheduleDate ?? x.ScheduleDate ?? "",
|
||||
scheduleTime: x.scheduleStartTime ?? x.ScheduleStartTime ?? "",
|
||||
account: x.accountName ?? x.AccountName ?? "",
|
||||
assignedTo: x.employeeName ?? x.EmployeeName ?? "",
|
||||
locationNumber: x.locationNumber ?? x.LocationNumber ?? "",
|
||||
locationName: x.locationName ?? x.LocationName ?? "",
|
||||
workOrderCode: x.workOrderCode ?? x.WorkOrderCode ?? "",
|
||||
status: x.status ?? x.Status ?? ""
|
||||
}));
|
||||
|
||||
return {
|
||||
rows,
|
||||
page: res.PageNumber,
|
||||
pageSize: res.PageSize,
|
||||
totalCount: res.TotalCount,
|
||||
totalPages: res.TotalPages
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { getFollowUps } from "../api";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { fetchFollowUpsPaged } from "./api";
|
||||
|
||||
const useFollowUps = () => {
|
||||
const [rows, setRows] = useState([]);
|
||||
|
|
@ -7,35 +7,41 @@ const useFollowUps = () => {
|
|||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const reload = async () => {
|
||||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const followups = await getFollowUps();
|
||||
let filtered = followups;
|
||||
if (search) {
|
||||
filtered = followups.filter((f) =>
|
||||
f.workOrder.toLowerCase().includes(search.toLowerCase()) ||
|
||||
f.reason.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
setTotalCount(filtered.length);
|
||||
const start = (page - 1) * pageSize;
|
||||
setRows(filtered.slice(start, start + pageSize));
|
||||
} catch (error) {
|
||||
setErr(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
setErr("");
|
||||
fetchFollowUpsPaged({ search, page, pageSize, status: "Open" })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
})
|
||||
.catch((e) => setErr(e.message || "Failed to load"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [search, page, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [search, page, pageSize]);
|
||||
}, [reload]);
|
||||
|
||||
return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
|
||||
return useMemo(
|
||||
() => ({
|
||||
rows,
|
||||
search,
|
||||
setSearch,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
totalCount,
|
||||
loading,
|
||||
err,
|
||||
reload,
|
||||
}),
|
||||
[rows, search, page, pageSize, totalCount, loading, err, reload]
|
||||
);
|
||||
};
|
||||
|
||||
export default useFollowUps;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { fetchContactsPaged } from "./api";
|
||||
import { fetchFollowUpsPaged } from "./api";
|
||||
|
||||
export default function useItems() {
|
||||
const [rows, setRows] = useState([]);
|
||||
|
|
@ -13,7 +13,7 @@ export default function useItems() {
|
|||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
setErr("");
|
||||
fetchContactsPaged({ search, page, pageSize })
|
||||
fetchFollowUpsPaged({ search, page, pageSize, status: "Open" })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
|
|
|
|||
|
|
@ -1,73 +1,49 @@
|
|||
// Mock locations data
|
||||
const mockLocations = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Springfield Office",
|
||||
address: "123 Main St",
|
||||
city: "Springfield",
|
||||
state: "IL",
|
||||
zipCode: "62701",
|
||||
phone: "(555) 123-4567",
|
||||
contact: "Jane Smith",
|
||||
contactEmail: "jane.smith@company.com",
|
||||
status: "Active",
|
||||
accountId: "ACC001",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Chicago Distribution Center",
|
||||
address: "456 Oak Ave",
|
||||
city: "Chicago",
|
||||
state: "IL",
|
||||
zipCode: "60601",
|
||||
phone: "(555) 234-5678",
|
||||
contact: "Bob Wilson",
|
||||
contactEmail: "bob.wilson@company.com",
|
||||
status: "Active",
|
||||
accountId: "ACC002",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Indianapolis Warehouse",
|
||||
address: "789 Pine Rd",
|
||||
city: "Indianapolis",
|
||||
state: "IN",
|
||||
zipCode: "46201",
|
||||
phone: "(555) 345-6789",
|
||||
contact: "Alice Brown",
|
||||
contactEmail: "alice.brown@company.com",
|
||||
status: "Active",
|
||||
accountId: "ACC003",
|
||||
},
|
||||
];
|
||||
import { locationApi } from "../../services/api";
|
||||
|
||||
export const getLocations = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return [...mockLocations];
|
||||
export const getLocations = async (params = {}) => {
|
||||
try {
|
||||
const response = await locationApi.getAll(params);
|
||||
const locations = response.data || response;
|
||||
return Array.isArray(locations) ? locations : [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching locations:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLocationById = async (id) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return mockLocations.find(l => l.id === parseInt(id));
|
||||
try {
|
||||
return await locationApi.getById(id);
|
||||
} catch (error) {
|
||||
console.error("Error fetching location:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const addLocation = async (location) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newLocation = { id: mockLocations.length + 1, ...location };
|
||||
mockLocations.push(newLocation);
|
||||
return newLocation;
|
||||
try {
|
||||
return await locationApi.create(location);
|
||||
} catch (error) {
|
||||
console.error("Error creating location:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const editLocation = async (location) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockLocations.findIndex(l => l.id === location.id);
|
||||
if (idx >= 0) mockLocations[idx] = location;
|
||||
return location;
|
||||
try {
|
||||
return await locationApi.update(location.id, location);
|
||||
} catch (error) {
|
||||
console.error("Error updating location:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteLocation = async (id) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockLocations.findIndex(l => l.id === id);
|
||||
if (idx >= 0) mockLocations.splice(idx, 1);
|
||||
return true;
|
||||
try {
|
||||
await locationApi.delete(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting location:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
289
src/pages/locations/form/LocationFormPage.css
Normal file
289
src/pages/locations/form/LocationFormPage.css
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/* Location Form Page - Matching Employee Form Style */
|
||||
.locationFormPage {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.panel-connected {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
padding: 8px 15px;
|
||||
margin-bottom: 20px;
|
||||
list-style: none;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb-alt {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
padding: 10px 20px;
|
||||
margin: 0 0 20px 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.breadcrumb > li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.breadcrumb > li + li:before {
|
||||
content: ">\00a0";
|
||||
padding: 0 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.breadcrumb > li a {
|
||||
color: #0088cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb > li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb > .active {
|
||||
color: #00a0d1;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Form Header */
|
||||
.header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 300;
|
||||
line-height: 1.1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.heading.thin {
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.heading.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #0088cc;
|
||||
}
|
||||
|
||||
.fa-stack {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fa-stack-1x,
|
||||
.fa-stack-2x {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa-stack-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fa-stack-1x {
|
||||
font-size: 1em;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.fa-inverse {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Row and Columns */
|
||||
.row {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
[class*="col-"] {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.col-sm-4 {
|
||||
width: 33.33333333%;
|
||||
}
|
||||
|
||||
.col-sm-6 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.col-md-4 {
|
||||
width: 33.33333333%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
label {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.required-field::after {
|
||||
content: " *";
|
||||
color: #d9534f;
|
||||
}
|
||||
|
||||
/* Form Controls */
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #66afe9;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||
}
|
||||
|
||||
select.form-control {
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
height: auto;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* Form Actions */
|
||||
.form-actions {
|
||||
padding: 19px 20px 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 1.42857143;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
background-color: #e6e6e6;
|
||||
border-color: #adadad;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #0088cc;
|
||||
border-color: #0088cc;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #006699;
|
||||
border-color: #005580;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 767px) {
|
||||
[class*="col-sm-"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { useParams, useNavigate } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { getLocationById, addLocation, editLocation } from "../api";
|
||||
import "./LocationFormPage.css";
|
||||
|
||||
export default function LocationFormPage() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -74,180 +75,241 @@ export default function LocationFormPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="formPage">
|
||||
<div className="pageTitle">
|
||||
{isEdit ? "Edit Location" : "Create Location"}
|
||||
</div>
|
||||
<div className="locationFormPage">
|
||||
<div className="panel panel-default panel-connected">
|
||||
<div className="panel-body">
|
||||
<ol className="breadcrumb breadcrumb-alt">
|
||||
<li>
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); navigate("/locations"); }}>
|
||||
Locations
|
||||
</a>
|
||||
</li>
|
||||
<li className="active">{isEdit ? "Edit" : "Create"}</li>
|
||||
</ol>
|
||||
|
||||
<form onSubmit={handleSave} className="formWrapper">
|
||||
<div className="card formCard">
|
||||
<div className="formBody">
|
||||
{/* BASIC INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Location Information</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Location name"
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Account</label>
|
||||
<input
|
||||
type="text"
|
||||
name="accountId"
|
||||
value={formData.accountId}
|
||||
onChange={handleChange}
|
||||
placeholder="Account ID or name"
|
||||
/>
|
||||
<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 Location" : "Create a Location"}</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Address *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Street address"
|
||||
/>
|
||||
{/* Location Information Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Location Information</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="name">Name</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Location name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="accountId">Account</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="accountId"
|
||||
name="accountId"
|
||||
value={formData.accountId}
|
||||
onChange={handleChange}
|
||||
placeholder="Account ID or name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="address">Address</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={handleChange}
|
||||
placeholder="Street address"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="city">City</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="city"
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleChange}
|
||||
placeholder="City"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="state">State</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="state"
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="IL">Illinois</option>
|
||||
<option value="IN">Indiana</option>
|
||||
<option value="MI">Michigan</option>
|
||||
<option value="OH">Ohio</option>
|
||||
<option value="WI">Wisconsin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="zipCode">Zip Code</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="zipCode"
|
||||
name="zipCode"
|
||||
value={formData.zipCode}
|
||||
onChange={handleChange}
|
||||
placeholder="12345"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>City *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="City"
|
||||
/>
|
||||
{/* Contact Information Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Contact Information</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>State *</label>
|
||||
<select
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="IL">Illinois</option>
|
||||
<option value="IN">Indiana</option>
|
||||
<option value="MI">Michigan</option>
|
||||
<option value="OH">Ohio</option>
|
||||
<option value="WI">Wisconsin</option>
|
||||
</select>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="phone">Phone</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
placeholder="(555) 000-0000"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="contact">Contact Person</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="contact"
|
||||
name="contact"
|
||||
value={formData.contact}
|
||||
onChange={handleChange}
|
||||
placeholder="Full name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Zip Code *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="zipCode"
|
||||
value={formData.zipCode}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="12345"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CONTACT INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Contact Information</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Phone *</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="(555) 000-0000"
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Contact Person *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="contact"
|
||||
value={formData.contact}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="contactEmail">Contact Email</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="email"
|
||||
id="contactEmail"
|
||||
name="contactEmail"
|
||||
value={formData.contactEmail}
|
||||
onChange={handleChange}
|
||||
placeholder="email@company.com"
|
||||
required
|
||||
/>
|
||||
</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="Closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Contact Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
name="contactEmail"
|
||||
value={formData.contactEmail}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="email@company.com"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* STATUS SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Status</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Status *</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
<option value="Closed">Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACTION BUTTONS */}
|
||||
<div className="formActions">
|
||||
<button
|
||||
type="button"
|
||||
className="btnSecondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btnPrimary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
30
src/pages/locations/list/api.js
Normal file
30
src/pages/locations/list/api.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { get } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const fetchLocationsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
|
||||
const res = await get(`${API_URL}/Location/GetLocationList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
|
||||
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||
id: x.id ?? x.Id,
|
||||
name: x.name ?? x.Name ?? "",
|
||||
address: x.address ?? x.Address ?? "",
|
||||
city: x.city ?? x.City ?? "",
|
||||
state: x.state ?? x.State ?? "",
|
||||
zipCode: x.zipCode ?? x.ZipCode ?? x.Zip ?? "",
|
||||
phone: x.phone ?? x.Phone ?? "",
|
||||
contact: x.contact ?? x.Contact ?? "",
|
||||
contactEmail: x.contactEmail ?? x.ContactEmail ?? "",
|
||||
status: x.status ?? x.Status ?? "",
|
||||
accountId: x.accountId ?? x.AccountId ?? "",
|
||||
}));
|
||||
return {
|
||||
rows,
|
||||
page: res?.pageNumber ?? 1,
|
||||
pageSize: res?.pageSize ?? pageSize,
|
||||
totalCount: res?.totalCount ?? rows.length,
|
||||
totalPages: res?.totalPages ?? 1,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteLocation = (id) =>
|
||||
get(`${API_URL}/Location/DeleteLocation?id=${id}`);
|
||||
|
||||
|
|
@ -1,42 +1,31 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { getLocations } from "../api";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { fetchLocationsPaged } from "./api";
|
||||
|
||||
const useLocations = () => {
|
||||
export default function useLocations() {
|
||||
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(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const reload = async () => {
|
||||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const locations = await getLocations();
|
||||
let filtered = locations;
|
||||
if (search) {
|
||||
filtered = locations.filter((l) =>
|
||||
l.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.city.toLowerCase().includes(search.toLowerCase()) ||
|
||||
l.phone.includes(search)
|
||||
);
|
||||
}
|
||||
setTotalCount(filtered.length);
|
||||
const start = (page - 1) * pageSize;
|
||||
setRows(filtered.slice(start, start + pageSize));
|
||||
} catch (error) {
|
||||
setErr(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
setErr("");
|
||||
fetchLocationsPaged({ search, page, pageSize })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
})
|
||||
.catch((e) => setErr(e.message || "Failed to load"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [search, page, pageSize]);
|
||||
|
||||
return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
|
||||
};
|
||||
useEffect(() => { reload(); }, [reload]);
|
||||
|
||||
export default useLocations;
|
||||
return useMemo(() => ({
|
||||
rows, search, setSearch, page, setPage, pageSize, setPageSize,
|
||||
totalCount, loading, err, reload
|
||||
}), [rows, search, page, pageSize, totalCount, loading, err, reload]);
|
||||
}
|
||||
|
|
|
|||
5
src/pages/user/api.js
Normal file
5
src/pages/user/api.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { post } from '../../apiUtil';
|
||||
import { API_URL } from '../../constants';
|
||||
|
||||
export const login = (item) => post(`${API_URL}/Authentication/login`, item);
|
||||
|
||||
|
|
@ -1,73 +1,49 @@
|
|||
// Mock work orders data
|
||||
const mockWorkOrders = [
|
||||
{
|
||||
id: 1,
|
||||
workOrderNumber: "WO-001",
|
||||
title: "HVAC System Maintenance",
|
||||
location: "Springfield Office",
|
||||
priority: "High",
|
||||
status: "In Progress",
|
||||
createdDate: "2024-01-15",
|
||||
dueDate: "2024-01-22",
|
||||
assignedTo: "John Anderson",
|
||||
description: "Quarterly maintenance for HVAC system including filter replacement and inspection",
|
||||
estimatedHours: 4,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
workOrderNumber: "WO-002",
|
||||
title: "Electrical Repair",
|
||||
location: "Chicago Distribution Center",
|
||||
priority: "Medium",
|
||||
status: "Open",
|
||||
createdDate: "2024-01-16",
|
||||
dueDate: "2024-01-25",
|
||||
assignedTo: "Sarah Martinez",
|
||||
description: "Fix faulty outlet in warehouse section C",
|
||||
estimatedHours: 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
workOrderNumber: "WO-003",
|
||||
title: "Plumbing Issue",
|
||||
location: "Indianapolis Warehouse",
|
||||
priority: "Low",
|
||||
status: "Completed",
|
||||
createdDate: "2024-01-10",
|
||||
dueDate: "2024-01-20",
|
||||
assignedTo: "Mike Johnson",
|
||||
description: "Repair leaking pipe in bathroom",
|
||||
estimatedHours: 3,
|
||||
},
|
||||
];
|
||||
import { workOrderApi } from "../../services/api";
|
||||
|
||||
export const getWorkOrders = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return [...mockWorkOrders];
|
||||
export const getWorkOrders = async (params = {}) => {
|
||||
try {
|
||||
const response = await workOrderApi.getAll(params);
|
||||
const workOrders = response.data || response;
|
||||
return Array.isArray(workOrders) ? workOrders : [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching work orders:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWorkOrderById = async (id) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
return mockWorkOrders.find(w => w.id === parseInt(id));
|
||||
try {
|
||||
return await workOrderApi.getById(id);
|
||||
} catch (error) {
|
||||
console.error("Error fetching work order:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const addWorkOrder = async (workOrder) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const newWorkOrder = { id: mockWorkOrders.length + 1, ...workOrder };
|
||||
mockWorkOrders.push(newWorkOrder);
|
||||
return newWorkOrder;
|
||||
try {
|
||||
return await workOrderApi.create(workOrder);
|
||||
} catch (error) {
|
||||
console.error("Error creating work order:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const editWorkOrder = async (workOrder) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockWorkOrders.findIndex(w => w.id === workOrder.id);
|
||||
if (idx >= 0) mockWorkOrders[idx] = workOrder;
|
||||
return workOrder;
|
||||
try {
|
||||
return await workOrderApi.update(workOrder.id, workOrder);
|
||||
} catch (error) {
|
||||
console.error("Error updating work order:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteWorkOrder = async (id) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const idx = mockWorkOrders.findIndex(w => w.id === id);
|
||||
if (idx >= 0) mockWorkOrders.splice(idx, 1);
|
||||
return true;
|
||||
try {
|
||||
await workOrderApi.delete(id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error deleting work order:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
289
src/pages/workorders/form/WorkOrderFormPage.css
Normal file
289
src/pages/workorders/form/WorkOrderFormPage.css
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
/* Work Order Form Page - Matching Employee Form Style */
|
||||
.workOrderFormPage {
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 20px;
|
||||
background-color: #fff;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.panel-connected {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
padding: 8px 15px;
|
||||
margin-bottom: 20px;
|
||||
list-style: none;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb-alt {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
padding: 10px 20px;
|
||||
margin: 0 0 20px 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.breadcrumb > li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.breadcrumb > li + li:before {
|
||||
content: ">\00a0";
|
||||
padding: 0 5px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.breadcrumb > li a {
|
||||
color: #0088cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb > li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb > .active {
|
||||
color: #00a0d1;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Form Header */
|
||||
.header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 300;
|
||||
line-height: 1.1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.heading.thin {
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.heading.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #0088cc;
|
||||
}
|
||||
|
||||
.fa-stack {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fa-stack-1x,
|
||||
.fa-stack-2x {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa-stack-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fa-stack-1x {
|
||||
font-size: 1em;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.fa-inverse {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Row and Columns */
|
||||
.row {
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
[class*="col-"] {
|
||||
position: relative;
|
||||
min-height: 1px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.col-sm-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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { useParams, useNavigate } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { getWorkOrderById, addWorkOrder, editWorkOrder } from "../api";
|
||||
import "./WorkOrderFormPage.css";
|
||||
|
||||
export default function WorkOrderFormPage() {
|
||||
const { id } = useParams();
|
||||
|
|
@ -74,189 +75,254 @@ export default function WorkOrderFormPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="formPage">
|
||||
<div className="pageTitle">
|
||||
{isEdit ? "Edit Work Order" : "Create Work Order"}
|
||||
</div>
|
||||
<div className="workOrderFormPage">
|
||||
<div className="panel panel-default panel-connected">
|
||||
<div className="panel-body">
|
||||
<ol className="breadcrumb breadcrumb-alt">
|
||||
<li>
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); navigate("/workorders"); }}>
|
||||
Work Orders
|
||||
</a>
|
||||
</li>
|
||||
<li className="active">{isEdit ? "Edit" : "Create"}</li>
|
||||
</ol>
|
||||
|
||||
<form onSubmit={handleSave} className="formWrapper">
|
||||
<div className="card formCard">
|
||||
<div className="formBody">
|
||||
{/* BASIC INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Work Order Details</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Work Order Number *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="workOrderNumber"
|
||||
value={formData.workOrderNumber}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="e.g., WO-001"
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Work order title"
|
||||
/>
|
||||
<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 Work Order" : "Create a Work Order"}</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Location *</label>
|
||||
<select
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select Location</option>
|
||||
<option value="Springfield Office">Springfield Office</option>
|
||||
<option value="Chicago Distribution Center">Chicago Distribution Center</option>
|
||||
<option value="Indianapolis Warehouse">Indianapolis Warehouse</option>
|
||||
</select>
|
||||
{/* Work Order Details Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Work Order Details</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Assigned To</label>
|
||||
<select
|
||||
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 className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="workOrderNumber">Work Order Number</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="workOrderNumber"
|
||||
name="workOrderNumber"
|
||||
value={formData.workOrderNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., WO-001"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="title">Title</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Work order title"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DATES & PRIORITY SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Schedule & Priority</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Created Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="createdDate"
|
||||
value={formData.createdDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Due Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="dueDate"
|
||||
value={formData.dueDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Estimated Hours *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="estimatedHours"
|
||||
value={formData.estimatedHours}
|
||||
onChange={handleChange}
|
||||
required
|
||||
min="0.5"
|
||||
step="0.5"
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="location">Location</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="location"
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select Location</option>
|
||||
<option value="Springfield Office">Springfield Office</option>
|
||||
<option value="Chicago Distribution Center">Chicago Distribution Center</option>
|
||||
<option value="Indianapolis Warehouse">Indianapolis Warehouse</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label htmlFor="assignedTo">Assigned To</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="assignedTo"
|
||||
name="assignedTo"
|
||||
value={formData.assignedTo}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">Select Employee</option>
|
||||
<option value="John Anderson">John Anderson</option>
|
||||
<option value="Sarah Martinez">Sarah Martinez</option>
|
||||
<option value="Mike Johnson">Mike Johnson</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Priority *</label>
|
||||
<select
|
||||
name="priority"
|
||||
value={formData.priority}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
{/* Schedule & Priority Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Schedule & Priority</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Status *</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Open">Open</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="On Hold">On Hold</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
<div className="row">
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="createdDate">Created Date</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="date"
|
||||
id="createdDate"
|
||||
name="createdDate"
|
||||
value={formData.createdDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="dueDate">Due Date</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="date"
|
||||
id="dueDate"
|
||||
name="dueDate"
|
||||
value={formData.dueDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="estimatedHours">Estimated Hours</label>
|
||||
<input
|
||||
className="form-control"
|
||||
type="number"
|
||||
id="estimatedHours"
|
||||
name="estimatedHours"
|
||||
value={formData.estimatedHours}
|
||||
onChange={handleChange}
|
||||
required
|
||||
min="0.5"
|
||||
step="0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="form-group">
|
||||
<label className="required-field" htmlFor="priority">Priority</label>
|
||||
<select
|
||||
className="form-control"
|
||||
id="priority"
|
||||
name="priority"
|
||||
value={formData.priority}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Urgent">Urgent</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="Open">Open</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="On Hold">On Hold</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DESCRIPTION SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Description</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup fullWidth">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Detailed description of the work to be performed"
|
||||
rows="5"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #ddd",
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
/>
|
||||
{/* Description Section */}
|
||||
<div className="form-group">
|
||||
<div className="header">
|
||||
<h5 className="heading text-uppercase">
|
||||
<strong>Description</strong>
|
||||
</h5>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
rows="5"
|
||||
placeholder="Detailed description of the work to be performed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACTION BUTTONS */}
|
||||
<div className="formActions">
|
||||
<button
|
||||
type="button"
|
||||
className="btnSecondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btnPrimary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</button>
|
||||
{/* 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>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
262
src/pages/workorders/form/WorkOrderFormPage.js.backup
Normal file
262
src/pages/workorders/form/WorkOrderFormPage.js.backup
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { getWorkOrderById, addWorkOrder, editWorkOrder } from "../api";
|
||||
|
||||
export default function WorkOrderFormPage() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const isEdit = !!id;
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
workOrderNumber: "",
|
||||
title: "",
|
||||
location: "",
|
||||
priority: "Medium",
|
||||
status: "Open",
|
||||
createdDate: new Date().toISOString().split("T")[0],
|
||||
dueDate: "",
|
||||
assignedTo: "",
|
||||
description: "",
|
||||
estimatedHours: 1,
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && id) {
|
||||
const loadWorkOrder = async () => {
|
||||
try {
|
||||
const wo = await getWorkOrderById(id);
|
||||
if (wo) {
|
||||
setFormData(wo);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load work order", err);
|
||||
toast.error("Failed to load work order");
|
||||
}
|
||||
};
|
||||
loadWorkOrder();
|
||||
}
|
||||
}, [id, isEdit]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
if (isEdit) {
|
||||
await editWorkOrder(formData);
|
||||
toast.success("Work Order updated successfully!");
|
||||
} else {
|
||||
await addWorkOrder(formData);
|
||||
toast.success("Work Order created successfully!");
|
||||
}
|
||||
navigate("/workorders");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to save work order");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate("/workorders");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="formPage">
|
||||
<div className="pageTitle">
|
||||
{isEdit ? "Edit Work Order" : "Create Work Order"}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="formWrapper">
|
||||
<div className="card formCard">
|
||||
<div className="formBody">
|
||||
{/* BASIC INFO SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Work Order Details</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Work Order Number *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="workOrderNumber"
|
||||
value={formData.workOrderNumber}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="e.g., WO-001"
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Work order title"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Location *</label>
|
||||
<select
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select Location</option>
|
||||
<option value="Springfield Office">Springfield Office</option>
|
||||
<option value="Chicago Distribution Center">Chicago Distribution Center</option>
|
||||
<option value="Indianapolis Warehouse">Indianapolis Warehouse</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Assigned To</label>
|
||||
<select
|
||||
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>
|
||||
|
||||
{/* DATES & PRIORITY SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Schedule & Priority</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Created Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="createdDate"
|
||||
value={formData.createdDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Due Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
name="dueDate"
|
||||
value={formData.dueDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Estimated Hours *</label>
|
||||
<input
|
||||
type="number"
|
||||
name="estimatedHours"
|
||||
value={formData.estimatedHours}
|
||||
onChange={handleChange}
|
||||
required
|
||||
min="0.5"
|
||||
step="0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="formRow">
|
||||
<div className="formGroup">
|
||||
<label>Priority *</label>
|
||||
<select
|
||||
name="priority"
|
||||
value={formData.priority}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="formGroup">
|
||||
<label>Status *</label>
|
||||
<select
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="Open">Open</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="On Hold">On Hold</option>
|
||||
<option value="Completed">Completed</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DESCRIPTION SECTION */}
|
||||
<div className="formSection">
|
||||
<div className="sectionTitle">Description</div>
|
||||
<div className="formRow">
|
||||
<div className="formGroup fullWidth">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Detailed description of the work to be performed"
|
||||
rows="5"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #ddd",
|
||||
fontFamily: "inherit",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ACTION BUTTONS */}
|
||||
<div className="formActions">
|
||||
<button
|
||||
type="button"
|
||||
className="btnSecondary"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btnPrimary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
28
src/pages/workorders/list/api.js
Normal file
28
src/pages/workorders/list/api.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { get } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const fetchWorkOrdersPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
|
||||
const res = await get(`${API_URL}/WorkOrder/GetWorkOrderList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
|
||||
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||
id: x.id ?? x.Id,
|
||||
number: x.workOrderNumber ?? x.WorkOrderNumber ?? x.number ?? "",
|
||||
title: x.title ?? x.Title ?? "",
|
||||
location: x.location ?? x.Location ?? x.locationName ?? x.LocationName ?? "",
|
||||
priority: x.priority ?? x.Priority ?? "",
|
||||
status: x.status ?? x.Status ?? "",
|
||||
createdDate: x.createdDate ?? x.CreatedDate ?? "",
|
||||
dueDate: x.dueDate ?? x.DueDate ?? "",
|
||||
assignedTo: x.assignedTo ?? x.AssignedTo ?? x.employeeName ?? x.EmployeeName ?? "",
|
||||
}));
|
||||
return {
|
||||
rows,
|
||||
page: res?.pageNumber ?? 1,
|
||||
pageSize: res?.pageSize ?? pageSize,
|
||||
totalCount: res?.totalCount ?? rows.length,
|
||||
totalPages: res?.totalPages ?? 1,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteWorkOrder = (id) =>
|
||||
get(`${API_URL}/WorkOrder/DeleteWorkOrder?id=${id}`);
|
||||
|
||||
|
|
@ -1,42 +1,31 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { getWorkOrders } from "../api";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { fetchWorkOrdersPaged } from "./api";
|
||||
|
||||
const useWorkOrders = () => {
|
||||
export default function useWorkOrders() {
|
||||
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(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
const reload = async () => {
|
||||
const reload = useCallback(() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const workorders = await getWorkOrders();
|
||||
let filtered = workorders;
|
||||
if (search) {
|
||||
filtered = workorders.filter((w) =>
|
||||
w.title.toLowerCase().includes(search.toLowerCase()) ||
|
||||
w.number.toLowerCase().includes(search.toLowerCase()) ||
|
||||
w.location.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
setTotalCount(filtered.length);
|
||||
const start = (page - 1) * pageSize;
|
||||
setRows(filtered.slice(start, start + pageSize));
|
||||
} catch (error) {
|
||||
setErr(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
setErr("");
|
||||
fetchWorkOrdersPaged({ search, page, pageSize })
|
||||
.then(({ rows, totalCount }) => {
|
||||
setRows(rows);
|
||||
setTotalCount(totalCount);
|
||||
})
|
||||
.catch((e) => setErr(e.message || "Failed to load"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [search, page, pageSize]);
|
||||
|
||||
return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
|
||||
};
|
||||
useEffect(() => { reload(); }, [reload]);
|
||||
|
||||
export default useWorkOrders;
|
||||
return useMemo(() => ({
|
||||
rows, search, setSearch, page, setPage, pageSize, setPageSize,
|
||||
totalCount, loading, err, reload
|
||||
}), [rows, search, page, pageSize, totalCount, loading, err, reload]);
|
||||
}
|
||||
|
|
|
|||
134
src/services/api.js
Normal file
134
src/services/api.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// API Base Configuration
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000';
|
||||
const API_PREFIX = '/api';
|
||||
|
||||
// HTTP Methods Helper
|
||||
class ApiService {
|
||||
constructor(baseURL = API_BASE_URL, prefix = API_PREFIX) {
|
||||
this.baseURL = baseURL;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseURL}${this.prefix}${endpoint}`;
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.message || `HTTP Error: ${response.status}`);
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('API Request Error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async get(endpoint, params = {}) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
||||
return this.request(url, { method: 'GET' });
|
||||
}
|
||||
|
||||
async post(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async put(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async delete(endpoint) {
|
||||
return this.request(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
const api = new ApiService();
|
||||
|
||||
// Employee API
|
||||
export const employeeApi = {
|
||||
getAll: (params) => api.get('/employees', params),
|
||||
getById: (id) => api.get(`/employees/${id}`),
|
||||
create: (data) => api.post('/employees', data),
|
||||
update: (id, data) => api.put(`/employees/${id}`, data),
|
||||
delete: (id) => api.delete(`/employees/${id}`),
|
||||
};
|
||||
|
||||
// Contact API
|
||||
export const contactApi = {
|
||||
getAll: (params) => api.get('/contacts', params),
|
||||
getById: (id) => api.get(`/contacts/${id}`),
|
||||
create: (data) => api.post('/contacts', data),
|
||||
update: (id, data) => api.put(`/contacts/${id}`, data),
|
||||
delete: (id) => api.delete(`/contacts/${id}`),
|
||||
};
|
||||
|
||||
// Account API
|
||||
export const accountApi = {
|
||||
getAll: (params) => api.get('/accounts', params),
|
||||
getById: (id) => api.get(`/accounts/${id}`),
|
||||
create: (data) => api.post('/accounts', data),
|
||||
update: (id, data) => api.put(`/accounts/${id}`, data),
|
||||
delete: (id) => api.delete(`/accounts/${id}`),
|
||||
};
|
||||
|
||||
// Location API
|
||||
export const locationApi = {
|
||||
getAll: (params) => api.get('/locations', params),
|
||||
getById: (id) => api.get(`/locations/${id}`),
|
||||
create: (data) => api.post('/locations', data),
|
||||
update: (id, data) => api.put(`/locations/${id}`, data),
|
||||
delete: (id) => api.delete(`/locations/${id}`),
|
||||
};
|
||||
|
||||
// Work Order API
|
||||
export const workOrderApi = {
|
||||
getAll: (params) => api.get('/workorders', params),
|
||||
getById: (id) => api.get(`/workorders/${id}`),
|
||||
create: (data) => api.post('/workorders', data),
|
||||
update: (id, data) => api.put(`/workorders/${id}`, data),
|
||||
delete: (id) => api.delete(`/workorders/${id}`),
|
||||
};
|
||||
|
||||
// PM Schedule API
|
||||
export const pmScheduleApi = {
|
||||
getAll: (params) => api.get('/pmschedules', params),
|
||||
getById: (id) => api.get(`/pmschedules/${id}`),
|
||||
create: (data) => api.post('/pmschedules', data),
|
||||
update: (id, data) => api.put(`/pmschedules/${id}`, data),
|
||||
delete: (id) => api.delete(`/pmschedules/${id}`),
|
||||
};
|
||||
|
||||
// Follow-up API
|
||||
export const followupApi = {
|
||||
getAll: (params) => api.get('/followups', params),
|
||||
getById: (id) => api.get(`/followups/${id}`),
|
||||
create: (data) => api.post('/followups', data),
|
||||
update: (id, data) => api.put(`/followups/${id}`, data),
|
||||
delete: (id) => api.delete(`/followups/${id}`),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
177
src/services/authService.js
Normal file
177
src/services/authService.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
// Authentication Service
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || "http://localhost:5000";
|
||||
|
||||
/**
|
||||
* Login user with username and password
|
||||
* @param {string} username - User's email/username
|
||||
* @param {string} password - User's password
|
||||
* @returns {Promise<Object>} - Authentication response with token and user data
|
||||
*/
|
||||
export async function login(username, password) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Authentication/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
Username: username,
|
||||
Password: password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
throw new Error("Login failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user - clear local storage
|
||||
*/
|
||||
export function logout() {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAuthenticated() {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return false;
|
||||
|
||||
// Check if token is expired
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||
const expiration = new Date(payload.exp * 1000);
|
||||
return expiration > new Date();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user from local storage
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
export function getCurrentUser() {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (!userStr) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentication token
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function getToken() {
|
||||
return localStorage.getItem("token");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change user password
|
||||
* @param {string} currentPassword - Current password
|
||||
* @param {string} newPassword - New password
|
||||
* @param {string} confirmPassword - Confirm new password
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function changePassword(currentPassword, newPassword, confirmPassword) {
|
||||
const token = getToken();
|
||||
const response = await fetch(`${API_BASE_URL}/api/Authentication/ChangePassword`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
Currentpassword: currentPassword,
|
||||
Newpassword: newPassword,
|
||||
Confirmpassword: confirmPassword,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to change password");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Request password reset
|
||||
* @param {string} email - User's email
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function forgotPassword(email) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Authentication/ForgetPassword?Email=${encodeURIComponent(email)}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to send password reset email");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password reset code
|
||||
* @param {string} code - Verification code
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function verifyResetCode(code) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Authentication/VerificationCode?code=${encodeURIComponent(code)}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Invalid verification code");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password with code
|
||||
* @param {string} email - User's email
|
||||
* @param {string} code - Verification code
|
||||
* @param {string} password - New password
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function resetPassword(email, code, password) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/Authentication/ResetPassword`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
Email: email,
|
||||
Code: code,
|
||||
Password: password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to reset password");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue