diff --git a/.env b/.env new file mode 100644 index 00000000..5888767c --- /dev/null +++ b/.env @@ -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 + diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..183677fd --- /dev/null +++ b/.env.example @@ -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 + diff --git a/.gitignore b/.gitignore index b5148e25..4317cb1f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +db.txt +seed-data.sql +/backend \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bfc315ab..d52505cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/react": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", "@mui/icons-material": "^7.3.1", "@mui/material": "^7.3.1", "@testing-library/dom": "^10.4.1", @@ -2451,6 +2455,52 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "peer": true, + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz", + "integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -12943,6 +12993,16 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -15646,16 +15706,16 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -18332,6 +18392,41 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==" }, + "@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "peer": true, + "requires": { + "preact": "~10.12.1" + } + }, + "@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "requires": {} + }, + "@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "requires": {} + }, + "@fullcalendar/react": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz", + "integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==", + "requires": {} + }, + "@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "requires": { + "@fullcalendar/daygrid": "~6.1.20" + } + }, "@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -25654,6 +25749,12 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "peer": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -27628,9 +27729,9 @@ } }, "typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "peer": true }, "unbox-primitive": { diff --git a/package.json b/package.json index db909acb..ffb0d212 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,10 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@fullcalendar/daygrid": "^6.1.20", + "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/react": "^6.1.20", + "@fullcalendar/timegrid": "^6.1.20", "@mui/icons-material": "^7.3.1", "@mui/material": "^7.3.1", "@testing-library/dom": "^10.4.1", diff --git a/public/BrandingImage.png b/public/BrandingImage.png new file mode 100644 index 00000000..16cda47b Binary files /dev/null and b/public/BrandingImage.png differ diff --git a/src/App.js b/src/App.js index 8d762ba0..ef675548 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,14 @@ import React, { useState } from "react"; -import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import Sidebar from "./components/Sidebar"; import Topbar from "./components/Topbar"; import Dashboard from "./pages/Dashboard"; +// AUTH +import LoginPage from "./pages/auth/LoginPage"; +import { isAuthenticated } from "./services/authService"; + // CONTACTS import ContactsList from "./pages/contacts/list/List"; import ContactFormPage from "./pages/contacts/form/ContactFormPage"; @@ -33,60 +37,96 @@ import FollowUpFormPage from "./pages/followup/form/FollowUpFormPage"; import PMScheduleList from "./pages/PmSchedule/list/List"; import PMScheduleFormPage from "./pages/PmSchedule/form/PmScheduleFormPage"; +// ASSETS +import AssetsList from "./pages/assets/list/List"; +import AssetFormPage from "./pages/assets/form/AssetFormPage"; + +// CALENDAR +import CalendarPage from "./pages/calendar/list/CalendarPage"; +import EventFormPage from "./pages/calendar/form/EventFormPage"; + +// Protected Route Component +function ProtectedRoute({ children }) { + return isAuthenticated() ? children : ; +} + export default function App() { const [navCollapsed, setNavCollapsed] = useState(false); return ( -
- + + {/* Public Routes */} + } /> -
- setNavCollapsed(v => !v)} /> + {/* Protected Routes */} + +
+ -
- - } /> - } /> +
+ setNavCollapsed(v => !v)} /> - {/* CONTACTS */} - } /> - } /> - } /> +
+ + } /> + } /> - {/* ACCOUNTS */} - } /> - } /> - } /> + {/* CONTACTS */} + } /> + } /> + } /> - {/* EMPLOYEES */} - } /> - } /> - } /> + {/* ACCOUNTS */} + } /> + } /> + } /> - {/* LOCATIONS */} - } /> - } /> - } /> + {/* EMPLOYEES */} + } /> + } /> + } /> - {/* WORK ORDERS */} - } /> - } /> - } /> + {/* LOCATIONS */} + } /> + } /> + } /> - {/* FOLLOW UPS */} - } /> - } /> - } /> + {/* WORK ORDERS */} + } /> + } /> + } /> - {/* PM SCHEDULES */} - } /> - } /> - } /> - -
-
-
+ {/* FOLLOW UPS */} + } /> + } /> + } /> + + {/* PM SCHEDULES */} + } /> + } /> + } /> + + {/* ASSETS */} + } /> + } /> + } /> + + {/* CALENDAR */} + } /> + } /> + } /> + +
+
+
+ + } + /> +
); } diff --git a/src/components/Topbar.js b/src/components/Topbar.js index a605baf4..7f34e742 100644 --- a/src/components/Topbar.js +++ b/src/components/Topbar.js @@ -1,6 +1,25 @@ import React from "react"; +import { useNavigate } from "react-router-dom"; +import { logout, getCurrentUser } from "../services/authService"; export default function Topbar({ onToggleNav }) { + const navigate = useNavigate(); + const currentUser = getCurrentUser(); + + const handleLogout = () => { + logout(); + navigate("/login"); + }; + + const getInitials = (fullname) => { + if (!fullname) return "U"; + const parts = fullname.split(" "); + if (parts.length >= 2) { + return (parts[0][0] + parts[1][0]).toUpperCase(); + } + return fullname.substring(0, 2).toUpperCase(); + }; + return (
@@ -22,8 +41,23 @@ export default function Topbar({ onToggleNav }) {
🔔1
-
Adam Moussa
-
AM
+
{currentUser?.fullname || "User"}
+
{getInitials(currentUser?.fullname)}
+
); diff --git a/src/pages/PmSchedule/form/PmScheduleFormPage.css b/src/pages/PmSchedule/form/PmScheduleFormPage.css new file mode 100644 index 00000000..36081949 --- /dev/null +++ b/src/pages/PmSchedule/form/PmScheduleFormPage.css @@ -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%; + } +} + diff --git a/src/pages/PmSchedule/form/PmScheduleFormPage.js b/src/pages/PmSchedule/form/PmScheduleFormPage.js index f4ef8148..5f5b06ff 100644 --- a/src/pages/PmSchedule/form/PmScheduleFormPage.js +++ b/src/pages/PmSchedule/form/PmScheduleFormPage.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router"; import { toast } from "react-toastify"; import { getPMScheduleById, addPMSchedule, editPMSchedule } from "../api"; +import "./PmScheduleFormPage.css"; export default function PmScheduleFormPage() { const { id } = useParams(); @@ -72,164 +73,225 @@ export default function PmScheduleFormPage() { }; return ( -
-
- {isEdit ? "Edit PM Schedule" : "Create PM Schedule"} -
+
+
+
+
    +
  1. + { e.preventDefault(); navigate("/pmschedules"); }}> + PM Schedules + +
  2. +
  3. {isEdit ? "Edit" : "Create"}
  4. +
-
-
-
- {/* ASSET INFO SECTION */} -
-
Asset Information
-
-
- - -
-
- - +
+ + {/* Page Header */} +
+
+

+ + + + + {isEdit ? "Edit a PM Schedule" : "Create a PM Schedule"} +

-
- {/* SCHEDULE INFO SECTION */} -
-
Schedule Information
-
-
- - + {/* Asset Information Section */} +
+
+
+ Asset Information +
-
- - -
-
- - +
+
+
+ + +
+
+
+
+ + +
+
-
- {/* ASSIGNMENT SECTION */} -
-
Assignment
-
-
- - + {/* Schedule Information Section */} +
+
+
+ Schedule Information +
-
- - +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
-
- {/* NOTES SECTION */} -
-
Notes
-
-
- -