commiting changes

This commit is contained in:
npalOmega 2026-01-11 17:35:46 -06:00
parent 3461d3159e
commit 27d68b50e9
52 changed files with 2699 additions and 352 deletions

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -1,21 +1,77 @@
import React from "react";
import React, { useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Sidebar from "./components/Sidebar";
import Topbar from "./components/Topbar";
import Contacts from "./components/Contacts";
import ContactsList from './pages/contacts/list/List'
import ContactFormPage from './pages/contacts/form/ContactFormPage'
// CONTACTS
import ContactsList from "./pages/contacts/list/List";
import ContactFormPage from "./pages/contacts/form/ContactFormPage";
import ContactEditPage from "./pages/contacts/form/ContactFormPage";
// ACCOUNTS
import AccountList from "./pages/accounts/list/List";
import AccountForm from "./pages/accounts/form/AccountFormPage";
import AccountEdit from "./pages/accounts/form/AccountFormPage";
// EMPLOYEES
import EmployeeList from "./pages/employees/list/List";
import EmployeeForm from "./pages/employees/form/EmployeeFormPage";
import EmployeeEdit from "./pages/employees/form/EmployeeFormPage";
// FOLLOW UPS
import FollowUpList from "./pages/followup/list/List";
import FollowUpForm from "./pages/followup/form/FollowUpFormPage";
import FollowUpEdit from "./pages/followup/form/FollowUpFormPage";
// PM SCHEDULES
import PMScheduleList from "./pages/PmSchedule/list/List";
import PMScheduleForm from "./pages/PmSchedule/form/PmScheduleFormPage";
import PMScheduleEdit from "./pages/PmSchedule/form/PmScheduleFormPage";
export default function App() {
const [navCollapsed, setNavCollapsed] = useState(false);
return (
<div className="app">
<Sidebar />
<div className="mainArea">
<Topbar />
<div className="contentWrapper">
<ContactFormPage />
<BrowserRouter>
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
<Sidebar />
<div className="mainArea">
<Topbar onToggleNav={() => setNavCollapsed(v => !v)} />
<div className="contentWrapper">
<Routes>
{/* CONTACTS ROUTES */}
<Route path="/contacts" element={<ContactsList />} />
<Route path="/contacts/new" element={<ContactFormPage />} />
<Route path="/contacts/edit/:id" element={<ContactEditPage />} />
{/* ACCOUNTS ROUTES */}
<Route path="/accounts" element={<AccountList />} />
<Route path="/accounts/new" element={<AccountForm />} />
<Route path="/accounts/edit/:id" element={<AccountEdit />} />
{/* EMPLOYEES ROUTES */}
<Route path="/employees" element={<EmployeeList />} />
<Route path="/employees/new" element={<EmployeeForm />} />
<Route path="/employees/edit/:id" element={<EmployeeEdit />} />
{/* FOLLOW UPS */}
<Route path="/followups" element={<FollowUpList />} />
<Route path="/followups/new" element={<FollowUpForm />} />
<Route path="/followups/edit/:id" element={<FollowUpEdit />} />
{/* PM SCHEDULES */}
<Route path="/pmschedules" element={<PMScheduleList />} />
<Route path="/pmschedules/new" element={<PMScheduleForm />} />
<Route path="/pmschedules/edit/:id" element={<PMScheduleEdit />} />
</Routes>
</div>
</div>
</div>
</div>
</BrowserRouter>
);
}
}

View file

@ -23,7 +23,7 @@ export const get = (url) =>
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
// throw new Error(ex.message || API_ERROR_MESSAGE);
});
export const post = (url, payload) =>
@ -34,7 +34,7 @@ export const post = (url, payload) =>
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
// throw new Error(ex.message || API_ERROR_MESSAGE);
});
export const postFormBody = (url, payload) => {
@ -53,7 +53,7 @@ export const post = (url, payload) =>
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
// throw new Error(ex.message || API_ERROR_MESSAGE);
});
};
@ -65,7 +65,7 @@ export const put = (url, payload) =>
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
// throw new Error(ex.message || API_ERROR_MESSAGE);
});
export const putFormBody = (url, payload) => {
@ -84,7 +84,7 @@ export const putFormBody = (url, payload) => {
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
// throw new Error(ex.message || API_ERROR_MESSAGE);
});
}
export const deleteItem = (url, payload) =>
@ -95,14 +95,14 @@ export const deleteItem = (url, payload) =>
})
.then(handleHttpResponse)
.catch((ex) => {
throw new Error(ex.message || API_ERROR_MESSAGE);
// throw new Error(ex.message || API_ERROR_MESSAGE);
});
const handleHttpResponse = (response) => {
if (response.ok)
return response.json();
else if (response.status === 401) {
throw new Error("You are not authorized to access this page.");
//throw new Error("You are not authorized to access this page.");
}
throw new Error(API_ERROR_MESSAGE);
};

View file

@ -1,4 +1,5 @@
import React from "react";
import { NavLink } from "react-router-dom";
import {
FaTachometerAlt,
FaCalendarAlt,
@ -15,143 +16,49 @@ import {
} from "react-icons/fa";
const navItems = [
{ label: "Dashboard", icon: <FaTachometerAlt /> },
{ label: "Calendar", icon: <FaCalendarAlt /> },
{ label: "Documents", icon: <FaFileAlt /> },
{ label: "Employees", icon: <FaUserTie /> },
{ label: "Accounts", icon: <FaMoneyCheckAlt /> },
{ label: "Contacts", icon: <FaAddressBook /> },
{ label: "Reports", icon: <FaChartBar /> },
{ label: "Work Orders", icon: <FaTools /> },
{ label: "Follow Ups", icon: <FaBell /> },
{ label: "PM Schedules", icon: <FaClipboardList /> },
{ label: "Assets", icon: <FaCubes /> },
{ label: "Locations", icon: <FaMapMarkerAlt /> },
{ label: "Dashboard", icon: <FaTachometerAlt />, path: "/" },
{ label: "Calendar", icon: <FaCalendarAlt />, path: "/calendar" },
{ label: "Documents", icon: <FaFileAlt />, path: "/documents" },
{ label: "Employees", icon: <FaUserTie />, path: "/employees" },
{ label: "Accounts", icon: <FaMoneyCheckAlt />, path: "/accounts" },
{ label: "Contacts", icon: <FaAddressBook />, path: "/contacts" },
{ label: "Reports", icon: <FaChartBar />, path: "/reports" },
{ label: "Work Orders", icon: <FaTools />, path: "/workorders" },
{ label: "Follow Ups", icon: <FaBell />, path: "/followups" },
{ label: "PM Schedules", icon: <FaClipboardList />, path: "/pmschedules" },
{ label: "Assets", icon: <FaCubes />, path: "/assets" },
{ label: "Locations", icon: <FaMapMarkerAlt />, path: "/locations" },
];
export default function Sidebar() {
const [selected, setSelected] = React.useState("Contacts");
return (
<aside
className="sidebar"
style={{
width: 250,
height: "100vh",
backgroundColor: "#fbfbfb", // left nav background color
color: "#333",
display: "flex",
flexDirection: "column",
padding: 20,
boxSizing: "border-box",
userSelect: "none",
}}
>
<div
className="logoRow"
style={{
display: "flex",
alignItems: "center",
marginBottom: 30,
gap: 10,
userSelect: "none",
}}
>
<div
className="logoMark"
style={{
fontSize: 28,
fontWeight: "bold",
color: "#e74c3c",
}}
>
SEA
</div>
<div
className="logoText"
style={{
fontSize: 18,
lineHeight: 1.2,
color: "#333",
}}
>
Haven
<br />
<span style={{ fontSize: 14, fontWeight: "normal", color: "#666" }}>
Industries
</span>
</div>
<aside className="sidebar">
{/* Logo */}
<div className="logoRow">
<img
src="/logo.png"
alt="SeaHaven Industries"
className="sidebarLogo"
/>
</div>
<div
className="navTitle"
style={{
fontSize: 14,
fontWeight: "bold",
marginBottom: 15,
letterSpacing: 1.5,
color: "#888",
userSelect: "none",
}}
>
NAVIGATION
</div>
<div className="navTitle">NAVIGATION</div>
<ul
className="navList"
style={{
listStyle: "none",
padding: 0,
margin: 0,
flexGrow: 1,
}}
>
{navItems.map(({ label, icon }, i) => {
const isActive = selected === label;
return (
<li
key={i}
className={`navItem ${isActive ? "active" : ""}`}
style={{
display: "flex",
alignItems: "center",
padding: "10px 15px",
marginBottom: 8,
borderRadius: 6,
cursor: "pointer",
backgroundColor: isActive ? "#eee" : "transparent", // selected bg #eee
color: "#333",
fontWeight: isActive ? "bold" : "normal",
userSelect: "none",
transition: "background-color 0.2s",
}}
onClick={() => setSelected(label)}
onMouseEnter={(e) => {
if (!isActive) e.currentTarget.style.backgroundColor = "#f5f5f5";
}}
onMouseLeave={(e) => {
if (!isActive) e.currentTarget.style.backgroundColor = "transparent";
}}
<ul className="navList">
{navItems.map(({ label, icon, path }) => (
<li key={path}>
<NavLink
to={path}
className={({ isActive }) =>
`navItem ${isActive ? "active" : ""}`
}
>
<span
className="glyph"
style={{
marginRight: 12,
fontSize: 18,
display: "inline-flex",
width: 20,
justifyContent: "center",
color: isActive ? "#999" : "#aaa",
userSelect: "none",
}}
>
{icon}
</span>
<span>{label}</span>
</li>
);
})}
<span className="glyph">{icon}</span>
<span className="navLabel">{label}</span>
</NavLink>
</li>
))}
</ul>
</aside>
);
}
}

