-
Assignment
-
-
-
-
+ {/* Schedule Information Section */}
+
+
+
+ Schedule Information
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- {/* NOTES SECTION */}
-
-
Notes
-
-
-
-
+ {/* Assignment Section */}
+
+
+
+ Assignment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
- {/* ACTION BUTTONS */}
-
-
-
+ {/* Notes Section */}
+
+
+ {/* Form Actions */}
+
+
+
+
+
-
-
+
+
);
}
diff --git a/src/pages/PmSchedule/list/api.js b/src/pages/PmSchedule/list/api.js
index 665c9889..1552bde0 100644
--- a/src/pages/PmSchedule/list/api.js
+++ b/src/pages/PmSchedule/list/api.js
@@ -8,23 +8,23 @@ export const fetchPmSchedulesPaged = async ({
pageSize = 12,
}) => {
const res = await get(
- `${API_URL}/PmSchedule/GetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`
+ `${API_URL}/PMSchedule/GetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`
);
- const rows = (res?.Data || []).map(x => ({
- id: x.Id,
- name: x.Name,
- trade: x.Trade,
- status: x.Status,
- nextWorkOrderDate: x.NextWorkOrderDate,
- owner: x.Owner,
+ const rows = (res?.data || res?.Data || []).map(x => ({
+ id: x.id ?? x.Id,
+ name: x.name ?? x.Name ?? "",
+ trade: x.trade ?? x.Trade ?? "",
+ status: x.status ?? x.Status ?? "",
+ nextWorkOrderDate: x.nextWorkOrderDate ?? x.NextWorkOrderDate ?? "",
+ owner: x.owner ?? x.Owner ?? "",
}));
return {
rows,
- page: res.PageNumber,
- pageSize: res.PageSize,
- totalCount: res.TotalCount,
- totalPages: res.TotalPages,
+ page: res?.pageNumber ?? res?.PageNumber ?? 1,
+ pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
+ totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
+ totalPages: res?.totalPages ?? res?.TotalPages ?? 1,
};
};
diff --git a/src/pages/PmSchedule/list/useItems.js b/src/pages/PmSchedule/list/useItems.js
index ed8d1bf4..0f89928f 100644
--- a/src/pages/PmSchedule/list/useItems.js
+++ b/src/pages/PmSchedule/list/useItems.js
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from "react";
-import { fetchContactsPaged } from "./api";
+import { fetchPmSchedulesPaged } from "./api";
export default function useItems() {
const [rows, setRows] = useState([]);
@@ -13,7 +13,7 @@ export default function useItems() {
const reload = useCallback(() => {
setLoading(true);
setErr("");
- fetchContactsPaged({ search, page, pageSize })
+ fetchPmSchedulesPaged({ search, page, pageSize })
.then(({ rows, totalCount }) => {
setRows(rows);
setTotalCount(totalCount);
diff --git a/src/pages/PmSchedule/list/usePMSchedules.js b/src/pages/PmSchedule/list/usePMSchedules.js
index d7f2eeb7..f603c65e 100644
--- a/src/pages/PmSchedule/list/usePMSchedules.js
+++ b/src/pages/PmSchedule/list/usePMSchedules.js
@@ -1,5 +1,5 @@
-import { useState, useEffect } from "react";
-import { getPMSchedules } from "../api";
+import { useState, useEffect, useCallback, useMemo } from "react";
+import { fetchPmSchedulesPaged } from "./api";
const usePMSchedules = () => {
const [rows, setRows] = useState([]);
@@ -7,35 +7,41 @@ const usePMSchedules = () => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [totalCount, setTotalCount] = useState(0);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [err, setErr] = useState("");
- const reload = async () => {
+ const reload = useCallback(() => {
setLoading(true);
- try {
- const pmschedules = await getPMSchedules();
- let filtered = pmschedules;
- if (search) {
- filtered = pmschedules.filter((p) =>
- p.asset.toLowerCase().includes(search.toLowerCase()) ||
- p.frequency.toLowerCase().includes(search.toLowerCase())
- );
- }
- setTotalCount(filtered.length);
- const start = (page - 1) * pageSize;
- setRows(filtered.slice(start, start + pageSize));
- } catch (error) {
- setErr(error.message);
- } finally {
- setLoading(false);
- }
- };
+ setErr("");
+ fetchPmSchedulesPaged({ search, page, pageSize })
+ .then(({ rows, totalCount }) => {
+ setRows(rows);
+ setTotalCount(totalCount);
+ })
+ .catch((e) => setErr(e.message || "Failed to load"))
+ .finally(() => setLoading(false));
+ }, [search, page, pageSize]);
useEffect(() => {
reload();
- }, [search, page, pageSize]);
+ }, [reload]);
- return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
+ return useMemo(
+ () => ({
+ rows,
+ search,
+ setSearch,
+ page,
+ setPage,
+ pageSize,
+ setPageSize,
+ totalCount,
+ loading,
+ err,
+ reload,
+ }),
+ [rows, search, page, pageSize, totalCount, loading, err, reload]
+ );
};
export default usePMSchedules;
diff --git a/src/pages/accounts/api.js b/src/pages/accounts/api.js
index a382d14d..1598926c 100644
--- a/src/pages/accounts/api.js
+++ b/src/pages/accounts/api.js
@@ -1,66 +1,49 @@
-// Mock accounts data
-const mockAccounts = [
- {
- id: 1,
- name: "Acme Corp",
- email: "contact@acmecorp.com",
- phone: "(555) 123-4567",
- owner: "John Smith",
- status: "Active",
- },
- {
- id: 2,
- name: "Global Industries",
- email: "info@globalind.com",
- phone: "(555) 234-5678",
- owner: "Sarah Johnson",
- status: "Active",
- },
- {
- id: 3,
- name: "Tech Solutions LLC",
- email: "support@techsol.com",
- phone: "(555) 345-6789",
- owner: "Mike Chen",
- status: "Inactive",
- },
- {
- id: 4,
- name: "Premier Services",
- email: "hello@premier.com",
- phone: "(555) 456-7890",
- owner: "Lisa Brown",
- status: "Active",
- },
-];
+import { accountApi } from "../../services/api";
-export const getAccounts = async () => {
- await new Promise(resolve => setTimeout(resolve, 300));
- return [...mockAccounts];
+export const getAccounts = async (params = {}) => {
+ try {
+ const response = await accountApi.getAll(params);
+ const accounts = response.data || response;
+ return Array.isArray(accounts) ? accounts : [];
+ } catch (error) {
+ console.error("Error fetching accounts:", error);
+ throw error;
+ }
};
export const getAccountById = async (id) => {
- await new Promise(resolve => setTimeout(resolve, 200));
- return mockAccounts.find(a => a.id === parseInt(id));
+ try {
+ return await accountApi.getById(id);
+ } catch (error) {
+ console.error("Error fetching account:", error);
+ throw error;
+ }
};
export const addAccount = async (account) => {
- await new Promise(resolve => setTimeout(resolve, 500));
- const newAccount = { id: mockAccounts.length + 1, ...account };
- mockAccounts.push(newAccount);
- return newAccount;
+ try {
+ return await accountApi.create(account);
+ } catch (error) {
+ console.error("Error creating account:", error);
+ throw error;
+ }
};
export const editAccount = async (account) => {
- await new Promise(resolve => setTimeout(resolve, 500));
- const idx = mockAccounts.findIndex(a => a.id === account.id);
- if (idx >= 0) mockAccounts[idx] = account;
- return account;
+ try {
+ return await accountApi.update(account.id, account);
+ } catch (error) {
+ console.error("Error updating account:", error);
+ throw error;
+ }
};
export const deleteAccount = async (id) => {
- await new Promise(resolve => setTimeout(resolve, 500));
- const idx = mockAccounts.findIndex(a => a.id === id);
- if (idx >= 0) mockAccounts.splice(idx, 1);
- return true;
+ try {
+ await accountApi.delete(id);
+ return true;
+ } catch (error) {
+ console.error("Error deleting account:", error);
+ throw error;
+ }
};
diff --git a/src/pages/accounts/list/api.js b/src/pages/accounts/list/api.js
index 21762832..b5be4008 100644
--- a/src/pages/accounts/list/api.js
+++ b/src/pages/accounts/list/api.js
@@ -1,20 +1,9 @@
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
-export const fetchAccountsPaged = async ({
- search = "",
- page = 1,
- pageSize = 12,
-}) => {
- const res = await get(
- `${API_URL}/Account/GetAccountList?search=${encodeURIComponent(
- search
- )}&page=${page}&pageSize=${pageSize}`
- );
-
- const data = res?.data || res?.Data || [];
-
- const rows = data.map((x) => ({
+export const fetchAccountsPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
+ const res = await get(`${API_URL}/Account/GetAccountList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
+ const rows = (res?.data || res?.Data || []).map(x => ({
id: x.id ?? x.Id,
name: x.name ?? x.Name ?? x.AccountName ?? "",
number: x.number ?? x.AccountNumber ?? "",
@@ -24,13 +13,12 @@ export const fetchAccountsPaged = async ({
phoneNumber: x.phoneNumber ?? x.Phone ?? x.PhoneNumber ?? "",
owner: x.owner ?? x.Owner ?? "",
}));
-
return {
rows,
- page: res?.page ?? res?.PageNumber ?? page,
- pageSize: res?.pageSize ?? res?.PageSize ?? pageSize,
- totalCount: res?.totalCount ?? res?.TotalCount ?? rows.length,
- totalPages: res?.totalPages ?? res?.TotalPages ?? 1,
+ page: res?.pageNumber ?? 1,
+ pageSize: res?.pageSize ?? pageSize,
+ totalCount: res?.totalCount ?? rows.length,
+ totalPages: res?.totalPages ?? 1,
};
};
diff --git a/src/pages/accounts/list/useAccounts.js b/src/pages/accounts/list/useAccounts.js
index 7302da9c..56299ea4 100644
--- a/src/pages/accounts/list/useAccounts.js
+++ b/src/pages/accounts/list/useAccounts.js
@@ -1,42 +1,31 @@
-import { useState, useEffect } from "react";
-import { getAccounts } from "../api";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { fetchAccountsPaged } from "./api";
-const useAccounts = () => {
+export default function useAccounts() {
const [rows, setRows] = useState([]);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [totalCount, setTotalCount] = useState(0);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [err, setErr] = useState("");
- const reload = async () => {
+ const reload = useCallback(() => {
setLoading(true);
- try {
- const allAccounts = await getAccounts();
- let filtered = allAccounts;
- if (search) {
- filtered = allAccounts.filter((a) =>
- a.name.toLowerCase().includes(search.toLowerCase()) ||
- a.email.toLowerCase().includes(search.toLowerCase()) ||
- a.phone.includes(search)
- );
- }
- setTotalCount(filtered.length);
- const start = (page - 1) * pageSize;
- setRows(filtered.slice(start, start + pageSize));
- } catch (error) {
- setErr(error.message);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- reload();
+ setErr("");
+ fetchAccountsPaged({ search, page, pageSize })
+ .then(({ rows, totalCount }) => {
+ setRows(rows);
+ setTotalCount(totalCount);
+ })
+ .catch((e) => setErr(e.message || "Failed to load"))
+ .finally(() => setLoading(false));
}, [search, page, pageSize]);
- return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
-};
+ useEffect(() => { reload(); }, [reload]);
-export default useAccounts;
+ return useMemo(() => ({
+ rows, search, setSearch, page, setPage, pageSize, setPageSize,
+ totalCount, loading, err, reload
+ }), [rows, search, page, pageSize, totalCount, loading, err, reload]);
+}
diff --git a/src/pages/assets/form/AssetFormPage.js b/src/pages/assets/form/AssetFormPage.js
new file mode 100644
index 00000000..9a8498f8
--- /dev/null
+++ b/src/pages/assets/form/AssetFormPage.js
@@ -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 (
+
+
+
+ );
+ }
+
+ return (
+
+ }
+ onClick={() => navigate("/assets")}
+ sx={{ mb: 2 }}
+ >
+ Back to Assets
+
+
+
+
+ {isEdit ? "Edit Asset" : "Create New Asset"}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account
+
+
+
+
+
+
+ Location
+
+
+
+
+
+
+ }
+ label="Active"
+ />
+
+
+
+
+
+ }
+ disabled={loading}
+ >
+ {loading ? "Saving..." : isEdit ? "Update Asset" : "Create Asset"}
+
+
+
+
+
+
+
+ );
+};
+
+export default AssetFormPage;
+
diff --git a/src/pages/assets/list/Delete.js b/src/pages/assets/list/Delete.js
new file mode 100644
index 00000000..9e4da221
--- /dev/null
+++ b/src/pages/assets/list/Delete.js
@@ -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 (
+
+ );
+};
+
+export default DeleteDialog;
+
diff --git a/src/pages/assets/list/List.js b/src/pages/assets/list/List.js
new file mode 100644
index 00000000..145794c3
--- /dev/null
+++ b/src/pages/assets/list/List.js
@@ -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 (
+
+
+
+ Assets
+
+ }
+ onClick={() => navigate("/assets/new")}
+ >
+ New Asset
+
+
+
+
+ setSearchInput(e.target.value)}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ }}
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading ? (
+
+
+
+ ) : (
+ <>
+
+
+ {totalPages > 1 && (
+
+ handlePageChange(value)}
+ color="primary"
+ />
+
+ )}
+ >
+ )}
+
+ setDeleteDialogOpen(false)}
+ onConfirm={handleDeleteConfirm}
+ itemName={itemToDelete?.name || ""}
+ />
+
+ );
+};
+
+export default AssetsList;
+
diff --git a/src/pages/assets/list/Table.js b/src/pages/assets/list/Table.js
new file mode 100644
index 00000000..fb88ab05
--- /dev/null
+++ b/src/pages/assets/list/Table.js
@@ -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 (
+
+
+
+
+ Asset Code
+ Name
+ Description
+ Account
+ Location
+ Status
+ Actions
+
+
+
+ {items.length === 0 ? (
+
+
+ No assets found
+
+
+ ) : (
+ items.map((item) => (
+
+ {item.assetCode}
+ {item.name}
+ {item.description || "-"}
+ {item.accountName || "-"}
+ {item.locationName || "-"}
+
+
+
+
+ handleEdit(item.id)}
+ title="Edit"
+ >
+
+
+ onDelete(item.id)}
+ title="Delete"
+ >
+
+
+
+
+ ))
+ )}
+
+
+
+ );
+};
+
+export default AssetsTable;
+
diff --git a/src/pages/assets/list/api.js b/src/pages/assets/list/api.js
new file mode 100644
index 00000000..38d200d3
--- /dev/null
+++ b/src/pages/assets/list/api.js
@@ -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}`);
+
diff --git a/src/pages/assets/list/useItems.js b/src/pages/assets/list/useItems.js
new file mode 100644
index 00000000..dbfe01cf
--- /dev/null
+++ b/src/pages/assets/list/useItems.js
@@ -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,
+ };
+};
+
diff --git a/src/pages/auth/LoginPage.css b/src/pages/auth/LoginPage.css
new file mode 100644
index 00000000..e20ac8da
--- /dev/null
+++ b/src/pages/auth/LoginPage.css
@@ -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;
+}
+
diff --git a/src/pages/auth/LoginPage.js b/src/pages/auth/LoginPage.js
new file mode 100644
index 00000000..f65ef6e9
--- /dev/null
+++ b/src/pages/auth/LoginPage.js
@@ -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 (
+
+
+
+ {/* Copyright */}
+
+ ©2026 Core 7 Technologies Inc. All rights reserved.
+
+
+ );
+}
+
diff --git a/src/pages/calendar/api.js b/src/pages/calendar/api.js
new file mode 100644
index 00000000..60d4be46
--- /dev/null
+++ b/src/pages/calendar/api.js
@@ -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();
+}
+
diff --git a/src/pages/calendar/components/CalendarActionBar.css b/src/pages/calendar/components/CalendarActionBar.css
new file mode 100644
index 00000000..18b39add
--- /dev/null
+++ b/src/pages/calendar/components/CalendarActionBar.css
@@ -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;
+ }
+}
+
diff --git a/src/pages/calendar/components/CalendarActionBar.js b/src/pages/calendar/components/CalendarActionBar.js
new file mode 100644
index 00000000..26e1469c
--- /dev/null
+++ b/src/pages/calendar/components/CalendarActionBar.js
@@ -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 (
+
+
+
+ {/* Right Side Navigation */}
+
+
+ );
+}
+
diff --git a/src/pages/calendar/form/EventFormPage.css b/src/pages/calendar/form/EventFormPage.css
new file mode 100644
index 00000000..2f5e43d9
--- /dev/null
+++ b/src/pages/calendar/form/EventFormPage.css
@@ -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;
+ }
+}
diff --git a/src/pages/calendar/form/EventFormPage.js b/src/pages/calendar/form/EventFormPage.js
new file mode 100644
index 00000000..894d6e92
--- /dev/null
+++ b/src/pages/calendar/form/EventFormPage.js
@@ -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 (
+
+
+
+
+
+ -
+ Calendar
+
+ - {isEdit ? "Edit" : "Create"}
+
+
+
+
+
+
+
+ );
+}
+
+
diff --git a/src/pages/calendar/list/CalendarPage.css b/src/pages/calendar/list/CalendarPage.css
new file mode 100644
index 00000000..8f6a17e1
--- /dev/null
+++ b/src/pages/calendar/list/CalendarPage.css
@@ -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;
+ }
+}
+
diff --git a/src/pages/calendar/list/CalendarPage.js b/src/pages/calendar/list/CalendarPage.js
new file mode 100644
index 00000000..97e4475e
--- /dev/null
+++ b/src/pages/calendar/list/CalendarPage.js
@@ -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 (
+ <>
+
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}
+ />
+
+
+ {/* Calendar Container */}
+
+ {loading && (
+
+ )}
+
+ {/* Custom Title Display */}
+
+
{currentTitle}
+
+
+
+
+
+ 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}
+ />
+
+ >
+ );
+}
+
diff --git a/src/pages/calendar/list/Table.js b/src/pages/calendar/list/Table.js
new file mode 100644
index 00000000..84ec4c2d
--- /dev/null
+++ b/src/pages/calendar/list/Table.js
@@ -0,0 +1,163 @@
+export default function Table({
+ loading,
+ error,
+ rows,
+ onEdit,
+ onDelete,
+ page,
+ setPage,
+ pageSize,
+ setPageSize,
+ totalCount,
+ totalPages,
+}) {
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (rows.length === 0) {
+ return (
+
+
+
+
No events found
+
Create your first event to get started
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
+ | Title |
+ Start Date |
+ End Date |
+ Location |
+ Color |
+ All Day |
+ Actions |
+
+
+
+ {rows.map((row) => (
+
+ |
+ {row.title}
+ {row.description && (
+ {row.description.substring(0, 100)}
+ )}
+ |
+
+ {formatDate(row.startDate)}
+ {row.startTime && {row.startTime} }
+ |
+
+ {formatDate(row.endDate)}
+ {row.endTime && {row.endTime} }
+ |
+ {row.location || "-"} |
+
+
+ {row.eventColor || "primary"}
+
+ |
+ {row.allDay ? "Yes" : "No"} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+
+ Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, totalCount)} of{" "}
+ {totalCount} events
+
+
+
+
+
+
+
+
+ Page {page} of {totalPages}
+
+
+
+
+
+ );
+}
+
diff --git a/src/pages/calendar/list/useEvents.js b/src/pages/calendar/list/useEvents.js
new file mode 100644
index 00000000..a2668855
--- /dev/null
+++ b/src/pages/calendar/list/useEvents.js
@@ -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;
+
diff --git a/src/pages/employees/api.js b/src/pages/employees/api.js
index f3e8e7d3..d2e944cb 100644
--- a/src/pages/employees/api.js
+++ b/src/pages/employees/api.js
@@ -1,94 +1,131 @@
-// Mock employees data
-const mockEmployees = [
- {
- id: 1,
- name: "John Anderson",
- email: "john.anderson@company.com",
- phone: "(555) 123-4567",
- jobTitle: "Senior Technician",
- role: "Technician",
- status: "Active",
- hireDate: "2020-03-15",
- address: "123 Main St, Springfield, IL 62701",
- department: "Field Service",
- },
- {
- id: 2,
- name: "Sarah Martinez",
- email: "sarah.martinez@company.com",
- phone: "(555) 234-5678",
- jobTitle: "Service Manager",
- role: "Manager",
- status: "Active",
- hireDate: "2019-06-01",
- address: "456 Oak Ave, Springfield, IL 62702",
- department: "Operations",
- },
- {
- id: 3,
- name: "Mike Johnson",
- email: "mike.johnson@company.com",
- phone: "(555) 345-6789",
- jobTitle: "Technician",
- role: "Technician",
- status: "Active",
- hireDate: "2021-01-20",
- address: "789 Pine Rd, Springfield, IL 62703",
- department: "Field Service",
- },
- {
- id: 4,
- name: "Emily Chen",
- email: "emily.chen@company.com",
- phone: "(555) 456-7890",
- jobTitle: "HR Specialist",
- role: "Admin",
- status: "Active",
- hireDate: "2022-05-10",
- address: "321 Oak St, Springfield, IL 62704",
- department: "Admin",
- },
- {
- id: 5,
- name: "David Williams",
- email: "david.williams@company.com",
- phone: "(555) 567-8901",
- jobTitle: "Field Supervisor",
- role: "Supervisor",
- status: "Active",
- hireDate: "2018-11-30",
- address: "654 Pine Ave, Springfield, IL 62705",
- department: "Field Service",
- },
-];
+import { employeeApi } from "../../services/api";
-export const getEmployees = async () => {
- await new Promise(resolve => setTimeout(resolve, 300));
- return [...mockEmployees];
+// Transform backend data to frontend format
+const transformEmployee = (employee) => {
+ if (!employee) return null;
+
+ return {
+ id: employee.id,
+ name: `${employee.firstName || ""} ${employee.lastName || ""}`.trim(),
+ email: employee.email || "",
+ phone: employee.phone || "",
+ jobTitle: employee.jobTitle || "",
+ role: employee.role || "",
+ status: employee.status || "",
+ hireDate: employee.hireDate || "",
+ address: employee.address || "",
+ department: employee.department || "",
+ // Include all other fields for form editing
+ firstName: employee.firstName || "",
+ middleName: employee.middleName || "",
+ lastName: employee.lastName || "",
+ dateOfBirth: employee.dateOfBirth || "",
+ street: employee.street || "",
+ streetLineTwo: employee.streetLineTwo || "",
+ city: employee.city || "",
+ state: employee.state || "",
+ zipCode: employee.zipCode || "",
+ country: employee.country || "US",
+ county: employee.county || "",
+ timeZone: employee.timeZone || "Eastern Standard Time",
+ gender: employee.gender || "",
+ employeeNumber: employee.employeeNumber || "",
+ regionName: employee.regionName || "",
+ userName: employee.userName || "",
+ smsAddress: employee.smsAddress || "",
+ website: employee.website || "",
+ facebookUrl: employee.facebookUrl || "",
+ linkedInUrl: employee.linkedInUrl || "",
+ twitterUrl: employee.twitterUrl || "",
+ };
+};
+
+// Transform frontend data to backend format
+const transformToBackend = (employee) => {
+ return {
+ id: employee.id,
+ firstName: employee.firstName || "",
+ middleName: employee.middleName || null,
+ lastName: employee.lastName || "",
+ email: employee.email || "",
+ phone: employee.phone || "",
+ jobTitle: employee.jobTitle || "",
+ role: employee.role || "Technician",
+ status: employee.status || "Active",
+ hireDate: employee.hireDate || null,
+ dateOfBirth: employee.dateOfBirth || null,
+ address: employee.address || "",
+ street: employee.street || "",
+ streetLineTwo: employee.streetLineTwo || null,
+ city: employee.city || "",
+ state: employee.state || "",
+ zipCode: employee.zipCode || "",
+ country: employee.country || "US",
+ county: employee.county || null,
+ department: employee.department || "Field Service",
+ timeZone: employee.timeZone || "Eastern Standard Time",
+ gender: employee.gender || null,
+ employeeNumber: employee.employeeNumber || null,
+ regionName: employee.regionName || null,
+ userName: employee.userName || employee.email || "",
+ smsAddress: employee.smsAddress || null,
+ website: employee.website || null,
+ facebookUrl: employee.facebookUrl || null,
+ linkedInUrl: employee.linkedInUrl || null,
+ twitterUrl: employee.twitterUrl || null,
+ };
+};
+
+export const getEmployees = async (params = {}) => {
+ try {
+ const response = await employeeApi.getAll(params);
+ // Handle both paginated and non-paginated responses
+ const employees = response.data || response;
+ return Array.isArray(employees) ? employees.map(transformEmployee) : [];
+ } catch (error) {
+ console.error("Error fetching employees:", error);
+ throw error;
+ }
};
export const getEmployeeById = async (id) => {
- await new Promise(resolve => setTimeout(resolve, 200));
- return mockEmployees.find(e => e.id === parseInt(id));
+ try {
+ const employee = await employeeApi.getById(id);
+ return transformEmployee(employee);
+ } catch (error) {
+ console.error("Error fetching employee:", error);
+ throw error;
+ }
};
export const addEmployee = async (employee) => {
- await new Promise(resolve => setTimeout(resolve, 500));
- const newEmployee = { id: mockEmployees.length + 1, ...employee };
- mockEmployees.push(newEmployee);
- return newEmployee;
+ try {
+ const backendData = transformToBackend(employee);
+ const created = await employeeApi.create(backendData);
+ return transformEmployee(created);
+ } catch (error) {
+ console.error("Error creating employee:", error);
+ throw error;
+ }
};
export const editEmployee = async (employee) => {
- await new Promise(resolve => setTimeout(resolve, 500));
- const idx = mockEmployees.findIndex(e => e.id === employee.id);
- if (idx >= 0) mockEmployees[idx] = employee;
- return employee;
+ try {
+ const backendData = transformToBackend(employee);
+ const updated = await employeeApi.update(employee.id, backendData);
+ return transformEmployee(updated);
+ } catch (error) {
+ console.error("Error updating employee:", error);
+ throw error;
+ }
};
export const deleteEmployee = async (id) => {
- await new Promise(resolve => setTimeout(resolve, 500));
- const idx = mockEmployees.findIndex(e => e.id === id);
- if (idx >= 0) mockEmployees.splice(idx, 1);
- return true;
+ try {
+ await employeeApi.delete(id);
+ return true;
+ } catch (error) {
+ console.error("Error deleting employee:", error);
+ throw error;
+ }
};
diff --git a/src/pages/employees/list/api.js b/src/pages/employees/list/api.js
index 463a4478..05a2df17 100644
--- a/src/pages/employees/list/api.js
+++ b/src/pages/employees/list/api.js
@@ -1,33 +1,23 @@
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
-export const fetchEmployeesPaged = async ({
- search = "",
- page = 1,
- pageSize = 12,
-}) => {
- const res = await get(
- `${API_URL}/Employee/GetEmployeeList?search=${encodeURIComponent(
- search
- )}&page=${page}&pageSize=${pageSize}`
- );
-
- const rows = (res || []).map((x) => ({
+export const fetchEmployeesPaged = async ({ search = "", page = 1, pageSize = 12 }) => {
+ const res = await get(`${API_URL}/Employee/GetEmployeeList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`);
+ const rows = (res?.data || res?.Data || []).map(x => ({
id: x.id,
- name: `${x.name ?? ""} ${x.name ?? ""}`.trim(),
+ name: `${x.firstName ?? ""} ${x.lastName ?? ""}`.trim(),
jobTitle: x.jobTitle ?? "",
email: x.email ?? "",
phone: x.phone ?? "",
department: x.department ?? "",
status: x.status ?? "",
}));
-
return {
rows,
- page: res?.PageNumber ?? 1,
- pageSize: res?.PageSize ?? pageSize,
- totalCount: res?.TotalCount ?? rows.length,
- totalPages: res?.TotalPages ?? 1,
+ page: res?.pageNumber ?? 1,
+ pageSize: res?.pageSize ?? pageSize,
+ totalCount: res?.totalCount ?? rows.length,
+ totalPages: res?.totalPages ?? 1,
};
};
diff --git a/src/pages/employees/list/useEmployees.js b/src/pages/employees/list/useEmployees.js
index f36a3654..3f811643 100644
--- a/src/pages/employees/list/useEmployees.js
+++ b/src/pages/employees/list/useEmployees.js
@@ -1,42 +1,31 @@
-import { useState, useEffect } from "react";
-import { getEmployees } from "../api";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { fetchEmployeesPaged } from "./api";
-const useEmployees = () => {
+export default function useEmployees() {
const [rows, setRows] = useState([]);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(12);
const [totalCount, setTotalCount] = useState(0);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [err, setErr] = useState("");
- const reload = async () => {
+ const reload = useCallback(() => {
setLoading(true);
- try {
- const employees = await getEmployees();
- let filtered = employees;
- if (search) {
- filtered = employees.filter((e) =>
- e.name.toLowerCase().includes(search.toLowerCase()) ||
- e.email.toLowerCase().includes(search.toLowerCase()) ||
- e.phone.includes(search)
- );
- }
- setTotalCount(filtered.length);
- const start = (page - 1) * pageSize;
- setRows(filtered.slice(start, start + pageSize));
- } catch (error) {
- setErr(error.message);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- reload();
+ setErr("");
+ fetchEmployeesPaged({ search, page, pageSize })
+ .then(({ rows, totalCount }) => {
+ setRows(rows);
+ setTotalCount(totalCount);
+ })
+ .catch((e) => setErr(e.message || "Failed to load"))
+ .finally(() => setLoading(false));
}, [search, page, pageSize]);
- return { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload };
-};
+ useEffect(() => { reload(); }, [reload]);
-export default useEmployees;
+ return useMemo(() => ({
+ rows, search, setSearch, page, setPage, pageSize, setPageSize,
+ totalCount, loading, err, reload
+ }), [rows, search, page, pageSize, totalCount, loading, err, reload]);
+}
diff --git a/src/pages/followup/form/FollowUpFormPage.css b/src/pages/followup/form/FollowUpFormPage.css
new file mode 100644
index 00000000..bc823fa6
--- /dev/null
+++ b/src/pages/followup/form/FollowUpFormPage.css
@@ -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%;
+ }
+}
+
diff --git a/src/pages/followup/form/FollowUpFormPage.js b/src/pages/followup/form/FollowUpFormPage.js
index b778c678..31ed1a11 100644
--- a/src/pages/followup/form/FollowUpFormPage.js
+++ b/src/pages/followup/form/FollowUpFormPage.js
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router";
import { toast } from "react-toastify";
import { getFollowUpById, addFollowUp, editFollowUp } from "../api";
+import "./FollowUpFormPage.css";
export default function FollowUpFormPage() {
const { id } = useParams();
@@ -71,149 +72,202 @@ export default function FollowUpFormPage() {
};
return (
-
-
- {isEdit ? "Edit Follow-up" : "Create Follow-up"}
-
+
+
+
+
+ -
+ { e.preventDefault(); navigate("/followups"); }}>
+ Follow-ups
+
+
+ - {isEdit ? "Edit" : "Create"}
+
-