mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 11:39:00 +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 |
78
src/App.js
78
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 Sidebar from "./components/Sidebar";
|
||||||
import Topbar from "./components/Topbar";
|
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() {
|
export default function App() {
|
||||||
|
const [navCollapsed, setNavCollapsed] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<BrowserRouter>
|
||||||
<Sidebar />
|
<div className={`app ${navCollapsed ? "nav-collapsed" : ""}`}>
|
||||||
<div className="mainArea">
|
<Sidebar />
|
||||||
<Topbar />
|
|
||||||
<div className="contentWrapper">
|
<div className="mainArea">
|
||||||
<ContactFormPage />
|
<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>
|
</div>
|
||||||
</div>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@ export const get = (url) =>
|
||||||
})
|
})
|
||||||
.then(handleHttpResponse)
|
.then(handleHttpResponse)
|
||||||
.catch((ex) => {
|
.catch((ex) => {
|
||||||
throw new Error(ex.message || API_ERROR_MESSAGE);
|
// throw new Error(ex.message || API_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const post = (url, payload) =>
|
export const post = (url, payload) =>
|
||||||
|
|
@ -34,7 +34,7 @@ export const post = (url, payload) =>
|
||||||
})
|
})
|
||||||
.then(handleHttpResponse)
|
.then(handleHttpResponse)
|
||||||
.catch((ex) => {
|
.catch((ex) => {
|
||||||
throw new Error(ex.message || API_ERROR_MESSAGE);
|
// throw new Error(ex.message || API_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const postFormBody = (url, payload) => {
|
export const postFormBody = (url, payload) => {
|
||||||
|
|
@ -53,7 +53,7 @@ export const post = (url, payload) =>
|
||||||
})
|
})
|
||||||
.then(handleHttpResponse)
|
.then(handleHttpResponse)
|
||||||
.catch((ex) => {
|
.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)
|
.then(handleHttpResponse)
|
||||||
.catch((ex) => {
|
.catch((ex) => {
|
||||||
throw new Error(ex.message || API_ERROR_MESSAGE);
|
// throw new Error(ex.message || API_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const putFormBody = (url, payload) => {
|
export const putFormBody = (url, payload) => {
|
||||||
|
|
@ -84,7 +84,7 @@ export const putFormBody = (url, payload) => {
|
||||||
})
|
})
|
||||||
.then(handleHttpResponse)
|
.then(handleHttpResponse)
|
||||||
.catch((ex) => {
|
.catch((ex) => {
|
||||||
throw new Error(ex.message || API_ERROR_MESSAGE);
|
// throw new Error(ex.message || API_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export const deleteItem = (url, payload) =>
|
export const deleteItem = (url, payload) =>
|
||||||
|
|
@ -95,14 +95,14 @@ export const deleteItem = (url, payload) =>
|
||||||
})
|
})
|
||||||
.then(handleHttpResponse)
|
.then(handleHttpResponse)
|
||||||
.catch((ex) => {
|
.catch((ex) => {
|
||||||
throw new Error(ex.message || API_ERROR_MESSAGE);
|
// throw new Error(ex.message || API_ERROR_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleHttpResponse = (response) => {
|
const handleHttpResponse = (response) => {
|
||||||
if (response.ok)
|
if (response.ok)
|
||||||
return response.json();
|
return response.json();
|
||||||
else if (response.status === 401) {
|
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);
|
throw new Error(API_ERROR_MESSAGE);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
FaTachometerAlt,
|
FaTachometerAlt,
|
||||||
FaCalendarAlt,
|
FaCalendarAlt,
|
||||||
|
|
@ -15,142 +16,48 @@ import {
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ label: "Dashboard", icon: <FaTachometerAlt /> },
|
{ label: "Dashboard", icon: <FaTachometerAlt />, path: "/" },
|
||||||
{ label: "Calendar", icon: <FaCalendarAlt /> },
|
{ label: "Calendar", icon: <FaCalendarAlt />, path: "/calendar" },
|
||||||
{ label: "Documents", icon: <FaFileAlt /> },
|
{ label: "Documents", icon: <FaFileAlt />, path: "/documents" },
|
||||||
{ label: "Employees", icon: <FaUserTie /> },
|
{ label: "Employees", icon: <FaUserTie />, path: "/employees" },
|
||||||
{ label: "Accounts", icon: <FaMoneyCheckAlt /> },
|
{ label: "Accounts", icon: <FaMoneyCheckAlt />, path: "/accounts" },
|
||||||
{ label: "Contacts", icon: <FaAddressBook /> },
|
{ label: "Contacts", icon: <FaAddressBook />, path: "/contacts" },
|
||||||
{ label: "Reports", icon: <FaChartBar /> },
|
{ label: "Reports", icon: <FaChartBar />, path: "/reports" },
|
||||||
{ label: "Work Orders", icon: <FaTools /> },
|
{ label: "Work Orders", icon: <FaTools />, path: "/workorders" },
|
||||||
{ label: "Follow Ups", icon: <FaBell /> },
|
{ label: "Follow Ups", icon: <FaBell />, path: "/followups" },
|
||||||
{ label: "PM Schedules", icon: <FaClipboardList /> },
|
{ label: "PM Schedules", icon: <FaClipboardList />, path: "/pmschedules" },
|
||||||
{ label: "Assets", icon: <FaCubes /> },
|
{ label: "Assets", icon: <FaCubes />, path: "/assets" },
|
||||||
{ label: "Locations", icon: <FaMapMarkerAlt /> },
|
{ label: "Locations", icon: <FaMapMarkerAlt />, path: "/locations" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const [selected, setSelected] = React.useState("Contacts");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside className="sidebar">
|
||||||
className="sidebar"
|
{/* Logo */}
|
||||||
style={{
|
<div className="logoRow">
|
||||||
width: 250,
|
<img
|
||||||
height: "100vh",
|
src="/logo.png"
|
||||||
backgroundColor: "#fbfbfb", // left nav background color
|
alt="SeaHaven Industries"
|
||||||
color: "#333",
|
className="sidebarLogo"
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="navTitle">NAVIGATION</div>
|
||||||
className="navTitle"
|
|
||||||
style={{
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: "bold",
|
|
||||||
marginBottom: 15,
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
color: "#888",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
NAVIGATION
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul
|
<ul className="navList">
|
||||||
className="navList"
|
{navItems.map(({ label, icon, path }) => (
|
||||||
style={{
|
<li key={path}>
|
||||||
listStyle: "none",
|
<NavLink
|
||||||
padding: 0,
|
to={path}
|
||||||
margin: 0,
|
className={({ isActive }) =>
|
||||||
flexGrow: 1,
|
`navItem ${isActive ? "active" : ""}`
|
||||||
}}
|
}
|
||||||
>
|
|
||||||
{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";
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span
|
<span className="glyph">{icon}</span>
|
||||||
className="glyph"
|
<span className="navLabel">{label}</span>
|
||||||
style={{
|
</NavLink>
|
||||||
marginRight: 12,
|
</li>
|
||||||
fontSize: 18,
|
))}
|
||||||
display: "inline-flex",
|
|
||||||
width: 20,
|
|
||||||
justifyContent: "center",
|
|
||||||
color: isActive ? "#999" : "#aaa",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</span>
|
|
||||||
<span>{label}</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function Topbar(){
|
export default function Topbar({ onToggleNav }) {
|
||||||
return (
|
return (
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="topLeft">
|
<div className="topLeft">
|
||||||
<button className="hamburger">☰</button>
|
<button
|
||||||
|
className="hamburger"
|
||||||
|
onClick={onToggleNav}
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="searchBox">
|
<div className="searchBox">
|
||||||
<input placeholder="Search by name or number..." />
|
<input placeholder="Search by name or number..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="topRight">
|
<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="username">Adam Moussa</div>
|
||||||
<div className="avatar">AM</div>
|
<div className="avatar">AM</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
419
src/index.css
419
src/index.css
|
|
@ -1,136 +1,307 @@
|
||||||
:root{
|
/* =========================================================
|
||||||
|
ROOT + RESET
|
||||||
|
========================================================= */
|
||||||
|
:root {
|
||||||
--sidebar-w: 220px;
|
--sidebar-w: 220px;
|
||||||
|
--sidebar-collapsed-w: 56px;
|
||||||
--topbar-h: 64px;
|
--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%}
|
box-sizing: border-box;
|
||||||
body{
|
}
|
||||||
margin:0;
|
|
||||||
|
html, body, #root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background:#f4f6f7;
|
font-size: 13px;
|
||||||
color:#333;
|
background: var(--bg);
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout */
|
/* =========================================================
|
||||||
.app{display:flex;min-height:100vh;align-items:stretch}
|
APP LAYOUT
|
||||||
.sidebar{
|
========================================================= */
|
||||||
width:var(--sidebar-w);
|
.app {
|
||||||
background:#f1f3f4;
|
display: flex;
|
||||||
border-right:1px solid #e0e3e4;
|
min-height: 100vh;
|
||||||
padding:18px 12px;
|
width: 100%;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* controls */
|
/* =========================================================
|
||||||
.controlsTop{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
SIDEBAR
|
||||||
.leftControls{display:flex;align-items:center;gap:12px}
|
========================================================= */
|
||||||
.iconBtn{border:1px solid #d6d9da;background:#fff;padding:8px;border-radius:4px}
|
.sidebar {
|
||||||
.filter{display:flex;gap:8px;align-items:center}
|
width: var(--sidebar-w);
|
||||||
.filter select{padding:8px;border-radius:4px;border:1px solid #d8d8d8;background:#fff}
|
height: 100vh;
|
||||||
.filter input{padding:8px;border-radius:4px;border:1px solid #d8d8d8;min-width:360px}
|
background: #fbfbfb;
|
||||||
.searchBtn{background:#0b95d4;color:#fff;border:none;padding:8px 12px;border-radius:4px}
|
border-right: 1px solid var(--border);
|
||||||
.rightControls{display:flex;gap:8px;align-items:center}
|
padding: 20px;
|
||||||
.iconSmall{background:transparent;border:1px solid #e0e0e0;padding:6px;border-radius:6px;cursor:pointer}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
/* table */
|
flex-shrink: 0;
|
||||||
.tableWrap{margin-top:6px}
|
transition: width .25s ease;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
.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 */
|
/* Collapsed width */
|
||||||
.tableFooter{display:flex;justify-content:space-between;align-items:center;margin-top:12px;padding:8px 6px;color:#666;font-size:13px}
|
.app.nav-collapsed .sidebar {
|
||||||
.tableFooter select{margin-left:8px;padding:6px}
|
width: var(--sidebar-collapsed-w);
|
||||||
|
}
|
||||||
/* rightPanel */
|
/* Sidebar logo */
|
||||||
.rightPanel{
|
.sidebarLogo {
|
||||||
width:320px;
|
max-width: 140px;
|
||||||
position:fixed;
|
height: auto;
|
||||||
right:0;
|
display: block;
|
||||||
top:0;
|
}
|
||||||
bottom:0;
|
|
||||||
background:#222b34;
|
/* Center logo when collapsed */
|
||||||
color:white;
|
.app.nav-collapsed .sidebarLogo {
|
||||||
border-left:1px solid rgba(255,255,255,0.04);
|
max-width: 32px;
|
||||||
padding:18px;
|
margin: 0 auto;
|
||||||
overflow: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"));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
root.render(
|
root.render(
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
<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