mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 06:53:12 +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*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@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/icons-material": "^7.3.1",
|
||||||
"@mui/material": "^7.3.1",
|
"@mui/material": "^7.3.1",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
|
@ -2451,6 +2455,52 @@
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"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": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
@ -15646,16 +15706,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.2",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.17"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
|
|
@ -18332,6 +18392,41 @@
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
|
||||||
"integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="
|
"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": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"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": {
|
"prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
@ -27628,9 +27729,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "5.9.2",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"unbox-primitive": {
|
"unbox-primitive": {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@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/icons-material": "^7.3.1",
|
||||||
"@mui/material": "^7.3.1",
|
"@mui/material": "^7.3.1",
|
||||||
"@testing-library/dom": "^10.4.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 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 Sidebar from "./components/Sidebar";
|
||||||
import Topbar from "./components/Topbar";
|
import Topbar from "./components/Topbar";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
|
|
||||||
|
// AUTH
|
||||||
|
import LoginPage from "./pages/auth/LoginPage";
|
||||||
|
import { isAuthenticated } from "./services/authService";
|
||||||
|
|
||||||
// CONTACTS
|
// CONTACTS
|
||||||
import ContactsList from "./pages/contacts/list/List";
|
import ContactsList from "./pages/contacts/list/List";
|
||||||
import ContactFormPage from "./pages/contacts/form/ContactFormPage";
|
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 PMScheduleList from "./pages/PmSchedule/list/List";
|
||||||
import PMScheduleFormPage from "./pages/PmSchedule/form/PmScheduleFormPage";
|
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() {
|
export default function App() {
|
||||||
const [navCollapsed, setNavCollapsed] = useState(false);
|
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
|
<Routes>
|
||||||
<Sidebar />
|
{/* Public Routes */}
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
|
||||||
<div className="mainArea">
|
{/* Protected Routes */}
|
||||||
<Topbar onToggleNav={() => setNavCollapsed(v => !v)} />
|
<Route
|
||||||
|
path="/*"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
<div className="contentWrapper">
|
<div className="mainArea">
|
||||||
<Routes>
|
<Topbar onToggleNav={() => setNavCollapsed(v => !v)} />
|
||||||
<Route path="/" element={<Dashboard />} />
|
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
|
||||||
|
|
||||||
{/* CONTACTS */}
|
<div className="contentWrapper">
|
||||||
<Route path="/contacts" element={<ContactsList />} />
|
<Routes>
|
||||||
<Route path="/contacts/new" element={<ContactFormPage />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/contacts/edit/:id" element={<ContactFormPage />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
|
||||||
{/* ACCOUNTS */}
|
{/* CONTACTS */}
|
||||||
<Route path="/accounts" element={<AccountList />} />
|
<Route path="/contacts" element={<ContactsList />} />
|
||||||
<Route path="/accounts/new" element={<AccountFormPage />} />
|
<Route path="/contacts/new" element={<ContactFormPage />} />
|
||||||
<Route path="/accounts/edit/:id" element={<AccountFormPage />} />
|
<Route path="/contacts/edit/:id" element={<ContactFormPage />} />
|
||||||
|
|
||||||
{/* EMPLOYEES */}
|
{/* ACCOUNTS */}
|
||||||
<Route path="/employees" element={<EmployeeList />} />
|
<Route path="/accounts" element={<AccountList />} />
|
||||||
<Route path="/employees/new" element={<EmployeeFormPage />} />
|
<Route path="/accounts/new" element={<AccountFormPage />} />
|
||||||
<Route path="/employees/edit/:id" element={<EmployeeFormPage />} />
|
<Route path="/accounts/edit/:id" element={<AccountFormPage />} />
|
||||||
|
|
||||||
{/* LOCATIONS */}
|
{/* EMPLOYEES */}
|
||||||
<Route path="/locations" element={<LocationsList />} />
|
<Route path="/employees" element={<EmployeeList />} />
|
||||||
<Route path="/locations/new" element={<LocationFormPage />} />
|
<Route path="/employees/new" element={<EmployeeFormPage />} />
|
||||||
<Route path="/locations/edit/:id" element={<LocationFormPage />} />
|
<Route path="/employees/edit/:id" element={<EmployeeFormPage />} />
|
||||||
|
|
||||||
{/* WORK ORDERS */}
|
{/* LOCATIONS */}
|
||||||
<Route path="/workorders" element={<WorkOrdersList />} />
|
<Route path="/locations" element={<LocationsList />} />
|
||||||
<Route path="/workorders/new" element={<WorkOrderFormPage />} />
|
<Route path="/locations/new" element={<LocationFormPage />} />
|
||||||
<Route path="/workorders/edit/:id" element={<WorkOrderFormPage />} />
|
<Route path="/locations/edit/:id" element={<LocationFormPage />} />
|
||||||
|
|
||||||
{/* FOLLOW UPS */}
|
{/* WORK ORDERS */}
|
||||||
<Route path="/followups" element={<FollowUpList />} />
|
<Route path="/workorders" element={<WorkOrdersList />} />
|
||||||
<Route path="/followups/new" element={<FollowUpFormPage />} />
|
<Route path="/workorders/new" element={<WorkOrderFormPage />} />
|
||||||
<Route path="/followups/edit/:id" element={<FollowUpFormPage />} />
|
<Route path="/workorders/edit/:id" element={<WorkOrderFormPage />} />
|
||||||
|
|
||||||
{/* PM SCHEDULES */}
|
{/* FOLLOW UPS */}
|
||||||
<Route path="/pmschedules" element={<PMScheduleList />} />
|
<Route path="/followups" element={<FollowUpList />} />
|
||||||
<Route path="/pmschedules/new" element={<PMScheduleFormPage />} />
|
<Route path="/followups/new" element={<FollowUpFormPage />} />
|
||||||
<Route path="/pmschedules/edit/:id" element={<PMScheduleFormPage />} />
|
<Route path="/followups/edit/:id" element={<FollowUpFormPage />} />
|
||||||
</Routes>
|
|
||||||
</div>
|
{/* PM SCHEDULES */}
|
||||||
</div>
|
<Route path="/pmschedules" element={<PMScheduleList />} />
|
||||||
</div>
|
<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>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,25 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { logout, getCurrentUser } from "../services/authService";
|
||||||
|
|
||||||
export default function Topbar({ onToggleNav }) {
|
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 (
|
return (
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="topLeft">
|
<div className="topLeft">
|
||||||
|
|
@ -22,8 +41,23 @@ export default function Topbar({ onToggleNav }) {
|
||||||
<div className="notif">
|
<div className="notif">
|
||||||
🔔<span className="dot">1</span>
|
🔔<span className="dot">1</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="username">Adam Moussa</div>
|
<div className="username">{currentUser?.fullname || "User"}</div>
|
||||||
<div className="avatar">AM</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>
|
</div>
|
||||||
</header>
|
</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 { useParams, useNavigate } from "react-router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getPMScheduleById, addPMSchedule, editPMSchedule } from "../api";
|
import { getPMScheduleById, addPMSchedule, editPMSchedule } from "../api";
|
||||||
|
import "./PmScheduleFormPage.css";
|
||||||
|
|
||||||
export default function PmScheduleFormPage() {
|
export default function PmScheduleFormPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -72,164 +73,225 @@ export default function PmScheduleFormPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="formPage">
|
<div className="pmScheduleFormPage">
|
||||||
<div className="pageTitle">
|
<div className="panel panel-default panel-connected">
|
||||||
{isEdit ? "Edit PM Schedule" : "Create PM Schedule"}
|
<div className="panel-body">
|
||||||
</div>
|
<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="container">
|
||||||
<div className="card formCard">
|
<form onSubmit={handleSave}>
|
||||||
<div className="formBody">
|
{/* Page Header */}
|
||||||
{/* ASSET INFO SECTION */}
|
<div className="form-group">
|
||||||
<div className="formSection">
|
<div className="header">
|
||||||
<div className="sectionTitle">Asset Information</div>
|
<h3 className="heading thin">
|
||||||
<div className="formRow">
|
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
|
||||||
<div className="formGroup">
|
<i className="fa fa-square fa-stack-2x text-primary"></i>
|
||||||
<label>Asset ID *</label>
|
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
|
||||||
<input
|
</span>
|
||||||
type="text"
|
<span>{isEdit ? "Edit a PM Schedule" : "Create a PM Schedule"}</span>
|
||||||
name="asset"
|
</h3>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SCHEDULE INFO SECTION */}
|
{/* Asset Information Section */}
|
||||||
<div className="formSection">
|
<div className="form-group">
|
||||||
<div className="sectionTitle">Schedule Information</div>
|
<div className="header">
|
||||||
<div className="formRow">
|
<h5 className="heading text-uppercase">
|
||||||
<div className="formGroup">
|
<strong>Asset Information</strong>
|
||||||
<label>Frequency *</label>
|
</h5>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="formGroup">
|
<div className="row">
|
||||||
<label>Last Service Date *</label>
|
<div className="col-sm-6">
|
||||||
<input
|
<div className="form-group">
|
||||||
type="date"
|
<label className="required-field" htmlFor="asset">Asset ID</label>
|
||||||
name="lastServiceDate"
|
<input
|
||||||
value={formData.lastServiceDate}
|
className="form-control"
|
||||||
onChange={handleChange}
|
type="text"
|
||||||
required
|
id="asset"
|
||||||
/>
|
name="asset"
|
||||||
</div>
|
value={formData.asset}
|
||||||
<div className="formGroup">
|
onChange={handleChange}
|
||||||
<label>Next Service Date *</label>
|
placeholder="e.g., HVAC-001"
|
||||||
<input
|
required
|
||||||
type="date"
|
/>
|
||||||
name="nextServiceDate"
|
</div>
|
||||||
value={formData.nextServiceDate}
|
</div>
|
||||||
onChange={handleChange}
|
<div className="col-sm-6">
|
||||||
required
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ASSIGNMENT SECTION */}
|
{/* Schedule Information Section */}
|
||||||
<div className="formSection">
|
<div className="form-group">
|
||||||
<div className="sectionTitle">Assignment</div>
|
<div className="header">
|
||||||
<div className="formRow">
|
<h5 className="heading text-uppercase">
|
||||||
<div className="formGroup">
|
<strong>Schedule Information</strong>
|
||||||
<label>Assigned To</label>
|
</h5>
|
||||||
<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 className="formGroup">
|
<div className="row">
|
||||||
<label>Status *</label>
|
<div className="col-sm-4">
|
||||||
<select
|
<div className="form-group">
|
||||||
name="status"
|
<label className="required-field" htmlFor="frequency">Frequency</label>
|
||||||
value={formData.status}
|
<select
|
||||||
onChange={handleChange}
|
className="form-control"
|
||||||
required
|
id="frequency"
|
||||||
>
|
name="frequency"
|
||||||
<option value="Active">Active</option>
|
value={formData.frequency}
|
||||||
<option value="Inactive">Inactive</option>
|
onChange={handleChange}
|
||||||
<option value="Suspended">Suspended</option>
|
required
|
||||||
</select>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* NOTES SECTION */}
|
{/* Assignment Section */}
|
||||||
<div className="formSection">
|
<div className="form-group">
|
||||||
<div className="sectionTitle">Notes</div>
|
<div className="header">
|
||||||
<div className="formRow">
|
<h5 className="heading text-uppercase">
|
||||||
<div className="formGroup fullWidth">
|
<strong>Assignment</strong>
|
||||||
<label>Notes</label>
|
</h5>
|
||||||
<textarea
|
</div>
|
||||||
name="notes"
|
<div className="row">
|
||||||
value={formData.notes}
|
<div className="col-sm-6">
|
||||||
onChange={handleChange}
|
<div className="form-group">
|
||||||
placeholder="PM schedule notes"
|
<label htmlFor="assignedTo">Assigned To</label>
|
||||||
rows="4"
|
<select
|
||||||
style={{
|
className="form-control"
|
||||||
width: "100%",
|
id="assignedTo"
|
||||||
padding: "10px",
|
name="assignedTo"
|
||||||
borderRadius: "4px",
|
value={formData.assignedTo}
|
||||||
border: "1px solid #ddd",
|
onChange={handleChange}
|
||||||
fontFamily: "inherit",
|
>
|
||||||
}}
|
<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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ACTION BUTTONS */}
|
{/* Notes Section */}
|
||||||
<div className="formActions">
|
<div className="form-group">
|
||||||
<button
|
<div className="header">
|
||||||
type="button"
|
<h5 className="heading text-uppercase">
|
||||||
className="btnSecondary"
|
<strong>Notes</strong>
|
||||||
onClick={handleCancel}
|
</h5>
|
||||||
disabled={loading}
|
</div>
|
||||||
>
|
<div className="row">
|
||||||
Cancel
|
<div className="col-sm-12">
|
||||||
</button>
|
<div className="form-group">
|
||||||
<button
|
<label htmlFor="notes">Notes</label>
|
||||||
type="submit"
|
<textarea
|
||||||
className="btnPrimary"
|
className="form-control"
|
||||||
disabled={loading}
|
id="notes"
|
||||||
>
|
name="notes"
|
||||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
value={formData.notes}
|
||||||
</button>
|
onChange={handleChange}
|
||||||
|
rows="4"
|
||||||
|
placeholder="PM schedule notes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form Actions */}
|
||||||
|
<div className="form-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-default"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,23 @@ export const fetchPmSchedulesPaged = async ({
|
||||||
pageSize = 12,
|
pageSize = 12,
|
||||||
}) => {
|
}) => {
|
||||||
const res = await get(
|
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 => ({
|
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||||
id: x.Id,
|
id: x.id ?? x.Id,
|
||||||
name: x.Name,
|
name: x.name ?? x.Name ?? "",
|
||||||
trade: x.Trade,
|
trade: x.trade ?? x.Trade ?? "",
|
||||||
status: x.Status,
|
status: x.status ?? x.Status ?? "",
|
||||||
nextWorkOrderDate: x.NextWorkOrderDate,
|
nextWorkOrderDate: x.nextWorkOrderDate ?? x.NextWorkOrderDate ?? "",
|
||||||
owner: x.Owner,
|
owner: x.owner ?? x.Owner ?? "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
rows,
|
||||||
page: res.PageNumber,
|
page: res?.pageNumber ?? res?.PageNumber ?? 1,
|
||||||
pageSize: res.PageSize,
|
pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
|
||||||
totalCount: res.TotalCount,
|
totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
|
||||||
totalPages: res.TotalPages,
|
totalPages: res?.totalPages ?? res?.TotalPages ?? 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { fetchContactsPaged } from "./api";
|
import { fetchPmSchedulesPaged } from "./api";
|
||||||
|
|
||||||
export default function useItems() {
|
export default function useItems() {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
|
@ -13,7 +13,7 @@ export default function useItems() {
|
||||||
const reload = useCallback(() => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr("");
|
setErr("");
|
||||||
fetchContactsPaged({ search, page, pageSize })
|
fetchPmSchedulesPaged({ search, page, pageSize })
|
||||||
.then(({ rows, totalCount }) => {
|
.then(({ rows, totalCount }) => {
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
setTotalCount(totalCount);
|
setTotalCount(totalCount);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { getPMSchedules } from "../api";
|
import { fetchPmSchedulesPaged } from "./api";
|
||||||
|
|
||||||
const usePMSchedules = () => {
|
const usePMSchedules = () => {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
|
@ -7,35 +7,41 @@ const usePMSchedules = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(12);
|
const [pageSize, setPageSize] = useState(12);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setErr("");
|
||||||
const pmschedules = await getPMSchedules();
|
fetchPmSchedulesPaged({ search, page, pageSize })
|
||||||
let filtered = pmschedules;
|
.then(({ rows, totalCount }) => {
|
||||||
if (search) {
|
setRows(rows);
|
||||||
filtered = pmschedules.filter((p) =>
|
setTotalCount(totalCount);
|
||||||
p.asset.toLowerCase().includes(search.toLowerCase()) ||
|
})
|
||||||
p.frequency.toLowerCase().includes(search.toLowerCase())
|
.catch((e) => setErr(e.message || "Failed to load"))
|
||||||
);
|
.finally(() => setLoading(false));
|
||||||
}
|
}, [search, page, pageSize]);
|
||||||
setTotalCount(filtered.length);
|
|
||||||
const start = (page - 1) * pageSize;
|
|
||||||
setRows(filtered.slice(start, start + pageSize));
|
|
||||||
} catch (error) {
|
|
||||||
setErr(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload();
|
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;
|
export default usePMSchedules;
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,49 @@
|
||||||
// Mock accounts data
|
import { accountApi } from "../../services/api";
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getAccounts = async () => {
|
export const getAccounts = async (params = {}) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
try {
|
||||||
return [...mockAccounts];
|
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) => {
|
export const getAccountById = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
try {
|
||||||
return mockAccounts.find(a => a.id === parseInt(id));
|
return await accountApi.getById(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching account:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addAccount = async (account) => {
|
export const addAccount = async (account) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const newAccount = { id: mockAccounts.length + 1, ...account };
|
return await accountApi.create(account);
|
||||||
mockAccounts.push(newAccount);
|
} catch (error) {
|
||||||
return newAccount;
|
console.error("Error creating account:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editAccount = async (account) => {
|
export const editAccount = async (account) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockAccounts.findIndex(a => a.id === account.id);
|
return await accountApi.update(account.id, account);
|
||||||
if (idx >= 0) mockAccounts[idx] = account;
|
} catch (error) {
|
||||||
return account;
|
console.error("Error updating account:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAccount = async (id) => {
|
export const deleteAccount = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockAccounts.findIndex(a => a.id === id);
|
await accountApi.delete(id);
|
||||||
if (idx >= 0) mockAccounts.splice(idx, 1);
|
return true;
|
||||||
return true;
|
} catch (error) {
|
||||||
|
console.error("Error deleting account:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,9 @@
|
||||||
import { get } from "../../../apiUtil";
|
import { get } from "../../../apiUtil";
|
||||||
import { API_URL } from "../../../constants";
|
import { API_URL } from "../../../constants";
|
||||||
|
|
||||||
export const fetchAccountsPaged = async ({
|
export const fetchAccountsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
|
||||||
search = "",
|
const res = await get(`${API_URL}/Account/GetAccountList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
|
||||||
page = 1,
|
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||||
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) => ({
|
|
||||||
id: x.id ?? x.Id,
|
id: x.id ?? x.Id,
|
||||||
name: x.name ?? x.Name ?? x.AccountName ?? "",
|
name: x.name ?? x.Name ?? x.AccountName ?? "",
|
||||||
number: x.number ?? x.AccountNumber ?? "",
|
number: x.number ?? x.AccountNumber ?? "",
|
||||||
|
|
@ -24,13 +13,12 @@ export const fetchAccountsPaged = async ({
|
||||||
phoneNumber: x.phoneNumber ?? x.Phone ?? x.PhoneNumber ?? "",
|
phoneNumber: x.phoneNumber ?? x.Phone ?? x.PhoneNumber ?? "",
|
||||||
owner: x.owner ?? x.Owner ?? "",
|
owner: x.owner ?? x.Owner ?? "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
rows,
|
||||||
page: res?.page ?? res?.PageNumber ?? page,
|
page: res?.pageNumber ?? 1,
|
||||||
pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
|
pageSize: res?.pageSize ?? pageSize,
|
||||||
totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
|
totalCount: res?.totalCount ?? rows.length,
|
||||||
totalPages: res?.totalPages ?? res?.TotalPages ?? 1,
|
totalPages: res?.totalPages ?? 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,31 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { getAccounts } from "../api";
|
import { fetchAccountsPaged } from "./api";
|
||||||
|
|
||||||
const useAccounts = () => {
|
export default function useAccounts() {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(12);
|
const [pageSize, setPageSize] = useState(12);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setErr("");
|
||||||
const allAccounts = await getAccounts();
|
fetchAccountsPaged({ search, page, pageSize })
|
||||||
let filtered = allAccounts;
|
.then(({ rows, totalCount }) => {
|
||||||
if (search) {
|
setRows(rows);
|
||||||
filtered = allAccounts.filter((a) =>
|
setTotalCount(totalCount);
|
||||||
a.name.toLowerCase().includes(search.toLowerCase()) ||
|
})
|
||||||
a.email.toLowerCase().includes(search.toLowerCase()) ||
|
.catch((e) => setErr(e.message || "Failed to load"))
|
||||||
a.phone.includes(search)
|
.finally(() => setLoading(false));
|
||||||
);
|
|
||||||
}
|
|
||||||
setTotalCount(filtered.length);
|
|
||||||
const start = (page - 1) * pageSize;
|
|
||||||
setRows(filtered.slice(start, start + pageSize));
|
|
||||||
} catch (error) {
|
|
||||||
setErr(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload();
|
|
||||||
}, [search, page, pageSize]);
|
}, [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
|
import { employeeApi } from "../../services/api";
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getEmployees = async () => {
|
// Transform backend data to frontend format
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
const transformEmployee = (employee) => {
|
||||||
return [...mockEmployees];
|
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) => {
|
export const getEmployeeById = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
try {
|
||||||
return mockEmployees.find(e => e.id === parseInt(id));
|
const employee = await employeeApi.getById(id);
|
||||||
|
return transformEmployee(employee);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching employee:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addEmployee = async (employee) => {
|
export const addEmployee = async (employee) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const newEmployee = { id: mockEmployees.length + 1, ...employee };
|
const backendData = transformToBackend(employee);
|
||||||
mockEmployees.push(newEmployee);
|
const created = await employeeApi.create(backendData);
|
||||||
return newEmployee;
|
return transformEmployee(created);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating employee:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editEmployee = async (employee) => {
|
export const editEmployee = async (employee) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockEmployees.findIndex(e => e.id === employee.id);
|
const backendData = transformToBackend(employee);
|
||||||
if (idx >= 0) mockEmployees[idx] = employee;
|
const updated = await employeeApi.update(employee.id, backendData);
|
||||||
return employee;
|
return transformEmployee(updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating employee:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteEmployee = async (id) => {
|
export const deleteEmployee = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockEmployees.findIndex(e => e.id === id);
|
await employeeApi.delete(id);
|
||||||
if (idx >= 0) mockEmployees.splice(idx, 1);
|
return true;
|
||||||
return true;
|
} catch (error) {
|
||||||
|
console.error("Error deleting employee:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,23 @@
|
||||||
import { get } from "../../../apiUtil";
|
import { get } from "../../../apiUtil";
|
||||||
import { API_URL } from "../../../constants";
|
import { API_URL } from "../../../constants";
|
||||||
|
|
||||||
export const fetchEmployeesPaged = async ({
|
export const fetchEmployeesPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
|
||||||
search = "",
|
const res = await get(`${API_URL}/Employee/GetEmployeeList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
|
||||||
page = 1,
|
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||||
pageSize = 12,
|
|
||||||
}) => {
|
|
||||||
const res = await get(
|
|
||||||
`${API_URL}/Employee/GetEmployeeList?search=${encodeURIComponent(
|
|
||||||
search
|
|
||||||
)}&page=${page}&pageSize=${pageSize}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const rows = (res || []).map((x) => ({
|
|
||||||
id: x.id,
|
id: x.id,
|
||||||
name: `${x.name ?? ""} ${x.name ?? ""}`.trim(),
|
name: `${x.firstName ?? ""} ${x.lastName ?? ""}`.trim(),
|
||||||
jobTitle: x.jobTitle ?? "",
|
jobTitle: x.jobTitle ?? "",
|
||||||
email: x.email ?? "",
|
email: x.email ?? "",
|
||||||
phone: x.phone ?? "",
|
phone: x.phone ?? "",
|
||||||
department: x.department ?? "",
|
department: x.department ?? "",
|
||||||
status: x.status ?? "",
|
status: x.status ?? "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
rows,
|
||||||
page: res?.PageNumber ?? 1,
|
page: res?.pageNumber ?? 1,
|
||||||
pageSize: res?.PageSize ?? pageSize,
|
pageSize: res?.pageSize ?? pageSize,
|
||||||
totalCount: res?.TotalCount ?? rows.length,
|
totalCount: res?.totalCount ?? rows.length,
|
||||||
totalPages: res?.TotalPages ?? 1,
|
totalPages: res?.totalPages ?? 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,31 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { getEmployees } from "../api";
|
import { fetchEmployeesPaged } from "./api";
|
||||||
|
|
||||||
const useEmployees = () => {
|
export default function useEmployees() {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(12);
|
const [pageSize, setPageSize] = useState(12);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setErr("");
|
||||||
const employees = await getEmployees();
|
fetchEmployeesPaged({ search, page, pageSize })
|
||||||
let filtered = employees;
|
.then(({ rows, totalCount }) => {
|
||||||
if (search) {
|
setRows(rows);
|
||||||
filtered = employees.filter((e) =>
|
setTotalCount(totalCount);
|
||||||
e.name.toLowerCase().includes(search.toLowerCase()) ||
|
})
|
||||||
e.email.toLowerCase().includes(search.toLowerCase()) ||
|
.catch((e) => setErr(e.message || "Failed to load"))
|
||||||
e.phone.includes(search)
|
.finally(() => setLoading(false));
|
||||||
);
|
|
||||||
}
|
|
||||||
setTotalCount(filtered.length);
|
|
||||||
const start = (page - 1) * pageSize;
|
|
||||||
setRows(filtered.slice(start, start + pageSize));
|
|
||||||
} catch (error) {
|
|
||||||
setErr(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload();
|
|
||||||
}, [search, page, pageSize]);
|
}, [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 { useParams, useNavigate } from "react-router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getFollowUpById, addFollowUp, editFollowUp } from "../api";
|
import { getFollowUpById, addFollowUp, editFollowUp } from "../api";
|
||||||
|
import "./FollowUpFormPage.css";
|
||||||
|
|
||||||
export default function FollowUpFormPage() {
|
export default function FollowUpFormPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -71,149 +72,202 @@ export default function FollowUpFormPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="formPage">
|
<div className="followUpFormPage">
|
||||||
<div className="pageTitle">
|
<div className="panel panel-default panel-connected">
|
||||||
{isEdit ? "Edit Follow-up" : "Create Follow-up"}
|
<div className="panel-body">
|
||||||
</div>
|
<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="container">
|
||||||
<div className="card formCard">
|
<form onSubmit={handleSave}>
|
||||||
<div className="formBody">
|
{/* Page Header */}
|
||||||
{/* BASIC INFO SECTION */}
|
<div className="form-group">
|
||||||
<div className="formSection">
|
<div className="header">
|
||||||
<div className="sectionTitle">Follow-up Details</div>
|
<h3 className="heading thin">
|
||||||
<div className="formRow">
|
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
|
||||||
<div className="formGroup">
|
<i className="fa fa-square fa-stack-2x text-primary"></i>
|
||||||
<label>Work Order *</label>
|
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
|
||||||
<select
|
</span>
|
||||||
name="workOrder"
|
<span>{isEdit ? "Edit a Follow-up" : "Create a Follow-up"}</span>
|
||||||
value={formData.workOrder}
|
</h3>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
{/* Follow-up Details Section */}
|
||||||
<div className="formGroup">
|
<div className="form-group">
|
||||||
<label>Scheduled Date *</label>
|
<div className="header">
|
||||||
<input
|
<h5 className="heading text-uppercase">
|
||||||
type="date"
|
<strong>Follow-up Details</strong>
|
||||||
name="scheduledDate"
|
</h5>
|
||||||
value={formData.scheduledDate}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="formGroup">
|
<div className="row">
|
||||||
<label>Status *</label>
|
<div className="col-sm-6">
|
||||||
<select
|
<div className="form-group">
|
||||||
name="status"
|
<label className="required-field" htmlFor="workOrder">Work Order</label>
|
||||||
value={formData.status}
|
<select
|
||||||
onChange={handleChange}
|
className="form-control"
|
||||||
required
|
id="workOrder"
|
||||||
>
|
name="workOrder"
|
||||||
<option value="Pending">Pending</option>
|
value={formData.workOrder}
|
||||||
<option value="In Progress">In Progress</option>
|
onChange={handleChange}
|
||||||
<option value="Completed">Completed</option>
|
required
|
||||||
<option value="Cancelled">Cancelled</option>
|
>
|
||||||
</select>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CREATOR INFO SECTION */}
|
{/* Creator Information Section */}
|
||||||
<div className="formSection">
|
<div className="form-group">
|
||||||
<div className="sectionTitle">Creator Information</div>
|
<div className="header">
|
||||||
<div className="formRow">
|
<h5 className="heading text-uppercase">
|
||||||
<div className="formGroup">
|
<strong>Creator Information</strong>
|
||||||
<label>Created By *</label>
|
</h5>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="formGroup">
|
<div className="row">
|
||||||
<label>Created Date *</label>
|
<div className="col-sm-6">
|
||||||
<input
|
<div className="form-group">
|
||||||
type="date"
|
<label className="required-field" htmlFor="createdBy">Created By</label>
|
||||||
name="createdDate"
|
<select
|
||||||
value={formData.createdDate}
|
className="form-control"
|
||||||
onChange={handleChange}
|
id="createdBy"
|
||||||
required
|
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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* NOTES SECTION */}
|
{/* Notes Section */}
|
||||||
<div className="formSection">
|
<div className="form-group">
|
||||||
<div className="sectionTitle">Notes</div>
|
<div className="header">
|
||||||
<div className="formRow">
|
<h5 className="heading text-uppercase">
|
||||||
<div className="formGroup fullWidth">
|
<strong>Notes</strong>
|
||||||
<label>Notes</label>
|
</h5>
|
||||||
<textarea
|
</div>
|
||||||
name="notes"
|
<div className="row">
|
||||||
value={formData.notes}
|
<div className="col-sm-12">
|
||||||
onChange={handleChange}
|
<div className="form-group">
|
||||||
placeholder="Additional notes or details"
|
<label htmlFor="notes">Notes</label>
|
||||||
rows="5"
|
<textarea
|
||||||
style={{
|
className="form-control"
|
||||||
width: "100%",
|
id="notes"
|
||||||
padding: "10px",
|
name="notes"
|
||||||
borderRadius: "4px",
|
value={formData.notes}
|
||||||
border: "1px solid #ddd",
|
onChange={handleChange}
|
||||||
fontFamily: "inherit",
|
rows="5"
|
||||||
}}
|
placeholder="Additional notes or details"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ACTION BUTTONS */}
|
{/* Form Actions */}
|
||||||
<div className="formActions">
|
<div className="form-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btnSecondary"
|
className="btn btn-default"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btnPrimary"
|
className="btn btn-primary"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/pages/followups/followUpApi.js
|
// src/pages/followups/followUpApi.js
|
||||||
import { get, post } from "../../apiUtil";
|
import { get, post } from "../../../apiUtil";
|
||||||
import { API_URL } from "../../constants";
|
import { API_URL } from "../../../constants";
|
||||||
|
|
||||||
export const fetchFollowUpsPaged = async ({
|
export const fetchFollowUpsPaged = async ({
|
||||||
search = "",
|
search = "",
|
||||||
|
|
@ -9,30 +9,30 @@ export const fetchFollowUpsPaged = async ({
|
||||||
status = "Open"
|
status = "Open"
|
||||||
}) => {
|
}) => {
|
||||||
const res = await get(
|
const res = await get(
|
||||||
`${API_URL}/FollowUp/GetFollowUps?search=${encodeURIComponent(
|
`${API_URL}/FollowUp/GetFollowUpList?search=${encodeURIComponent(
|
||||||
search
|
search
|
||||||
)}&page=${page}&pageSize=${pageSize}&status=${status}`
|
)}&page=${page}&pageSize=${pageSize}&status=${status}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const rows = (res?.Data || []).map(x => ({
|
const rows = (res?.data || res?.Data || []).map(x => ({
|
||||||
id: x.Id,
|
id: x.id ?? x.Id,
|
||||||
reason: x.Reason,
|
reason: x.reason ?? x.Reason ?? "",
|
||||||
scheduleDate: x.ScheduleDate,
|
scheduleDate: x.scheduleDate ?? x.ScheduleDate ?? "",
|
||||||
scheduleTime: x.ScheduleStartTime,
|
scheduleTime: x.scheduleStartTime ?? x.ScheduleStartTime ?? "",
|
||||||
account: x.AccountName,
|
account: x.accountName ?? x.AccountName ?? "",
|
||||||
assignedTo: x.EmployeeName,
|
assignedTo: x.employeeName ?? x.EmployeeName ?? "",
|
||||||
locationNumber: x.LocationNumber,
|
locationNumber: x.locationNumber ?? x.LocationNumber ?? "",
|
||||||
locationName: x.LocationName,
|
locationName: x.locationName ?? x.LocationName ?? "",
|
||||||
workOrderCode: x.WorkOrderCode,
|
workOrderCode: x.workOrderCode ?? x.WorkOrderCode ?? "",
|
||||||
status: x.Status
|
status: x.status ?? x.Status ?? ""
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
rows,
|
||||||
page: res.PageNumber,
|
page: res?.pageNumber ?? res?.PageNumber ?? 1,
|
||||||
pageSize: res.PageSize,
|
pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
|
||||||
totalCount: res.TotalCount,
|
totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
|
||||||
totalPages: res.TotalPages
|
totalPages: res?.totalPages ?? res?.TotalPages ?? 1
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { getFollowUps } from "../api";
|
import { fetchFollowUpsPaged } from "./api";
|
||||||
|
|
||||||
const useFollowUps = () => {
|
const useFollowUps = () => {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
|
@ -7,35 +7,41 @@ const useFollowUps = () => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(12);
|
const [pageSize, setPageSize] = useState(12);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setErr("");
|
||||||
const followups = await getFollowUps();
|
fetchFollowUpsPaged({ search, page, pageSize, status: "Open" })
|
||||||
let filtered = followups;
|
.then(({ rows, totalCount }) => {
|
||||||
if (search) {
|
setRows(rows);
|
||||||
filtered = followups.filter((f) =>
|
setTotalCount(totalCount);
|
||||||
f.workOrder.toLowerCase().includes(search.toLowerCase()) ||
|
})
|
||||||
f.reason.toLowerCase().includes(search.toLowerCase())
|
.catch((e) => setErr(e.message || "Failed to load"))
|
||||||
);
|
.finally(() => setLoading(false));
|
||||||
}
|
}, [search, page, pageSize]);
|
||||||
setTotalCount(filtered.length);
|
|
||||||
const start = (page - 1) * pageSize;
|
|
||||||
setRows(filtered.slice(start, start + pageSize));
|
|
||||||
} catch (error) {
|
|
||||||
setErr(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload();
|
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;
|
export default useFollowUps;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { fetchContactsPaged } from "./api";
|
import { fetchFollowUpsPaged } from "./api";
|
||||||
|
|
||||||
export default function useItems() {
|
export default function useItems() {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
|
|
@ -13,7 +13,7 @@ export default function useItems() {
|
||||||
const reload = useCallback(() => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr("");
|
setErr("");
|
||||||
fetchContactsPaged({ search, page, pageSize })
|
fetchFollowUpsPaged({ search, page, pageSize, status: "Open" })
|
||||||
.then(({ rows, totalCount }) => {
|
.then(({ rows, totalCount }) => {
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
setTotalCount(totalCount);
|
setTotalCount(totalCount);
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,49 @@
|
||||||
// Mock locations data
|
import { locationApi } from "../../services/api";
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getLocations = async () => {
|
export const getLocations = async (params = {}) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
try {
|
||||||
return [...mockLocations];
|
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) => {
|
export const getLocationById = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
try {
|
||||||
return mockLocations.find(l => l.id === parseInt(id));
|
return await locationApi.getById(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching location:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addLocation = async (location) => {
|
export const addLocation = async (location) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const newLocation = { id: mockLocations.length + 1, ...location };
|
return await locationApi.create(location);
|
||||||
mockLocations.push(newLocation);
|
} catch (error) {
|
||||||
return newLocation;
|
console.error("Error creating location:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editLocation = async (location) => {
|
export const editLocation = async (location) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockLocations.findIndex(l => l.id === location.id);
|
return await locationApi.update(location.id, location);
|
||||||
if (idx >= 0) mockLocations[idx] = location;
|
} catch (error) {
|
||||||
return location;
|
console.error("Error updating location:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteLocation = async (id) => {
|
export const deleteLocation = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockLocations.findIndex(l => l.id === id);
|
await locationApi.delete(id);
|
||||||
if (idx >= 0) mockLocations.splice(idx, 1);
|
return true;
|
||||||
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 { useParams, useNavigate } from "react-router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getLocationById, addLocation, editLocation } from "../api";
|
import { getLocationById, addLocation, editLocation } from "../api";
|
||||||
|
import "./LocationFormPage.css";
|
||||||
|
|
||||||
export default function LocationFormPage() {
|
export default function LocationFormPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -74,180 +75,241 @@ export default function LocationFormPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="formPage">
|
<div className="locationFormPage">
|
||||||
<div className="pageTitle">
|
<div className="panel panel-default panel-connected">
|
||||||
{isEdit ? "Edit Location" : "Create Location"}
|
<div className="panel-body">
|
||||||
</div>
|
<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="container">
|
||||||
<div className="card formCard">
|
<form onSubmit={handleSave}>
|
||||||
<div className="formBody">
|
{/* Page Header */}
|
||||||
{/* BASIC INFO SECTION */}
|
<div className="form-group">
|
||||||
<div className="formSection">
|
<div className="header">
|
||||||
<div className="sectionTitle">Location Information</div>
|
<h3 className="heading thin">
|
||||||
<div className="formRow">
|
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
|
||||||
<div className="formGroup">
|
<i className="fa fa-square fa-stack-2x text-primary"></i>
|
||||||
<label>Name *</label>
|
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
|
||||||
<input
|
</span>
|
||||||
type="text"
|
<span>{isEdit ? "Edit a Location" : "Create a Location"}</span>
|
||||||
name="name"
|
</h3>
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
{/* Location Information Section */}
|
||||||
<div className="formGroup">
|
<div className="form-group">
|
||||||
<label>Address *</label>
|
<div className="header">
|
||||||
<input
|
<h5 className="heading text-uppercase">
|
||||||
type="text"
|
<strong>Location Information</strong>
|
||||||
name="address"
|
</h5>
|
||||||
value={formData.address}
|
</div>
|
||||||
onChange={handleChange}
|
<div className="row">
|
||||||
required
|
<div className="col-sm-6">
|
||||||
placeholder="Street address"
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
{/* Contact Information Section */}
|
||||||
<div className="formGroup">
|
<div className="form-group">
|
||||||
<label>City *</label>
|
<div className="header">
|
||||||
<input
|
<h5 className="heading text-uppercase">
|
||||||
type="text"
|
<strong>Contact Information</strong>
|
||||||
name="city"
|
</h5>
|
||||||
value={formData.city}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
placeholder="City"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="formGroup">
|
<div className="row">
|
||||||
<label>State *</label>
|
<div className="col-sm-6">
|
||||||
<select
|
<div className="form-group">
|
||||||
name="state"
|
<label className="required-field" htmlFor="phone">Phone</label>
|
||||||
value={formData.state}
|
<input
|
||||||
onChange={handleChange}
|
className="form-control"
|
||||||
required
|
type="tel"
|
||||||
>
|
id="phone"
|
||||||
<option value="IL">Illinois</option>
|
name="phone"
|
||||||
<option value="IN">Indiana</option>
|
value={formData.phone}
|
||||||
<option value="MI">Michigan</option>
|
onChange={handleChange}
|
||||||
<option value="OH">Ohio</option>
|
placeholder="(555) 000-0000"
|
||||||
<option value="WI">Wisconsin</option>
|
required
|
||||||
</select>
|
/>
|
||||||
|
</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>
|
||||||
<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="row">
|
||||||
<div className="formSection">
|
<div className="col-sm-6">
|
||||||
<div className="sectionTitle">Contact Information</div>
|
<div className="form-group">
|
||||||
<div className="formRow">
|
<label className="required-field" htmlFor="contactEmail">Contact Email</label>
|
||||||
<div className="formGroup">
|
<input
|
||||||
<label>Phone *</label>
|
className="form-control"
|
||||||
<input
|
type="email"
|
||||||
type="tel"
|
id="contactEmail"
|
||||||
name="phone"
|
name="contactEmail"
|
||||||
value={formData.phone}
|
value={formData.contactEmail}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required
|
placeholder="email@company.com"
|
||||||
placeholder="(555) 000-0000"
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="formGroup">
|
</div>
|
||||||
<label>Contact Person *</label>
|
<div className="col-sm-6">
|
||||||
<input
|
<div className="form-group">
|
||||||
type="text"
|
<label className="required-field" htmlFor="status">Status</label>
|
||||||
name="contact"
|
<select
|
||||||
value={formData.contact}
|
className="form-control"
|
||||||
onChange={handleChange}
|
id="status"
|
||||||
required
|
name="status"
|
||||||
placeholder="Full name"
|
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>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
{/* Form Actions */}
|
||||||
<div className="formGroup">
|
<div className="form-actions">
|
||||||
<label>Contact Email *</label>
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="email"
|
className="btn btn-default"
|
||||||
name="contactEmail"
|
onClick={handleCancel}
|
||||||
value={formData.contactEmail}
|
disabled={loading}
|
||||||
onChange={handleChange}
|
>
|
||||||
required
|
Cancel
|
||||||
placeholder="email@company.com"
|
</button>
|
||||||
/>
|
<button
|
||||||
</div>
|
type="submit"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</section>
|
</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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { getLocations } from "../api";
|
import { fetchLocationsPaged } from "./api";
|
||||||
|
|
||||||
const useLocations = () => {
|
export default function useLocations() {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(12);
|
const [pageSize, setPageSize] = useState(12);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setErr("");
|
||||||
const locations = await getLocations();
|
fetchLocationsPaged({ search, page, pageSize })
|
||||||
let filtered = locations;
|
.then(({ rows, totalCount }) => {
|
||||||
if (search) {
|
setRows(rows);
|
||||||
filtered = locations.filter((l) =>
|
setTotalCount(totalCount);
|
||||||
l.name.toLowerCase().includes(search.toLowerCase()) ||
|
})
|
||||||
l.city.toLowerCase().includes(search.toLowerCase()) ||
|
.catch((e) => setErr(e.message || "Failed to load"))
|
||||||
l.phone.includes(search)
|
.finally(() => setLoading(false));
|
||||||
);
|
|
||||||
}
|
|
||||||
setTotalCount(filtered.length);
|
|
||||||
const start = (page - 1) * pageSize;
|
|
||||||
setRows(filtered.slice(start, start + pageSize));
|
|
||||||
} catch (error) {
|
|
||||||
setErr(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload();
|
|
||||||
}, [search, page, pageSize]);
|
}, [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
|
import { workOrderApi } from "../../services/api";
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const getWorkOrders = async () => {
|
export const getWorkOrders = async (params = {}) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
try {
|
||||||
return [...mockWorkOrders];
|
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) => {
|
export const getWorkOrderById = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
try {
|
||||||
return mockWorkOrders.find(w => w.id === parseInt(id));
|
return await workOrderApi.getById(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching work order:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addWorkOrder = async (workOrder) => {
|
export const addWorkOrder = async (workOrder) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const newWorkOrder = { id: mockWorkOrders.length + 1, ...workOrder };
|
return await workOrderApi.create(workOrder);
|
||||||
mockWorkOrders.push(newWorkOrder);
|
} catch (error) {
|
||||||
return newWorkOrder;
|
console.error("Error creating work order:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editWorkOrder = async (workOrder) => {
|
export const editWorkOrder = async (workOrder) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockWorkOrders.findIndex(w => w.id === workOrder.id);
|
return await workOrderApi.update(workOrder.id, workOrder);
|
||||||
if (idx >= 0) mockWorkOrders[idx] = workOrder;
|
} catch (error) {
|
||||||
return workOrder;
|
console.error("Error updating work order:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteWorkOrder = async (id) => {
|
export const deleteWorkOrder = async (id) => {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
try {
|
||||||
const idx = mockWorkOrders.findIndex(w => w.id === id);
|
await workOrderApi.delete(id);
|
||||||
if (idx >= 0) mockWorkOrders.splice(idx, 1);
|
return true;
|
||||||
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 { useParams, useNavigate } from "react-router";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { getWorkOrderById, addWorkOrder, editWorkOrder } from "../api";
|
import { getWorkOrderById, addWorkOrder, editWorkOrder } from "../api";
|
||||||
|
import "./WorkOrderFormPage.css";
|
||||||
|
|
||||||
export default function WorkOrderFormPage() {
|
export default function WorkOrderFormPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
@ -74,189 +75,254 @@ export default function WorkOrderFormPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="formPage">
|
<div className="workOrderFormPage">
|
||||||
<div className="pageTitle">
|
<div className="panel panel-default panel-connected">
|
||||||
{isEdit ? "Edit Work Order" : "Create Work Order"}
|
<div className="panel-body">
|
||||||
</div>
|
<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="container">
|
||||||
<div className="card formCard">
|
<form onSubmit={handleSave}>
|
||||||
<div className="formBody">
|
{/* Page Header */}
|
||||||
{/* BASIC INFO SECTION */}
|
<div className="form-group">
|
||||||
<div className="formSection">
|
<div className="header">
|
||||||
<div className="sectionTitle">Work Order Details</div>
|
<h3 className="heading thin">
|
||||||
<div className="formRow">
|
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
|
||||||
<div className="formGroup">
|
<i className="fa fa-square fa-stack-2x text-primary"></i>
|
||||||
<label>Work Order Number *</label>
|
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
|
||||||
<input
|
</span>
|
||||||
type="text"
|
<span>{isEdit ? "Edit a Work Order" : "Create a Work Order"}</span>
|
||||||
name="workOrderNumber"
|
</h3>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
{/* Work Order Details Section */}
|
||||||
<div className="formGroup">
|
<div className="form-group">
|
||||||
<label>Location *</label>
|
<div className="header">
|
||||||
<select
|
<h5 className="heading text-uppercase">
|
||||||
name="location"
|
<strong>Work Order Details</strong>
|
||||||
value={formData.location}
|
</h5>
|
||||||
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="formGroup">
|
<div className="row">
|
||||||
<label>Assigned To</label>
|
<div className="col-sm-6">
|
||||||
<select
|
<div className="form-group">
|
||||||
name="assignedTo"
|
<label className="required-field" htmlFor="workOrderNumber">Work Order Number</label>
|
||||||
value={formData.assignedTo}
|
<input
|
||||||
onChange={handleChange}
|
className="form-control"
|
||||||
>
|
type="text"
|
||||||
<option value="">Select Employee</option>
|
id="workOrderNumber"
|
||||||
<option value="John Anderson">John Anderson</option>
|
name="workOrderNumber"
|
||||||
<option value="Sarah Martinez">Sarah Martinez</option>
|
value={formData.workOrderNumber}
|
||||||
<option value="Mike Johnson">Mike Johnson</option>
|
onChange={handleChange}
|
||||||
</select>
|
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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* DATES & PRIORITY SECTION */}
|
<div className="row">
|
||||||
<div className="formSection">
|
<div className="col-sm-6">
|
||||||
<div className="sectionTitle">Schedule & Priority</div>
|
<div className="form-group">
|
||||||
<div className="formRow">
|
<label className="required-field" htmlFor="location">Location</label>
|
||||||
<div className="formGroup">
|
<select
|
||||||
<label>Created Date *</label>
|
className="form-control"
|
||||||
<input
|
id="location"
|
||||||
type="date"
|
name="location"
|
||||||
name="createdDate"
|
value={formData.location}
|
||||||
value={formData.createdDate}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
required
|
||||||
required
|
>
|
||||||
/>
|
<option value="">Select Location</option>
|
||||||
</div>
|
<option value="Springfield Office">Springfield Office</option>
|
||||||
<div className="formGroup">
|
<option value="Chicago Distribution Center">Chicago Distribution Center</option>
|
||||||
<label>Due Date *</label>
|
<option value="Indianapolis Warehouse">Indianapolis Warehouse</option>
|
||||||
<input
|
</select>
|
||||||
type="date"
|
</div>
|
||||||
name="dueDate"
|
</div>
|
||||||
value={formData.dueDate}
|
<div className="col-sm-6">
|
||||||
onChange={handleChange}
|
<div className="form-group">
|
||||||
required
|
<label htmlFor="assignedTo">Assigned To</label>
|
||||||
/>
|
<select
|
||||||
</div>
|
className="form-control"
|
||||||
<div className="formGroup">
|
id="assignedTo"
|
||||||
<label>Estimated Hours *</label>
|
name="assignedTo"
|
||||||
<input
|
value={formData.assignedTo}
|
||||||
type="number"
|
onChange={handleChange}
|
||||||
name="estimatedHours"
|
>
|
||||||
value={formData.estimatedHours}
|
<option value="">Select Employee</option>
|
||||||
onChange={handleChange}
|
<option value="John Anderson">John Anderson</option>
|
||||||
required
|
<option value="Sarah Martinez">Sarah Martinez</option>
|
||||||
min="0.5"
|
<option value="Mike Johnson">Mike Johnson</option>
|
||||||
step="0.5"
|
</select>
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="formRow">
|
{/* Schedule & Priority Section */}
|
||||||
<div className="formGroup">
|
<div className="form-group">
|
||||||
<label>Priority *</label>
|
<div className="header">
|
||||||
<select
|
<h5 className="heading text-uppercase">
|
||||||
name="priority"
|
<strong>Schedule & Priority</strong>
|
||||||
value={formData.priority}
|
</h5>
|
||||||
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="formGroup">
|
<div className="row">
|
||||||
<label>Status *</label>
|
<div className="col-sm-4">
|
||||||
<select
|
<div className="form-group">
|
||||||
name="status"
|
<label className="required-field" htmlFor="createdDate">Created Date</label>
|
||||||
value={formData.status}
|
<input
|
||||||
onChange={handleChange}
|
className="form-control"
|
||||||
required
|
type="date"
|
||||||
>
|
id="createdDate"
|
||||||
<option value="Open">Open</option>
|
name="createdDate"
|
||||||
<option value="In Progress">In Progress</option>
|
value={formData.createdDate}
|
||||||
<option value="On Hold">On Hold</option>
|
onChange={handleChange}
|
||||||
<option value="Completed">Completed</option>
|
required
|
||||||
<option value="Cancelled">Cancelled</option>
|
/>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* DESCRIPTION SECTION */}
|
{/* Description Section */}
|
||||||
<div className="formSection">
|
<div className="form-group">
|
||||||
<div className="sectionTitle">Description</div>
|
<div className="header">
|
||||||
<div className="formRow">
|
<h5 className="heading text-uppercase">
|
||||||
<div className="formGroup fullWidth">
|
<strong>Description</strong>
|
||||||
<label>Description</label>
|
</h5>
|
||||||
<textarea
|
</div>
|
||||||
name="description"
|
<div className="row">
|
||||||
value={formData.description}
|
<div className="col-sm-12">
|
||||||
onChange={handleChange}
|
<div className="form-group">
|
||||||
placeholder="Detailed description of the work to be performed"
|
<label htmlFor="description">Description</label>
|
||||||
rows="5"
|
<textarea
|
||||||
style={{
|
className="form-control"
|
||||||
width: "100%",
|
id="description"
|
||||||
padding: "10px",
|
name="description"
|
||||||
borderRadius: "4px",
|
value={formData.description}
|
||||||
border: "1px solid #ddd",
|
onChange={handleChange}
|
||||||
fontFamily: "inherit",
|
rows="5"
|
||||||
}}
|
placeholder="Detailed description of the work to be performed"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ACTION BUTTONS */}
|
{/* Form Actions */}
|
||||||
<div className="formActions">
|
<div className="form-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btnSecondary"
|
className="btn btn-default"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btnPrimary"
|
className="btn btn-primary"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</section>
|
</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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { getWorkOrders } from "../api";
|
import { fetchWorkOrdersPaged } from "./api";
|
||||||
|
|
||||||
const useWorkOrders = () => {
|
export default function useWorkOrders() {
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(12);
|
const [pageSize, setPageSize] = useState(12);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
setErr("");
|
||||||
const workorders = await getWorkOrders();
|
fetchWorkOrdersPaged({ search, page, pageSize })
|
||||||
let filtered = workorders;
|
.then(({ rows, totalCount }) => {
|
||||||
if (search) {
|
setRows(rows);
|
||||||
filtered = workorders.filter((w) =>
|
setTotalCount(totalCount);
|
||||||
w.title.toLowerCase().includes(search.toLowerCase()) ||
|
})
|
||||||
w.number.toLowerCase().includes(search.toLowerCase()) ||
|
.catch((e) => setErr(e.message || "Failed to load"))
|
||||||
w.location.toLowerCase().includes(search.toLowerCase())
|
.finally(() => setLoading(false));
|
||||||
);
|
|
||||||
}
|
|
||||||
setTotalCount(filtered.length);
|
|
||||||
const start = (page - 1) * pageSize;
|
|
||||||
setRows(filtered.slice(start, start + pageSize));
|
|
||||||
} catch (error) {
|
|
||||||
setErr(error.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload();
|
|
||||||
}, [search, page, pageSize]);
|
}, [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