+ );
+}
diff --git a/src/pages/accounts/list/Form.js b/src/pages/accounts/list/Form.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/accounts/list/List.js b/src/pages/accounts/list/List.js
new file mode 100644
index 00000000..c5e47b2c
--- /dev/null
+++ b/src/pages/accounts/list/List.js
@@ -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 (
+
+ );
+}
diff --git a/src/pages/accounts/list/Table.js b/src/pages/accounts/list/Table.js
new file mode 100644
index 00000000..bce57b8d
--- /dev/null
+++ b/src/pages/accounts/list/Table.js
@@ -0,0 +1,37 @@
+import React from "react";
+
+export default function Table({ loading, error, rows, onEdit, onDelete,
+ page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
+ return (
+
+ );
+}
diff --git a/src/pages/accounts/list/api.js b/src/pages/accounts/list/api.js
new file mode 100644
index 00000000..3bd62a40
--- /dev/null
+++ b/src/pages/accounts/list/api.js
@@ -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}`);
diff --git a/src/pages/accounts/list/styles.css b/src/pages/accounts/list/styles.css
new file mode 100644
index 00000000..801b23a9
--- /dev/null
+++ b/src/pages/accounts/list/styles.css
@@ -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;}
diff --git a/src/pages/accounts/list/useItems.js b/src/pages/accounts/list/useItems.js
new file mode 100644
index 00000000..ed8d1bf4
--- /dev/null
+++ b/src/pages/accounts/list/useItems.js
@@ -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]);
+}
diff --git a/src/pages/accounts/new/api.js b/src/pages/accounts/new/api.js
new file mode 100644
index 00000000..80a4ade5
--- /dev/null
+++ b/src/pages/accounts/new/api.js
@@ -0,0 +1,5 @@
+import { post } from "../../../apiUtil";
+import { API_URL } from "../../../constants";
+
+export const addAccount = (data) =>
+ post(`${API_URL}/Account/AddAccount`, data);
diff --git a/src/pages/employees/edit/api.js b/src/pages/employees/edit/api.js
new file mode 100644
index 00000000..7da1e39c
--- /dev/null
+++ b/src/pages/employees/edit/api.js
@@ -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);
+};
diff --git a/src/pages/employees/form/ContactFormPage.css b/src/pages/employees/form/ContactFormPage.css
new file mode 100644
index 00000000..790e2fc1
--- /dev/null
+++ b/src/pages/employees/form/ContactFormPage.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/employees/form/EmployeeFormPage.js b/src/pages/employees/form/EmployeeFormPage.js
new file mode 100644
index 00000000..1884fa50
--- /dev/null
+++ b/src/pages/employees/form/EmployeeFormPage.js
@@ -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 (
+
+ );
+}
diff --git a/src/pages/employees/list/Delete.js b/src/pages/employees/list/Delete.js
new file mode 100644
index 00000000..394ae1a9
--- /dev/null
+++ b/src/pages/employees/list/Delete.js
@@ -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 (
+
+
+
+
+
+
Are you sure you want to delete this contact?
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/employees/list/Form.js b/src/pages/employees/list/Form.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/employees/list/List.js b/src/pages/employees/list/List.js
new file mode 100644
index 00000000..637c534e
--- /dev/null
+++ b/src/pages/employees/list/List.js
@@ -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 (
+
+
+
Employees
+
+
+
+
setSearch(e.target.value)}
+ />
+
+ {loading ? (
+
Loading employees...
+ ) : (
+
+
+
+ | Name |
+ Job Title |
+ Email |
+ Phone |
+ Department |
+ Status |
+ |
+
+
+
+
+ {rows.map((r) => (
+
+ | {r.name} |
+ {r.jobTitle} |
+ {r.email} |
+ {r.phone} |
+ {r.department} |
+ {r.status} |
+
+
+
+ |
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/employees/list/Table.js b/src/pages/employees/list/Table.js
new file mode 100644
index 00000000..bce57b8d
--- /dev/null
+++ b/src/pages/employees/list/Table.js
@@ -0,0 +1,37 @@
+import React from "react";
+
+export default function Table({ loading, error, rows, onEdit, onDelete,
+ page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
+ return (
+
+ );
+}
diff --git a/src/pages/employees/list/api.js b/src/pages/employees/list/api.js
new file mode 100644
index 00000000..097ef582
--- /dev/null
+++ b/src/pages/employees/list/api.js
@@ -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}`);
diff --git a/src/pages/employees/list/styles.css b/src/pages/employees/list/styles.css
new file mode 100644
index 00000000..801b23a9
--- /dev/null
+++ b/src/pages/employees/list/styles.css
@@ -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;}
diff --git a/src/pages/employees/list/useItems.js b/src/pages/employees/list/useItems.js
new file mode 100644
index 00000000..ed8d1bf4
--- /dev/null
+++ b/src/pages/employees/list/useItems.js
@@ -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]);
+}
diff --git a/src/pages/employees/new/api.js b/src/pages/employees/new/api.js
new file mode 100644
index 00000000..5f3c6ca2
--- /dev/null
+++ b/src/pages/employees/new/api.js
@@ -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);
+};
diff --git a/src/pages/followup/edit/api.js b/src/pages/followup/edit/api.js
new file mode 100644
index 00000000..7da1e39c
--- /dev/null
+++ b/src/pages/followup/edit/api.js
@@ -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);
+};
diff --git a/src/pages/followup/form/ContactFormPage.css b/src/pages/followup/form/ContactFormPage.css
new file mode 100644
index 00000000..790e2fc1
--- /dev/null
+++ b/src/pages/followup/form/ContactFormPage.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/followup/form/FollowUpFormPage.js b/src/pages/followup/form/FollowUpFormPage.js
new file mode 100644
index 00000000..44e843b1
--- /dev/null
+++ b/src/pages/followup/form/FollowUpFormPage.js
@@ -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 (
+
+
{id ? "Edit Follow Up" : "Add Follow Up"}
+
+
+
+ );
+}
diff --git a/src/pages/followup/list/Delete.js b/src/pages/followup/list/Delete.js
new file mode 100644
index 00000000..394ae1a9
--- /dev/null
+++ b/src/pages/followup/list/Delete.js
@@ -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 (
+
+
+
+
+
+
Are you sure you want to delete this contact?
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/followup/list/Form.js b/src/pages/followup/list/Form.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/pages/followup/list/List.js b/src/pages/followup/list/List.js
new file mode 100644
index 00000000..7bc30e49
--- /dev/null
+++ b/src/pages/followup/list/List.js
@@ -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 (
+
+
+
Follow Ups
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+ | Reason |
+ Schedule Date |
+ Account |
+ Assigned To |
+ Work Order |
+ Status |
+ Actions |
+
+
+
+ {rows.map((x) => (
+
+ | {x.reason} |
+ {x.scheduleDate} |
+ {x.account} |
+ {x.assignedTo} |
+ {x.workOrderNumber} |
+ {x.status} |
+
+
+
+ |
+
+ ))}
+ {rows.length === 0 && (
+
+ | No follow ups found |
+
+ )}
+
+
+
+
+
+
+ Page {page} / {totalPages}
+
+
+
+
+ );
+}
diff --git a/src/pages/followup/list/Table.js b/src/pages/followup/list/Table.js
new file mode 100644
index 00000000..bce57b8d
--- /dev/null
+++ b/src/pages/followup/list/Table.js
@@ -0,0 +1,37 @@
+import React from "react";
+
+export default function Table({ loading, error, rows, onEdit, onDelete,
+ page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
+ return (
+
+ );
+}
diff --git a/src/pages/followup/list/api.js b/src/pages/followup/list/api.js
new file mode 100644
index 00000000..d634463b
--- /dev/null
+++ b/src/pages/followup/list/api.js
@@ -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}`);
diff --git a/src/pages/followup/list/styles.css b/src/pages/followup/list/styles.css
new file mode 100644
index 00000000..801b23a9
--- /dev/null
+++ b/src/pages/followup/list/styles.css
@@ -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;}
diff --git a/src/pages/followup/list/useItems.js b/src/pages/followup/list/useItems.js
new file mode 100644
index 00000000..ed8d1bf4
--- /dev/null
+++ b/src/pages/followup/list/useItems.js
@@ -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]);
+}
diff --git a/src/pages/followup/new/api.js b/src/pages/followup/new/api.js
new file mode 100644
index 00000000..016f17b3
--- /dev/null
+++ b/src/pages/followup/new/api.js
@@ -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}`);