mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 05:43:12 +00:00
commiting changes
This commit is contained in:
parent
3461d3159e
commit
27d68b50e9
52 changed files with 2699 additions and 352 deletions
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
80
src/App.js
80
src/App.js
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
419
src/index.css
419
src/index.css
|
|
@ -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}
|
||||
|
|
@ -6,7 +6,5 @@ import "./index.css";
|
|||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
6
src/pages/PmSchedule/edit/api.js
Normal file
6
src/pages/PmSchedule/edit/api.js
Normal 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);
|
||||
};
|
||||
180
src/pages/PmSchedule/form/ContactFormPage.css
Normal file
180
src/pages/PmSchedule/form/ContactFormPage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
114
src/pages/PmSchedule/form/PmScheduleFormPage.js
Normal file
114
src/pages/PmSchedule/form/PmScheduleFormPage.js
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/pages/PmSchedule/list/Delete.js
Normal file
22
src/pages/PmSchedule/list/Delete.js
Normal 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>
|
||||
);
|
||||
}
|
||||
0
src/pages/PmSchedule/list/Form.js
Normal file
0
src/pages/PmSchedule/list/Form.js
Normal file
63
src/pages/PmSchedule/list/List.js
Normal file
63
src/pages/PmSchedule/list/List.js
Normal 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>
|
||||
);
|
||||
}
|
||||
37
src/pages/PmSchedule/list/Table.js
Normal file
37
src/pages/PmSchedule/list/Table.js
Normal 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>
|
||||
);
|
||||
}
|
||||
30
src/pages/PmSchedule/list/api.js
Normal file
30
src/pages/PmSchedule/list/api.js
Normal 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,
|
||||
};
|
||||
};
|
||||
6
src/pages/PmSchedule/list/styles.css
Normal file
6
src/pages/PmSchedule/list/styles.css
Normal 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;}
|
||||
31
src/pages/PmSchedule/list/useItems.js
Normal file
31
src/pages/PmSchedule/list/useItems.js
Normal 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]);
|
||||
}
|
||||
11
src/pages/PmSchedule/new/api.js
Normal file
11
src/pages/PmSchedule/new/api.js
Normal 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);
|
||||
5
src/pages/accounts/edit/api.js
Normal file
5
src/pages/accounts/edit/api.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { post } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const editAccount = (data) =>
|
||||
post(`${API_URL}/Account/EditAccount`, data);
|
||||
223
src/pages/accounts/form/AccountFormPage.js
Normal file
223
src/pages/accounts/form/AccountFormPage.js
Normal 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;
|
||||
180
src/pages/accounts/form/ContactFormPage.css
Normal file
180
src/pages/accounts/form/ContactFormPage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/pages/accounts/list/Delete.js
Normal file
22
src/pages/accounts/list/Delete.js
Normal 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>
|
||||
);
|
||||
}
|
||||
0
src/pages/accounts/list/Form.js
Normal file
0
src/pages/accounts/list/Form.js
Normal file
52
src/pages/accounts/list/List.js
Normal file
52
src/pages/accounts/list/List.js
Normal 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>
|
||||
);
|
||||
}
|
||||
37
src/pages/accounts/list/Table.js
Normal file
37
src/pages/accounts/list/Table.js
Normal 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>
|
||||
);
|
||||
}
|
||||
36
src/pages/accounts/list/api.js
Normal file
36
src/pages/accounts/list/api.js
Normal 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}`);
|
||||
6
src/pages/accounts/list/styles.css
Normal file
6
src/pages/accounts/list/styles.css
Normal 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;}
|
||||
31
src/pages/accounts/list/useItems.js
Normal file
31
src/pages/accounts/list/useItems.js
Normal 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]);
|
||||
}
|
||||
5
src/pages/accounts/new/api.js
Normal file
5
src/pages/accounts/new/api.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { post } from "../../../apiUtil";
|
||||
import { API_URL } from "../../../constants";
|
||||
|
||||
export const addAccount = (data) =>
|
||||
post(`${API_URL}/Account/AddAccount`, data);
|
||||
6
src/pages/employees/edit/api.js
Normal file
6
src/pages/employees/edit/api.js
Normal 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);
|
||||
};
|
||||
180
src/pages/employees/form/ContactFormPage.css
Normal file
180
src/pages/employees/form/ContactFormPage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
189
src/pages/employees/form/EmployeeFormPage.js
Normal file
189
src/pages/employees/form/EmployeeFormPage.js
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/pages/employees/list/Delete.js
Normal file
22
src/pages/employees/list/Delete.js
Normal 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>
|
||||
);
|
||||
}
|
||||
0
src/pages/employees/list/Form.js
Normal file
0
src/pages/employees/list/Form.js
Normal file
92
src/pages/employees/list/List.js
Normal file
92
src/pages/employees/list/List.js
Normal 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>
|
||||
);
|
||||
}
|
||||
37
src/pages/employees/list/Table.js
Normal file
37
src/pages/employees/list/Table.js
Normal 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>
|
||||
);
|
||||
}
|
||||
35
src/pages/employees/list/api.js
Normal file
35
src/pages/employees/list/api.js
Normal 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}`);
|
||||
6
src/pages/employees/list/styles.css
Normal file
6
src/pages/employees/list/styles.css
Normal 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;}
|
||||
31
src/pages/employees/list/useItems.js
Normal file
31
src/pages/employees/list/useItems.js
Normal 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]);
|
||||
}
|
||||
6
src/pages/employees/new/api.js
Normal file
6
src/pages/employees/new/api.js
Normal 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);
|
||||
};
|
||||
6
src/pages/followup/edit/api.js
Normal file
6
src/pages/followup/edit/api.js
Normal 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);
|
||||
};
|
||||
180
src/pages/followup/form/ContactFormPage.css
Normal file
180
src/pages/followup/form/ContactFormPage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
106
src/pages/followup/form/FollowUpFormPage.js
Normal file
106
src/pages/followup/form/FollowUpFormPage.js
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/pages/followup/list/Delete.js
Normal file
22
src/pages/followup/list/Delete.js
Normal 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>
|
||||
);
|
||||
}
|
||||
0
src/pages/followup/list/Form.js
Normal file
0
src/pages/followup/list/Form.js
Normal file
98
src/pages/followup/list/List.js
Normal file
98
src/pages/followup/list/List.js
Normal 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>
|
||||
);
|
||||
}
|
||||
37
src/pages/followup/list/Table.js
Normal file
37
src/pages/followup/list/Table.js
Normal 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>
|
||||
);
|
||||
}
|
||||
49
src/pages/followup/list/api.js
Normal file
49
src/pages/followup/list/api.js
Normal 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}`);
|
||||
6
src/pages/followup/list/styles.css
Normal file
6
src/pages/followup/list/styles.css
Normal 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;}
|
||||
31
src/pages/followup/list/useItems.js
Normal file
31
src/pages/followup/list/useItems.js
Normal 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]);
|
||||
}
|
||||
43
src/pages/followup/new/api.js
Normal file
43
src/pages/followup/new/api.js
Normal 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}`);
|
||||
Loading…
Add table
Reference in a new issue