View file

@ -1,20 +1,30 @@
import React from "react";
export default function Topbar(){
export default function Topbar({ onToggleNav }) {
return (
<header className="topbar">
<div className="topLeft">
<button className="hamburger">☰</button>
<button
className="hamburger"
onClick={onToggleNav}
aria-label="Toggle navigation"
type="button"
>
☰
</button>
<div className="searchBox">
<input placeholder="Search by name or number..." />
</div>
</div>
<div className="topRight">
<div className="notif">🔔<span className="dot">1</span></div>
<div className="notif">
🔔<span className="dot">1</span>
</div>
<div className="username">Adam Moussa</div>
<div className="avatar">AM</div>
</div>
</header>
);
}
}

View file

@ -1,136 +1,307 @@
:root{
/* =========================================================
ROOT + RESET
========================================================= */
:root {
--sidebar-w: 220px;
--sidebar-collapsed-w: 56px;
--topbar-h: 64px;
--blue:#0b4b6a;
--muted:#f2f4f6;
--blue-main: #0c4f6f;
--blue-dark: #083a52;
--border: #d9dde0;
--bg: #f4f6f7;
}
*{box-sizing:border-box}
html,body,#root{height:100%}
body{
margin:0;
* {
box-sizing: border-box;
}
html, body, #root {
height: 100%;
}
body {
margin: 0;
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background:#f4f6f7;
color:#333;
font-size: 13px;
background: var(--bg);
color: #333;
}
/* Layout */
.app{display:flex;min-height:100vh;align-items:stretch}
.sidebar{
width:var(--sidebar-w);
background:#f1f3f4;
border-right:1px solid #e0e3e4;
padding:18px 12px;
position:fixed;
left:0;top:0;bottom:0;
overflow:auto;
}
.mainArea{
margin-left:var(--sidebar-w);
flex:1;
min-height:100vh;
display:flex;
flex-direction:column;
}
.topbar{
height:var(--topbar-h);
background:var(--blue);
color:white;
display:flex;
align-items:center;
justify-content:space-between;
padding:0 50px;
position:sticky;
top:0;
z-index:10;
}
.topbar .hamburger{background:transparent;border:none;color:white;font-size:20px;margin-right:12px}
.topbar .searchBox{background:#083a52; padding:6px 10px;border-radius:6px;min-width:420px;}
.topbar input{width:100%;border:none;padding:6px 8px;border-radius:4px;background:white;color:#333}
.topRight{display:flex;align-items:center;gap:12px}
.topRight .avatar{background:#1fa8de;color:white;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:600}
.topRight .notif{position:relative}
.topRight .dot{position:absolute;right:-6px;top:-6px;background:#ffb400;color:#222;padding:2px 6px;border-radius:10px;font-size:11px}
.topLeft {display: flex;}
/* Sidebar content */
.logoRow{display:flex;align-items:center;gap:8px;padding-bottom:16px}
.logoMark{width:48px;height:48px;background:linear-gradient(90deg,#60c6ff,#1fa8de);color:white;border-radius:6px;display:flex;align-items:center;justify-content:center;font-weight:800}
.logoText{font-size:14px;line-height:1}
.logoText span{font-size:11px;color:#556}
.navTitle{font-size:12px;color:#7b8588;margin:10px 6px}
.navList{list-style:none;padding:0;margin:0}
.navItem{display:flex;align-items:center;gap:10px;padding:10px;border-radius:6px;color:#394647;cursor:pointer}
.navItem .glyph{display:inline-block;width:22px;text-align:center;color:#8b9496}
.navItem:hover{background:#e9edf0}
.navItem.active{background:#e1edf4;border-left:3px solid #0b80b5}
/* Content */
.contentWrapper{padding:20px 40px;}
/* Contacts page */
.contactsPage .pageTitle{font-size:18px;font-weight:600;margin-bottom:10px}
.card.controlsCard{
background:white;
border-radius:4px;
padding:18px;
box-shadow:0 1px 0 rgba(0,0,0,0.06);
border:1px solid #e6e9ea;
/* =========================================================
APP LAYOUT
========================================================= */
.app {
display: flex;
min-height: 100vh;
width: 100%;
}
/* controls */
.controlsTop{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
.leftControls{display:flex;align-items:center;gap:12px}
.iconBtn{border:1px solid #d6d9da;background:#fff;padding:8px;border-radius:4px}
.filter{display:flex;gap:8px;align-items:center}
.filter select{padding:8px;border-radius:4px;border:1px solid #d8d8d8;background:#fff}
.filter input{padding:8px;border-radius:4px;border:1px solid #d8d8d8;min-width:360px}
.searchBtn{background:#0b95d4;color:#fff;border:none;padding:8px 12px;border-radius:4px}
.rightControls{display:flex;gap:8px;align-items:center}
.iconSmall{background:transparent;border:1px solid #e0e0e0;padding:6px;border-radius:6px;cursor:pointer}
/* table */
.tableWrap{margin-top:6px}
.contactTable{width:100%;border-collapse:collapse}
.contactTable thead th{
text-align:left;padding:12px;background:#9fb2bf;color:white;font-weight:600;border-bottom:1px solid #d5d8d9;
/* =========================================================
SIDEBAR
========================================================= */
.sidebar {
width: var(--sidebar-w);
height: 100vh;
background: #fbfbfb;
border-right: 1px solid var(--border);
padding: 20px;
display: flex;
flex-direction: column;
flex-shrink: 0;
transition: width .25s ease;
}
.contactTable thead .colCheck{width:48px}
.contactTable tbody td{border-bottom:1px solid #eee;padding:14px 12px;vertical-align:middle}
.contactTable tbody tr:nth-child(odd){background:#fff}
.contactTable tbody tr:nth-child(even){background:#fbfbfb}
.rowName{display:flex;align-items:center;gap:12px}
.avatarThumb{width:40px;height:40px;border-radius:6px;background:#f0b380;border:1px solid #9b6b45;display:flex;align-items:center;justify-content:center;color:#4b2b18;font-weight:700}
.nameText .nameLink{color:#0b6fa1;font-weight:600}
.nameText .sub{font-size:12px;color:#8f9a9f}
.mono{font-family:monospace;color:#444}
.link{color:#0a8ab6}
.actionsCol{width:120px;text-align:right}
/* footer of table */
.tableFooter{display:flex;justify-content:space-between;align-items:center;margin-top:12px;padding:8px 6px;color:#666;font-size:13px}
.tableFooter select{margin-left:8px;padding:6px}
/* rightPanel */
.rightPanel{
width:320px;
position:fixed;
right:0;
top:0;
bottom:0;
background:#222b34;
color:white;
border-left:1px solid rgba(255,255,255,0.04);
padding:18px;
overflow:auto;
/* Collapsed width */
.app.nav-collapsed .sidebar {
width: var(--sidebar-collapsed-w);
}
/* Sidebar logo */
.sidebarLogo {
max-width: 140px;
height: auto;
display: block;
}
/* Center logo when collapsed */
.app.nav-collapsed .sidebarLogo {
max-width: 32px;
margin: 0 auto;
}
/* =========================================================
LOGO
========================================================= */
.logoRow {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 28px;
}
.logoMark {
font-size: 26px;
font-weight: bold;
color: #e74c3c;
}
.logoText {
font-size: 17px;
line-height: 1.2;
}
.logoText span {
font-size: 13px;
color: #666;
}
/* =========================================================
NAVIGATION
========================================================= */
.navTitle {
font-size: 12px;
font-weight: bold;
letter-spacing: 1.5px;
color: #888;
margin-bottom: 14px;
}
.navList {
list-style: none;
padding: 0;
margin: 0;
flex-grow: 1;
}
/* Reset anchor defaults */
.navList a,
.navList a:visited,
.navList a:hover,
.navList a:active {
text-decoration: none;
color: #333;
}
/* Nav item */
.navItem {
display: flex;
align-items: center;
padding: 10px 14px;
border-radius: 6px;
margin-bottom: 6px;
font-weight: normal;
transition: background-color .15s ease;
}
/* Hover */
.navItem:hover {
background: #eef2f4;
}
/* Active (expanded only) */
.navItem.active {
background: #e6f1f7;
font-weight: 600;
border-left: 3px solid #0b80b5;
}
/* Icon */
.glyph {
font-size: 18px;
width: 22px;
display: inline-flex;
justify-content: center;
margin-right: 12px;
color: #8c949a;
}
/* Label */
.navLabel {
white-space: nowrap;
}
/* =========================================================
COLLAPSED SIDEBAR (LEGACY BEHAVIOR)
========================================================= */
/* Hide text only */
.app.nav-collapsed .navLabel,
.app.nav-collapsed .logoText,
.app.nav-collapsed .navTitle {
display: none;
}
/* Center icons */
.app.nav-collapsed .navItem {
justify-content: center;
padding-left: 0;
padding-right: 0;
}
.app.nav-collapsed .glyph {
margin-right: 0;
}
/* 🔥 REMOVE ACTIVE STYLING WHEN COLLAPSED (CRITICAL FIX) */
.app.nav-collapsed .navItem.active {
background: transparent;
border-left: none;
font-weight: normal;
}
/* Hover only (legacy) */
.app.nav-collapsed .navItem:hover {
background: #eef2f4;
}
/* =========================================================
MAIN AREA (NEVER MOVES)
========================================================= */
.mainArea {
flex: 1 1 auto;
min-width: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* =========================================================
TOPBAR
========================================================= */
.topbar {
height: var(--topbar-h);
background: linear-gradient(var(--blue-main), var(--blue-dark));
border-bottom: 1px solid #062c3e;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
color: #fff;
}
.topLeft {
display: flex;
align-items: center;
}
.hamburger {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
margin-right: 12px;
}
.searchBox {
background: #083a52;
padding: 4px;
border-radius: 4px;
min-width: 420px;
transition: min-width .25s ease;
}
.app.nav-collapsed .searchBox {
min-width: 300px;
}
.searchBox input {
width: 100%;
padding: 6px 8px;
border: none;
border-radius: 3px;
}
/* Top right */
.topRight {
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #1fa8de;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
}
/* =========================================================
CONTENT
========================================================= */
.contentWrapper {
padding: 10px;
}
/* =========================================================
TABLE (CONTACTS / EMPLOYEES)
========================================================= */
.contactTable {
width: 100%;
border-collapse: collapse;
}
.contactTable thead th {
padding: 10px 12px;
background: linear-gradient(#a9bdca, #8fa6b4);
color: #fff;
font-weight: 600;
border-bottom: 1px solid #7d96a6;
}
.contactTable tbody td {
padding: 10px 12px;
border-bottom: 1px solid #eee;
}
.contactTable tbody tr:nth-child(odd) {
background: #fff;
}
.contactTable tbody tr:nth-child(even) {
background: #fbfbfb;
}
.panelHeader{background:#623be3;padding:12px;border-radius:6px;text-align:center;margin-bottom:14px;font-weight:700;color:white}
.panelBody{padding:6px}
.panelInput{width:100%;padding:10px;border-radius:4px;border:none;background:#333;color:#ddd;margin-bottom:12px}
.panelRow{margin:10px 0}
.allianceList{margin-top:14px}
.allianceRow{display:flex;align-items:center;gap:10px;padding:8px;background:#2a3138;border-radius:6px;margin-bottom:10px;justify-content:space-between}
.pill{padding:6px 10px;border-radius:6px;color:white;font-weight:700}
.pill.blue{background:#6fb3e6}
.pill.purple{background:#7f63e3}
.pill.green{background:#2b9b59}
.numbers{color:#d5dbe0}

View file

@ -6,7 +6,5 @@ import "./index.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);

View file

@ -1,74 +0,0 @@
import React, { useState } from 'react';
import {
Paper, Typography, Box, Table, TableHead, TableRow, TableCell,
TableBody, TableContainer, IconButton, Checkbox, TablePagination, TextField, Button
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import sampleContacts from '../data/contacts';
export default function ContactsPage() {
const [rows] = useState(sampleContacts);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [query, setQuery] = useState('');
const filtered = rows.filter(r => r.name.toLowerCase().includes(query.toLowerCase()) || r.email.toLowerCase().includes(query.toLowerCase()));
return (
<Paper elevation={1} sx={{ p: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">Contacts</Typography>
<Button variant="contained" startIcon={<AddIcon />}>Add</Button>
</Box>
<Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
<TextField placeholder="Search contacts..." variant="outlined" size="small" value={query} onChange={(e)=>setQuery(e.target.value)} sx={{ flex: 1 }} />
<Button variant="outlined">Filter</Button>
</Box>
<TableContainer>
<Table>
<TableHead sx={{ bgcolor: '#9fb0bd' }}>
<TableRow>
<TableCell padding="checkbox"><Checkbox /></TableCell>
<TableCell>Name</TableCell>
<TableCell>Phone Number</TableCell>
<TableCell>Email</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filtered.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map(row => (
<TableRow key={row.id}>
<TableCell padding="checkbox"><Checkbox /></TableCell>
<TableCell>{row.name}</TableCell>
<TableCell>{row.phone}</TableCell>
<TableCell>{row.email}</TableCell>
<TableCell align="right">
<IconButton size="small"><EditIcon fontSize="small" /></IconButton>
<IconButton size="small"><DeleteIcon fontSize="small" /></IconButton>
</TableCell>
</TableRow>
))}
{filtered.length === 0 && <TableRow><TableCell colSpan={5} align="center">No contacts</TableCell></TableRow>}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={filtered.length}
page={page}
onPageChange={(e, p) => setPage(p)}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={(e) => { setRowsPerPage(parseInt(e.target.value, 10)); setPage(0); }}
rowsPerPageOptions={[5,10,25,50]}
sx={{ mt: 1 }}
/>
</Paper>
);
}

View file

@ -0,0 +1,6 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const editEmployee = async (payload) => {
return post(`${API_URL}/Employee/UpdateEmployee`, payload);
};

View file

@ -0,0 +1,180 @@
/* Wrapper & card */
.contactFormWrapper {
display: flex;
justify-content: center;
padding: 25px 20px;
background: #f5f6fa;
min-height: 100vh;
font-family: "Roboto", "Open Sans", Arial, sans-serif;
}
.formCard {
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
width: 100%;
max-width: 1100px;
padding: 20px 22px 26px;
border: 1px solid #e6e9ee;
}
/* Header */
.formHeader {
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
margin-bottom: 16px;
}
.formHeader h2 {
margin: 0;
color: #1f3b4d;
font-size: 20px;
font-weight: 600;
}
/* Section titles */
.sectionTitle {
margin: 0 0 12px;
color: #2b2f3b;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 700;
}
/* Account widget (assignment) */
.account-widget {
display: flex;
gap: 14px;
align-items: center;
padding: 12px;
border-radius: 4px;
background: #fbfcfd;
border: 1px dashed #d6e6ef;
color: #0a4b6b;
}
.account-icon {
width: 48px;
height: 48px;
background: #e9f4fb;
color: #0077a8;
border-radius: 4px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.account-info .account-title {
font-weight:700;
}
.account-info .account-action {
color: #0b63a7;
font-weight:700;
margin-top:4px;
}
.account-sub {
color:#667085;
font-size:12px;
margin-top:4px;
}
/* Form layout */
.contactFormBody {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 12px 18px;
align-items: start;
margin-bottom: 6px;
}
/* Form groups */
.formGroup {
display: flex;
flex-direction: column;
margin-bottom: 8px;
}
.formGroup.fullWidth {
grid-column: 1 / -1;
}
/* Labels and controls */
.formLabel {
font-size: 13px;
font-weight: 600;
color: #253244;
margin-bottom: 6px;
}
.required { color: #d93025; margin-left: 4px; }
.formControl {
display: block;
width: 100%;
height: 36px;
padding: 6px 10px;
font-size: 13px;
color: #1f2937;
background: #fff;
border: 1px solid #d6dbe4;
border-radius: 4px;
box-sizing: border-box;
}
.formControl:focus {
outline: none;
border-color: #0b78b0;
box-shadow: 0 0 0 4px rgba(11,120,176,0.08);
}
/* Hints / muted */
.muted {
color: #6b7280;
font-size: 13px;
margin-bottom: 8px;
}
.formHint {
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Form actions */
.formActions {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e9eef3;
padding-top: 14px;
margin-top: 14px;
}
.formNote {
color: #555;
font-size: 13px;
}
.btnPrimary {
background-color: #198754;
border: 1px solid #198754;
color: #fff;
padding: 10px 18px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
}
.btnPrimary:disabled {
background-color: #c9c9c9;
border-color: #c9c9c9;
cursor: not-allowed;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.contactFormBody {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.contactFormBody {
grid-template-columns: 1fr;
}
.formCard {
padding: 16px;
}
}

View file

@ -0,0 +1,114 @@
// pages/pmschedules/form/PmScheduleFormPage.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { addPmSchedule, editPmSchedule, getPmScheduleById } from "../new/api";
import { toast } from "react-toastify";
import "./ContactFormPage.css";
export default function PmScheduleFormPage() {
const { id } = useParams();
const isEdit = !!id;
const navigate = useNavigate();
const [model, setModel] = useState({});
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isEdit) load();
}, [id]);
const load = async () => {
const res = await getPmScheduleById(id);
setModel(res.Data);
};
const handleChange = (e) => {
const { name, value } = e.target;
setModel(prev => ({ ...prev, [name]: value }));
};
const submit = async (e) => {
e.preventDefault();
if (!model.Name) {
toast.error("Name is required");
return;
}
setLoading(true);
try {
if (isEdit) {
await editPmSchedule({ ...model, Id: id });
toast.success("PM Schedule updated");
} else {
await addPmSchedule(model);
toast.success("PM Schedule created");
}
navigate("/pm-schedules");
} finally {
setLoading(false);
}
};
return (
<div className="contactFormWrapper">
<div className="formCard">
<div className="formHeader">
<h2>{isEdit ? "Edit PM Schedule" : "Create PM Schedule"}</h2>
</div>
<form onSubmit={submit}>
<h5 className="sectionTitle">General Details</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">Name *</label>
<input
className="formControl"
name="Name"
value={model.Name || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Trade</label>
<input
className="formControl"
name="Trade"
value={model.Trade || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Priority</label>
<input
className="formControl"
name="Priority"
value={model.Priority || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup fullWidth">
<label className="formLabel">Additional Details</label>
<textarea
className="formControl"
name="AdditionalDetails"
value={model.AdditionalDetails || ""}
onChange={handleChange}
/>
</div>
</div>
<div className="formActions">
<button className="btnPrimary" disabled={loading}>
{loading ? "Saving..." : "Save PM Schedule"}
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -0,0 +1,22 @@
import React from "react";
import { deleteContact } from "./api";
export default function DeleteModal({ open, item, onClose, onSuccess }) {
if(!open) return null;
const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
return (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

View file

@ -0,0 +1,63 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { fetchPmSchedulesPaged } from "./api";
export default function PmScheduleListPage() {
const navigate = useNavigate();
const [data, setData] = useState([]);
const [search, setSearch] = useState("");
useEffect(() => {
load();
}, []);
const load = async () => {
const res = await fetchPmSchedulesPaged({});
setData(res.rows);
};
return (
<div className="pageWrapper">
<div className="pageHeader">
<h2>PM Schedules</h2>
<button
className="btnPrimary"
onClick={() => navigate("/pm-schedules/new")}
>
New PM Schedule
</button>
</div>
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Trade</th>
<th>Status</th>
<th>Next Work Order</th>
<th></th>
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id}>
<td>{row.name}</td>
<td>{row.trade}</td>
<td>{row.status}</td>
<td>{row.nextWorkOrderDate}</td>
<td>
<button
onClick={() =>
navigate(`/pm-schedules/${row.id}/edit`)
}
>
Edit
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,37 @@
import React from "react";
export default function Table({ loading, error, rows, onEdit, onDelete,
page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
return (
<div className="tableWrap">
{error && <div style={{ color: "#b00020" }}>{error}</div>}
<table className="contactTable">
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>Name</th><th>Account</th><th>Title</th><th>Email</th><th>Phone</th><th>Owner</th><th>Actions</th>
</tr>
</thead>
<tbody>
{loading ? <tr><td colSpan={8}>Loading…</td></tr> :
rows.length === 0 ? <tr><td colSpan={8}>No records found.</td></tr> :
rows.map(r => (
<tr key={r.id}>
<td><input type="checkbox" /></td>
<td>{r.name}</td>
<td>{r.account}</td>
<td>{r.title}</td>
<td>{r.email}</td>
<td>{r.phoneNumber}</td>
<td>{r.owner}</td>
<td>
<button className="iconSmall" onClick={()=>onEdit(r)}>✎</button>
<button className="iconSmall" onClick={()=>onDelete(r)}>🗑</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,30 @@
// pages/pmschedules/list/api.js
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const fetchPmSchedulesPaged = async ({
search = "",
page = 1,
pageSize = 12,
}) => {
const res = await get(
`${API_URL}/PmSchedule/GetList?search=${encodeURIComponent(search)}&page=${page}&pageSize=${pageSize}`
);
const rows = (res?.Data || []).map(x => ({
id: x.Id,
name: x.Name,
trade: x.Trade,
status: x.Status,
nextWorkOrderDate: x.NextWorkOrderDate,
owner: x.Owner,
}));
return {
rows,
page: res.PageNumber,
pageSize: res.PageSize,
totalCount: res.TotalCount,
totalPages: res.TotalPages,
};
};

View file

@ -0,0 +1,6 @@
/* CSS for modals, table, and controls – minimal to work with existing global styles */
.contactTable { width:100%; border-collapse:collapse; }
.contactTable th, .contactTable td { border:1px solid #ddd; padding:8px; }
.modalRoot{position:fixed;inset:0;z-index:1000;}
.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}

View file

@ -0,0 +1,31 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { fetchContactsPaged } from "./api";
export default function useItems() {
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("");
fetchContactsPaged({ 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]);
}

View file

@ -0,0 +1,11 @@
import { get, post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const getPmScheduleById = (id) =>
get(`${API_URL}/PmSchedule/GetById?id=${id}`);
export const addPmSchedule = (payload) =>
post(`${API_URL}/PmSchedule/Create`, payload);
export const editPmSchedule = (payload) =>
post(`${API_URL}/PmSchedule/Edit`, payload);

View file

@ -0,0 +1,5 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const editAccount = (data) =>
post(`${API_URL}/Account/EditAccount`, data);

View file

@ -0,0 +1,223 @@
// src/pages/accounts/form/AccountFormPage.js
import React, { useState } from "react";
import { useNavigate, useParams } from "react-router";
import { toast } from "react-toastify";
import "./ContactFormPage.css"; // reuse same CSS
import { addAccount } from "../new/api";
import { editAccount } from "../edit/api";
const AccountFormPage = () => {
const { id } = useParams();
const navigate = useNavigate();
const isEdit = !!id;
const [loading, setLoading] = useState(false);
const [account, setAccount] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setAccount((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!account.AccountName) {
toast.error("Account Name is required.");
return;
}
try {
setLoading(true);
const payload = { ...account };
if (isEdit) {
await editAccount(payload);
toast.success("Account updated successfully!");
} else {
await addAccount(payload);
toast.success("Account created successfully!");
}
navigate("/accounts");
} catch (err) {
console.error(err);
toast.error("Something went wrong while saving the account.");
} finally {
setLoading(false);
}
};
return (
<div className="contactFormWrapper">
<div className="formCard">
<div className="formHeader">
<h2>{isEdit ? "Edit Account" : "Create an Account"}</h2>
</div>
<form className="contactForm" onSubmit={handleSubmit} noValidate>
{/* GENERAL DETAILS */}
<section className="section">
<h5 className="sectionTitle">General Details</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">
Account Name <span className="required">*</span>
</label>
<input
name="AccountName"
className="formControl"
placeholder="Enter account name"
value={account.AccountName || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Account Number</label>
<input
name="AccountNumber"
className="formControl"
placeholder="Enter account number"
value={account.AccountNumber || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Industry</label>
<input
name="Industry"
className="formControl"
placeholder="Enter industry"
value={account.Industry || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Website</label>
<input
name="Website"
className="formControl"
placeholder="https://example.com"
value={account.Website || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup fullWidth">
<label className="formLabel">Notes</label>
<input
name="Notes"
className="formControl"
placeholder="Enter account notes or description"
value={account.Notes || ""}
onChange={handleChange}
/>
</div>
</div>
</section>
<hr />
{/* ADDRESS */}
<section className="section">
<h5 className="sectionTitle">Address</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">Address 1</label>
<input
name="Address1"
className="formControl"
placeholder="Enter street address"
value={account.Address1 || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Address 2</label>
<input
name="Address2"
className="formControl"
placeholder="Suite/Apt/Unit"
value={account.Address2 || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">City</label>
<input
name="City"
className="formControl"
placeholder="Enter city"
value={account.City || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">State / Province</label>
<input
name="State"
className="formControl"
placeholder="State"
value={account.State || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Zip/Postal Code</label>
<input
name="Zip"
className="formControl"
placeholder="Zip code"
value={account.Zip || ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Country</label>
<select
name="Country"
className="formControl"
value={account.Country || "US"}
onChange={handleChange}
>
<option value="">Select Country</option>
<option value="US">United States</option>
</select>
</div>
</div>
</section>
<hr />
{/* SUBMIT */}
<div className="formActions">
<div className="formNote">
All fields marked with <span className="required">*</span> are required.
</div>
<button className="btnPrimary" type="submit" disabled={loading}>
{loading ? "Saving..." : isEdit ? "Update Account" : "Create Account"}
</button>
</div>
</form>
</div>
</div>
);
};
export default AccountFormPage;

View file

@ -0,0 +1,180 @@
/* Wrapper & card */
.contactFormWrapper {
display: flex;
justify-content: center;
padding: 25px 20px;
background: #f5f6fa;
min-height: 100vh;
font-family: "Roboto", "Open Sans", Arial, sans-serif;
}
.formCard {
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
width: 100%;
max-width: 1100px;
padding: 20px 22px 26px;
border: 1px solid #e6e9ee;
}
/* Header */
.formHeader {
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
margin-bottom: 16px;
}
.formHeader h2 {
margin: 0;
color: #1f3b4d;
font-size: 20px;
font-weight: 600;
}
/* Section titles */
.sectionTitle {
margin: 0 0 12px;
color: #2b2f3b;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 700;
}
/* Account widget (assignment) */
.account-widget {
display: flex;
gap: 14px;
align-items: center;
padding: 12px;
border-radius: 4px;
background: #fbfcfd;
border: 1px dashed #d6e6ef;
color: #0a4b6b;
}
.account-icon {
width: 48px;
height: 48px;
background: #e9f4fb;
color: #0077a8;
border-radius: 4px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.account-info .account-title {
font-weight:700;
}
.account-info .account-action {
color: #0b63a7;
font-weight:700;
margin-top:4px;
}
.account-sub {
color:#667085;
font-size:12px;
margin-top:4px;
}
/* Form layout */
.contactFormBody {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 12px 18px;
align-items: start;
margin-bottom: 6px;
}
/* Form groups */
.formGroup {
display: flex;
flex-direction: column;
margin-bottom: 8px;
}
.formGroup.fullWidth {
grid-column: 1 / -1;
}
/* Labels and controls */
.formLabel {
font-size: 13px;
font-weight: 600;
color: #253244;
margin-bottom: 6px;
}
.required { color: #d93025; margin-left: 4px; }
.formControl {
display: block;
width: 100%;
height: 36px;
padding: 6px 10px;
font-size: 13px;
color: #1f2937;
background: #fff;
border: 1px solid #d6dbe4;
border-radius: 4px;
box-sizing: border-box;
}
.formControl:focus {
outline: none;
border-color: #0b78b0;
box-shadow: 0 0 0 4px rgba(11,120,176,0.08);
}
/* Hints / muted */
.muted {
color: #6b7280;
font-size: 13px;
margin-bottom: 8px;
}
.formHint {
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Form actions */
.formActions {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e9eef3;
padding-top: 14px;
margin-top: 14px;
}
.formNote {
color: #555;
font-size: 13px;
}
.btnPrimary {
background-color: #198754;
border: 1px solid #198754;
color: #fff;
padding: 10px 18px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
}
.btnPrimary:disabled {
background-color: #c9c9c9;
border-color: #c9c9c9;
cursor: not-allowed;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.contactFormBody {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.contactFormBody {
grid-template-columns: 1fr;
}
.formCard {
padding: 16px;
}
}

View file

@ -0,0 +1,22 @@
import React from "react";
import { deleteContact } from "./api";
export default function DeleteModal({ open, item, onClose, onSuccess }) {
if(!open) return null;
const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
return (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

View file

@ -0,0 +1,52 @@
import React, { useEffect, useState } from "react";
import { fetchAccountsPaged, deleteAccount } from "./api";
export default function AccountList() {
const [data, setData] = useState([]);
useEffect(() => {
load();
}, []);
const load = async () => {
const res = await fetchAccountsPaged({});
setData(res.rows);
};
return (
<div>
<h2>Accounts</h2>
<button onClick={() => (window.location.href = "/accounts/new")}>
+ Add Account
</button>
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Industry</th>
<th>Phone</th>
<th></th>
</tr>
</thead>
<tbody>
{data.map((x) => (
<tr key={x.id}>
<td>{x.name}</td>
<td>{x.industry}</td>
<td>{x.phone}</td>
<td>
<button onClick={() => (window.location.href = `/accounts/${x.id}/edit`)}>
Edit
</button>
<button onClick={() => deleteAccount(x.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,37 @@
import React from "react";
export default function Table({ loading, error, rows, onEdit, onDelete,
page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
return (
<div className="tableWrap">
{error && <div style={{ color: "#b00020" }}>{error}</div>}
<table className="contactTable">
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>Name</th><th>Account</th><th>Title</th><th>Email</th><th>Phone</th><th>Owner</th><th>Actions</th>
</tr>
</thead>
<tbody>
{loading ? <tr><td colSpan={8}>Loading…</td></tr> :
rows.length === 0 ? <tr><td colSpan={8}>No records found.</td></tr> :
rows.map(r => (
<tr key={r.id}>
<td><input type="checkbox" /></td>
<td>{r.name}</td>
<td>{r.account}</td>
<td>{r.title}</td>
<td>{r.email}</td>
<td>{r.phoneNumber}</td>
<td>{r.owner}</td>
<td>
<button className="iconSmall" onClick={()=>onEdit(r)}>✎</button>
<button className="iconSmall" onClick={()=>onDelete(r)}>🗑</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,36 @@
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const fetchAccountsPaged = async ({
search = "",
page = 1,
pageSize = 12,
}) => {
const res = await get(
`${API_URL}/Account/GetAccountList?search=${encodeURIComponent(
search
)}&page=${page}&pageSize=${pageSize}`
);
const rows = (res?.data || res?.Data || []).map((x) => ({
id: x.Id,
name: x.Name ?? x.AccountName ?? "",
number: x.AccountNumber ?? "",
industry: x.Industry ?? "",
website: x.Website ?? "",
primaryContact: x.PrimaryContact ?? "",
phoneNumber: x.Phone ?? x.PhoneNumber ?? "",
owner: x.Owner ?? "",
}));
return {
rows,
page: res?.PageNumber ?? 1,
pageSize: res?.PageSize ?? pageSize,
totalCount: res?.TotalCount ?? rows.length,
totalPages: res?.TotalPages ?? 1,
};
};
export const deleteAccount = (id) =>
get(`${API_URL}/Account/DeleteAccount?id=${id}`);

View file

@ -0,0 +1,6 @@
/* CSS for modals, table, and controls – minimal to work with existing global styles */
.contactTable { width:100%; border-collapse:collapse; }
.contactTable th, .contactTable td { border:1px solid #ddd; padding:8px; }
.modalRoot{position:fixed;inset:0;z-index:1000;}
.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}

View file

@ -0,0 +1,31 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { fetchContactsPaged } from "./api";
export default function useItems() {
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("");
fetchContactsPaged({ 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]);
}

View file

@ -0,0 +1,5 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const addAccount = (data) =>
post(`${API_URL}/Account/AddAccount`, data);

View file

@ -0,0 +1,6 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const editEmployee = async (payload) => {
return post(`${API_URL}/Employee/UpdateEmployee`, payload);
};

View file

@ -0,0 +1,180 @@
/* Wrapper & card */
.contactFormWrapper {
display: flex;
justify-content: center;
padding: 25px 20px;
background: #f5f6fa;
min-height: 100vh;
font-family: "Roboto", "Open Sans", Arial, sans-serif;
}
.formCard {
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
width: 100%;
max-width: 1100px;
padding: 20px 22px 26px;
border: 1px solid #e6e9ee;
}
/* Header */
.formHeader {
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
margin-bottom: 16px;
}
.formHeader h2 {
margin: 0;
color: #1f3b4d;
font-size: 20px;
font-weight: 600;
}
/* Section titles */
.sectionTitle {
margin: 0 0 12px;
color: #2b2f3b;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 700;
}
/* Account widget (assignment) */
.account-widget {
display: flex;
gap: 14px;
align-items: center;
padding: 12px;
border-radius: 4px;
background: #fbfcfd;
border: 1px dashed #d6e6ef;
color: #0a4b6b;
}
.account-icon {
width: 48px;
height: 48px;
background: #e9f4fb;
color: #0077a8;
border-radius: 4px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.account-info .account-title {
font-weight:700;
}
.account-info .account-action {
color: #0b63a7;
font-weight:700;
margin-top:4px;
}
.account-sub {
color:#667085;
font-size:12px;
margin-top:4px;
}
/* Form layout */
.contactFormBody {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 12px 18px;
align-items: start;
margin-bottom: 6px;
}
/* Form groups */
.formGroup {
display: flex;
flex-direction: column;
margin-bottom: 8px;
}
.formGroup.fullWidth {
grid-column: 1 / -1;
}
/* Labels and controls */
.formLabel {
font-size: 13px;
font-weight: 600;
color: #253244;
margin-bottom: 6px;
}
.required { color: #d93025; margin-left: 4px; }
.formControl {
display: block;
width: 100%;
height: 36px;
padding: 6px 10px;
font-size: 13px;
color: #1f2937;
background: #fff;
border: 1px solid #d6dbe4;
border-radius: 4px;
box-sizing: border-box;
}
.formControl:focus {
outline: none;
border-color: #0b78b0;
box-shadow: 0 0 0 4px rgba(11,120,176,0.08);
}
/* Hints / muted */
.muted {
color: #6b7280;
font-size: 13px;
margin-bottom: 8px;
}
.formHint {
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Form actions */
.formActions {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e9eef3;
padding-top: 14px;
margin-top: 14px;
}
.formNote {
color: #555;
font-size: 13px;
}
.btnPrimary {
background-color: #198754;
border: 1px solid #198754;
color: #fff;
padding: 10px 18px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
}
.btnPrimary:disabled {
background-color: #c9c9c9;
border-color: #c9c9c9;
cursor: not-allowed;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.contactFormBody {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.contactFormBody {
grid-template-columns: 1fr;
}
.formCard {
padding: 16px;
}
}

View file

@ -0,0 +1,189 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import { toast } from "react-toastify";
import { addEmployee } from "../new/api";
import { editEmployee } from "../edit/api";
import "./ContactFormPage.css";
export default function EmployeeFormPage() {
const { id } = useParams();
const isEdit = !!id;
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [employee, setEmployee] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setEmployee((p) => ({
...p,
[name]: type === "checkbox" ? checked : value,
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!employee.FirstName || !employee.LastName) {
toast.error("First & last name required");
return;
}
try {
setLoading(true);
if (isEdit) {
await editEmployee(employee);
toast.success("Employee updated!");
} else {
await addEmployee(employee);
toast.success("Employee created!");
}
navigate("/employees");
} catch (err) {
console.error(err);
toast.error("Operation failed.");
} finally {
setLoading(false);
}
};
return (
<div className="contactFormWrapper">
<div className="formCard">
<div className="formHeader">
<h2>{isEdit ? "Edit Employee" : "Create Employee"}</h2>
</div>
<form onSubmit={handleSubmit} className="contactForm">
{/* General Section */}
<section className="section">
<h5 className="sectionTitle">General Details</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">
First Name <span className="required">*</span>
</label>
<input
name="FirstName"
className="formControl"
value={employee.FirstName ?? ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Middle Name</label>
<input
name="MiddleName"
className="formControl"
value={employee.MiddleName ?? ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">
Last Name <span className="required">*</span>
</label>
<input
name="LastName"
className="formControl"
value={employee.LastName ?? ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Job Title</label>
<input
name="JobTitle"
className="formControl"
value={employee.JobTitle ?? ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Department</label>
<input
name="Department"
className="formControl"
value={employee.Department ?? ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Status</label>
<select
name="Status"
className="formControl"
value={employee.Status ?? ""}
onChange={handleChange}
>
<option value="">Select</option>
<option value="Active">Active</option>
<option value="Terminated">Terminated</option>
<option value="Leave">Leave</option>
</select>
</div>
<div className="formGroup fullWidth">
<label className="formLabel">About</label>
<input
name="About"
className="formControl"
value={employee.About ?? ""}
onChange={handleChange}
/>
</div>
</div>
</section>
<hr />
{/* Contact info */}
<section className="section">
<h5 className="sectionTitle">Contact Information</h5>
<div className="contactFormBody">
<div className="formGroup">
<label className="formLabel">Email</label>
<input
name="Email"
className="formControl"
value={employee.Email ?? ""}
onChange={handleChange}
/>
</div>
<div className="formGroup">
<label className="formLabel">Phone</label>
<input
name="Phone"
className="formControl"
value={employee.Phone ?? ""}
onChange={handleChange}
/>
</div>
</div>
</section>
<hr />
{/* Submit */}
<div className="formActions">
<div className="formNote">
All fields marked <span className="required">*</span> are required
</div>
<button className="btnPrimary" type="submit" disabled={loading}>
{loading ? "Saving..." : isEdit ? "Update Employee" : "Create Employee"}
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -0,0 +1,22 @@
import React from "react";
import { deleteContact } from "./api";
export default function DeleteModal({ open, item, onClose, onSuccess }) {
if(!open) return null;
const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
return (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

View file

@ -0,0 +1,92 @@
import React, { useEffect, useState } from "react";
import { fetchEmployeesPaged, deleteEmployee } from "./api";
import { useNavigate } from "react-router";
import { toast } from "react-toastify";
export default function EmployeeList() {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [rows, setRows] = useState([]);
const [search, setSearch] = useState("");
const load = async () => {
setLoading(true);
const res = await fetchEmployeesPaged({ search });
setRows(res.rows);
setLoading(false);
};
useEffect(() => {
load();
}, [search]);
const handleDelete = async (id) => {
if (!window.confirm("Delete this employee?")) return;
await deleteEmployee(id);
toast.success("Employee deleted");
load();
};
return (
<div className="pageWrapper">
<div className="pageHeader">
<h2>Employees</h2>
<button onClick={() => navigate("/employees/new")} className="btnPrimary">
+ New Employee
</button>
</div>
<input
className="searchBoxList"
placeholder="Search employees..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{loading ? (
<p>Loading employees...</p>
) : (
<table className="dataTable">
<thead>
<tr>
<th>Name</th>
<th>Job Title</th>
<th>Email</th>
<th>Phone</th>
<th>Department</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id}>
<td>{r.name}</td>
<td>{r.jobTitle}</td>
<td>{r.email}</td>
<td>{r.phone}</td>
<td>{r.department}</td>
<td>{r.status}</td>
<td>
<button
className="btnSmall"
onClick={() => navigate(`/employees/${r.id}`)}
>
Edit
</button>
<button
className="btnSmallDanger"
onClick={() => handleDelete(r.id)}
>
Del
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}

View file

@ -0,0 +1,37 @@
import React from "react";
export default function Table({ loading, error, rows, onEdit, onDelete,
page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
return (
<div className="tableWrap">
{error && <div style={{ color: "#b00020" }}>{error}</div>}
<table className="contactTable">
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>Name</th><th>Account</th><th>Title</th><th>Email</th><th>Phone</th><th>Owner</th><th>Actions</th>
</tr>
</thead>
<tbody>
{loading ? <tr><td colSpan={8}>Loading…</td></tr> :
rows.length === 0 ? <tr><td colSpan={8}>No records found.</td></tr> :
rows.map(r => (
<tr key={r.id}>
<td><input type="checkbox" /></td>
<td>{r.name}</td>
<td>{r.account}</td>
<td>{r.title}</td>
<td>{r.email}</td>
<td>{r.phoneNumber}</td>
<td>{r.owner}</td>
<td>
<button className="iconSmall" onClick={()=>onEdit(r)}>✎</button>
<button className="iconSmall" onClick={()=>onDelete(r)}>🗑</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,35 @@
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const fetchEmployeesPaged = async ({
search = "",
page = 1,
pageSize = 12,
}) => {
const res = await get(
`${API_URL}/Employee/GetEmployeeList?search=${encodeURIComponent(
search
)}&page=${page}&pageSize=${pageSize}`
);
const rows = (res?.data || res?.Data || []).map((x) => ({
id: x.Id,
name: `${x.FirstName ?? ""} ${x.LastName ?? ""}`.trim(),
jobTitle: x.JobTitle ?? "",
email: x.Email ?? "",
phone: x.Phone ?? "",
department: x.Department ?? "",
status: x.Status ?? "",
}));
return {
rows,
page: res?.PageNumber ?? 1,
pageSize: res?.PageSize ?? pageSize,
totalCount: res?.TotalCount ?? rows.length,
totalPages: res?.TotalPages ?? 1,
};
};
export const deleteEmployee = (id) =>
get(`${API_URL}/Employee/DeleteEmployee?id=${id}`);

View file

@ -0,0 +1,6 @@
/* CSS for modals, table, and controls – minimal to work with existing global styles */
.contactTable { width:100%; border-collapse:collapse; }
.contactTable th, .contactTable td { border:1px solid #ddd; padding:8px; }
.modalRoot{position:fixed;inset:0;z-index:1000;}
.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}

View file

@ -0,0 +1,31 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { fetchContactsPaged } from "./api";
export default function useItems() {
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("");
fetchContactsPaged({ 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]);
}

View file

@ -0,0 +1,6 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const addEmployee = async (payload) => {
return post(`${API_URL}/Employee/CreateEmployee`, payload);
};

View file

@ -0,0 +1,6 @@
import { post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const editEmployee = async (payload) => {
return post(`${API_URL}/Employee/UpdateEmployee`, payload);
};

View file

@ -0,0 +1,180 @@
/* Wrapper & card */
.contactFormWrapper {
display: flex;
justify-content: center;
padding: 25px 20px;
background: #f5f6fa;
min-height: 100vh;
font-family: "Roboto", "Open Sans", Arial, sans-serif;
}
.formCard {
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
width: 100%;
max-width: 1100px;
padding: 20px 22px 26px;
border: 1px solid #e6e9ee;
}
/* Header */
.formHeader {
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
margin-bottom: 16px;
}
.formHeader h2 {
margin: 0;
color: #1f3b4d;
font-size: 20px;
font-weight: 600;
}
/* Section titles */
.sectionTitle {
margin: 0 0 12px;
color: #2b2f3b;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 700;
}
/* Account widget (assignment) */
.account-widget {
display: flex;
gap: 14px;
align-items: center;
padding: 12px;
border-radius: 4px;
background: #fbfcfd;
border: 1px dashed #d6e6ef;
color: #0a4b6b;
}
.account-icon {
width: 48px;
height: 48px;
background: #e9f4fb;
color: #0077a8;
border-radius: 4px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.account-info .account-title {
font-weight:700;
}
.account-info .account-action {
color: #0b63a7;
font-weight:700;
margin-top:4px;
}
.account-sub {
color:#667085;
font-size:12px;
margin-top:4px;
}
/* Form layout */
.contactFormBody {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 12px 18px;
align-items: start;
margin-bottom: 6px;
}
/* Form groups */
.formGroup {
display: flex;
flex-direction: column;
margin-bottom: 8px;
}
.formGroup.fullWidth {
grid-column: 1 / -1;
}
/* Labels and controls */
.formLabel {
font-size: 13px;
font-weight: 600;
color: #253244;
margin-bottom: 6px;
}
.required { color: #d93025; margin-left: 4px; }
.formControl {
display: block;
width: 100%;
height: 36px;
padding: 6px 10px;
font-size: 13px;
color: #1f2937;
background: #fff;
border: 1px solid #d6dbe4;
border-radius: 4px;
box-sizing: border-box;
}
.formControl:focus {
outline: none;
border-color: #0b78b0;
box-shadow: 0 0 0 4px rgba(11,120,176,0.08);
}
/* Hints / muted */
.muted {
color: #6b7280;
font-size: 13px;
margin-bottom: 8px;
}
.formHint {
font-size: 12px;
color: #6b7280;
margin-top: 6px;
}
/* Form actions */
.formActions {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #e9eef3;
padding-top: 14px;
margin-top: 14px;
}
.formNote {
color: #555;
font-size: 13px;
}
.btnPrimary {
background-color: #198754;
border: 1px solid #198754;
color: #fff;
padding: 10px 18px;
font-size: 14px;
border-radius: 4px;
cursor: pointer;
}
.btnPrimary:disabled {
background-color: #c9c9c9;
border-color: #c9c9c9;
cursor: not-allowed;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.contactFormBody {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.contactFormBody {
grid-template-columns: 1fr;
}
.formCard {
padding: 16px;
}
}

View file

@ -0,0 +1,106 @@
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { getFollowUpById, saveFollowUp } from "../new/api";
export default function FollowUpFormPage() {
const navigate = useNavigate();
const { id } = useParams();
const [form, setForm] = useState({
id: 0,
followUpTypeId: 0,
workOrderId: null,
scheduleDate: "",
scheduleTime: "",
employeeId: "",
reason: "",
completeFollowUp: false,
});
useEffect(() => {
if (id) {
getFollowUpById(id).then((res) => {
setForm(res);
});
}
}, [id]);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setForm({
...form,
[name]: type === "checkbox" ? checked : value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
await saveFollowUp(form);
navigate("/followups");
};
return (
<div className="formPage">
<h2>{id ? "Edit Follow Up" : "Add Follow Up"}</h2>
<form onSubmit={handleSubmit}>
<label>Schedule Date</label>
<input
type="date"
name="scheduleDate"
value={form.scheduleDate}
onChange={handleChange}
required
/>
<label>Schedule Time</label>
<input
type="time"
name="scheduleTime"
value={form.scheduleTime}
onChange={handleChange}
required
/>
<label>Assigned To (Employee Id)</label>
<input
name="employeeId"
value={form.employeeId}
onChange={handleChange}
required
/>
<label>Reason</label>
<textarea
name="reason"
value={form.reason}
onChange={handleChange}
required
/>
<label>
<input
type="checkbox"
name="completeFollowUp"
checked={form.completeFollowUp}
onChange={handleChange}
/>
Complete Follow Up
</label>
<div className="formActions">
<button type="submit" className="btn btn-success">
Save
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => navigate("/followups")}
>
Cancel
</button>
</div>
</form>
</div>
);
}

View file

@ -0,0 +1,22 @@
import React from "react";
import { deleteContact } from "./api";
export default function DeleteModal({ open, item, onClose, onSuccess }) {
if(!open) return null;
const handleDelete=()=>{ deleteContact(item.id).then(()=>onSuccess()); };
return (
<div className="modalRoot">
<div className="modalScrim" onClick={onClose}/>
<div className="modalCard">
<div className="modalHeader"><div className="modalTitle">Delete Item?</div><button onClick={onClose}>✕</button></div>
<div className="modalBody">
<div>Are you sure you want to delete this contact?</div>
<div className="modalActions">
<button onClick={onClose}>Cancel</button>
<button onClick={handleDelete}>Delete</button>
</div>
</div>
</div>
</div>
);
}

View file

View file

@ -0,0 +1,98 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { fetchFollowUpsPaged, deleteFollowUp } from "../new/api";
export default function FollowUpList() {
const navigate = useNavigate();
const [rows, setRows] = useState([]);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const loadData = async () => {
const res = await fetchFollowUpsPaged({ search, page });
setRows(res.rows);
setTotalPages(res.totalPages);
};
useEffect(() => {
loadData();
}, [page]);
return (
<div>
<div className="pageHeader">
<h2>Follow Ups</h2>
<button
className="btn btn-primary"
onClick={() => navigate("/followups/new")}
>
+ Add Follow Up
</button>
</div>
<div className="searchRow">
<input
placeholder="Search follow ups..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button onClick={loadData}>Search</button>
</div>
<table className="table">
<thead>
<tr>
<th>Reason</th>
<th>Schedule Date</th>
<th>Account</th>
<th>Assigned To</th>
<th>Work Order</th>
<th>Status</th>
<th width="120">Actions</th>
</tr>
</thead>
<tbody>
{rows.map((x) => (
<tr key={x.id}>
<td>{x.reason}</td>
<td>{x.scheduleDate}</td>
<td>{x.account}</td>
<td>{x.assignedTo}</td>
<td>{x.workOrderNumber}</td>
<td>{x.status}</td>
<td>
<button onClick={() => navigate(`/followups/edit/${x.id}`)}>
Edit
</button>
<button onClick={() => deleteFollowUp(x.id)}>
Delete
</button>
</td>
</tr>
))}
{rows.length === 0 && (
<tr>
<td colSpan="7">No follow ups found</td>
</tr>
)}
</tbody>
</table>
<div className="pagination">
<button disabled={page === 1} onClick={() => setPage(page - 1)}>
Prev
</button>
<span>
Page {page} / {totalPages}
</span>
<button
disabled={page === totalPages}
onClick={() => setPage(page + 1)}
>
Next
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,37 @@
import React from "react";
export default function Table({ loading, error, rows, onEdit, onDelete,
page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
return (
<div className="tableWrap">
{error && <div style={{ color: "#b00020" }}>{error}</div>}
<table className="contactTable">
<thead>
<tr>
<th><input type="checkbox" /></th>
<th>Name</th><th>Account</th><th>Title</th><th>Email</th><th>Phone</th><th>Owner</th><th>Actions</th>
</tr>
</thead>
<tbody>
{loading ? <tr><td colSpan={8}>Loading…</td></tr> :
rows.length === 0 ? <tr><td colSpan={8}>No records found.</td></tr> :
rows.map(r => (
<tr key={r.id}>
<td><input type="checkbox" /></td>
<td>{r.name}</td>
<td>{r.account}</td>
<td>{r.title}</td>
<td>{r.email}</td>
<td>{r.phoneNumber}</td>
<td>{r.owner}</td>
<td>
<button className="iconSmall" onClick={()=>onEdit(r)}>✎</button>
<button className="iconSmall" onClick={()=>onDelete(r)}>🗑</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,49 @@
// src/pages/followups/followUpApi.js
import { get, post } from "../../apiUtil";
import { API_URL } from "../../constants";
export const fetchFollowUpsPaged = async ({
search = "",
page = 1,
pageSize = 12,
status = "Open"
}) => {
const res = await get(
`${API_URL}/FollowUp/GetFollowUps?search=${encodeURIComponent(
search
)}&page=${page}&pageSize=${pageSize}&status=${status}`
);
const rows = (res?.Data || []).map(x => ({
id: x.Id,
reason: x.Reason,
scheduleDate: x.ScheduleDate,
scheduleTime: x.ScheduleStartTime,
account: x.AccountName,
assignedTo: x.EmployeeName,
locationNumber: x.LocationNumber,
locationName: x.LocationName,
workOrderCode: x.WorkOrderCode,
status: x.Status
}));
return {
rows,
page: res.PageNumber,
pageSize: res.PageSize,
totalCount: res.TotalCount,
totalPages: res.TotalPages
};
};
export const createFollowUp = data =>
post(`${API_URL}/FollowUp/Create`, data);
export const updateFollowUp = data =>
post(`${API_URL}/FollowUp/Update`, data);
export const completeFollowUp = id =>
post(`${API_URL}/FollowUp/Complete?id=${id}`);
export const cancelFollowUp = id =>
post(`${API_URL}/FollowUp/Cancel?id=${id}`);

View file

@ -0,0 +1,6 @@
/* CSS for modals, table, and controls – minimal to work with existing global styles */
.contactTable { width:100%; border-collapse:collapse; }
.contactTable th, .contactTable td { border:1px solid #ddd; padding:8px; }
.modalRoot{position:fixed;inset:0;z-index:1000;}
.modalScrim{position:absolute;inset:0;background:rgba(0,0,0,.3);}
.modalCard{position:relative;margin:10% auto;padding:20px;background:#fff;width:400px;}

View file

@ -0,0 +1,31 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { fetchContactsPaged } from "./api";
export default function useItems() {
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("");
fetchContactsPaged({ 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]);
}

View file

@ -0,0 +1,43 @@
import { get, post } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const fetchFollowUpsPaged = async ({
search = "",
page = 1,
pageSize = 12,
completed = false,
}) => {
const res = await get(
`${API_URL}/FollowUp/GetFollowUpList?search=${encodeURIComponent(
search
)}&page=${page}&pageSize=${pageSize}&completed=${completed}`
);
const rows = (res?.Data || []).map((x) => ({
id: x.Id,
reason: x.Reason,
scheduleDate: x.ScheduleDate,
account: x.Account,
assignedTo: x.AssignedTo,
locationName: x.LocationName,
workOrderNumber: x.WorkOrderNumber,
status: x.Status,
}));
return {
rows,
page: res.PageNumber,
pageSize: res.PageSize,
totalCount: res.TotalCount,
totalPages: res.TotalPages,
};
};
export const getFollowUpById = (id) =>
get(`${API_URL}/FollowUp/GetById?id=${id}`);
export const saveFollowUp = (payload) =>
post(`${API_URL}/FollowUp/Save`, payload);
export const deleteFollowUp = (id) =>
get(`${API_URL}/FollowUp/Delete?id=${id}`);