Wire Follow Ups list and form to backend

Replaces the mock api with real endpoints, fixes list column mappings
to match the backend payload, and realigns the form to the FollowUps
model (Reason, ScheduleDate, ScheduleStartTime, EmployeeId, AccountId,
LocationId, WorkOrderId) with real lookup dropdowns for Employee,
Account, and Location. Also redesigns the list header to match the
Work Orders toolbar + chip filter look.
This commit is contained in:
Adam Moussa 2026-04-20 13:49:58 -04:00
parent a349a58822
commit bbcd1eb4e1
6 changed files with 485 additions and 327 deletions

View file

@ -1,64 +1,20 @@
// Mock follow-ups data
const mockFollowUps = [
{
id: 1,
workOrder: "WO-001",
reason: "Quarterly maintenance check",
scheduledDate: "2024-01-22",
status: "Pending",
createdBy: "John Anderson",
createdDate: "2024-01-15",
notes: "Check all HVAC filters and oil levels",
},
{
id: 2,
workOrder: "WO-002",
reason: "Verify electrical repairs",
scheduledDate: "2024-01-25",
status: "Pending",
createdBy: "Sarah Martinez",
createdDate: "2024-01-16",
notes: "Test all outlets to ensure proper operation",
},
{
id: 3,
workOrder: "WO-003",
reason: "Post-repair inspection",
scheduledDate: "2024-01-20",
status: "Completed",
createdBy: "Mike Johnson",
createdDate: "2024-01-10",
notes: "Plumbing inspection completed successfully",
},
];
import { get, post } from "../../apiUtil";
import { API_URL } from "../../constants";
export const getFollowUps = async () => {
await new Promise(resolve => setTimeout(resolve, 300));
return [...mockFollowUps];
};
export const getFollowUpById = id =>
get(`${API_URL}/FollowUp/GetById?id=${id}`);
export const getFollowUpById = async (id) => {
await new Promise(resolve => setTimeout(resolve, 200));
return mockFollowUps.find(f => f.id === parseInt(id));
};
export const addFollowUp = payload =>
post(`${API_URL}/FollowUp/Create`, payload);
export const addFollowUp = async (followUp) => {
await new Promise(resolve => setTimeout(resolve, 500));
const newFollowUp = { id: mockFollowUps.length + 1, ...followUp };
mockFollowUps.push(newFollowUp);
return newFollowUp;
};
export const editFollowUp = payload =>
post(`${API_URL}/FollowUp/Update`, payload);
export const editFollowUp = async (followUp) => {
await new Promise(resolve => setTimeout(resolve, 500));
const idx = mockFollowUps.findIndex(f => f.id === followUp.id);
if (idx >= 0) mockFollowUps[idx] = followUp;
return followUp;
};
export const deleteFollowUp = id =>
post(`${API_URL}/FollowUp/Delete?id=${id}`);
export const deleteFollowUp = async (id) => {
await new Promise(resolve => setTimeout(resolve, 500));
const idx = mockFollowUps.findIndex(f => f.id === id);
if (idx >= 0) mockFollowUps.splice(idx, 1);
return true;
};
export const completeFollowUp = id =>
post(`${API_URL}/FollowUp/Complete?id=${id}`);
export const cancelFollowUp = id =>
post(`${API_URL}/FollowUp/Cancel?id=${id}`);

View file

@ -2,66 +2,127 @@ import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router";
import { toast } from "react-toastify";
import { getFollowUpById, addFollowUp, editFollowUp } from "../api";
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
import "./FollowUpFormPage.css";
function normalizeId(v) {
if (v === "" || v === null || v === undefined) return null;
const n = parseInt(v, 10);
return Number.isNaN(n) ? null : n;
}
function toTimeString(t) {
if (!t) return "09:00";
const m = String(t).match(/^(\d{2}:\d{2})/);
return m ? m[1] : "09:00";
}
export default function FollowUpFormPage() {
const { id } = useParams();
const navigate = useNavigate();
const isEdit = !!id;
const [formData, setFormData] = useState({
workOrder: "",
reason: "",
scheduledDate: "",
status: "Pending",
createdBy: "",
createdDate: new Date().toISOString().split("T")[0],
notes: "",
scheduleDate: "",
scheduleStartTime: "09:00",
employeeId: "",
accountId: "",
locationId: "",
workOrderId: "",
});
const [loading, setLoading] = useState(false);
const [employees, setEmployees] = useState([]);
const [accounts, setAccounts] = useState([]);
const [locations, setLocations] = useState([]);
useEffect(() => {
if (isEdit && id) {
const loadFollowUp = async () => {
try {
const followUp = await getFollowUpById(id);
if (followUp) {
setFormData(prev => {
const merged = { ...prev };
for (const key of Object.keys(merged)) {
if (followUp[key] != null) merged[key] = followUp[key];
}
return merged;
});
}
} catch (err) {
console.error("Failed to load follow up", err);
toast.error("Failed to load follow up");
}
};
loadFollowUp();
}
get(`${API_URL}/Employee/GetEmployeeList?pageSize=500`)
.then((res) => {
const rows = res?.data || res?.Data || [];
setEmployees(
rows.map((e) => ({
id: e.id ?? e.Id,
name: `${e.firstName ?? e.FirstName ?? ""} ${e.lastName ?? e.LastName ?? ""}`.trim(),
}))
);
})
.catch(() => {});
get(`${API_URL}/Account/GetAccountList?pageSize=500`)
.then((res) => {
const rows = res?.data || res?.Data || [];
setAccounts(
rows.map((a) => ({
id: a.id ?? a.Id,
name: a.name ?? a.Name ?? "",
}))
);
})
.catch(() => {});
get(`${API_URL}/Contact/GetLocations`)
.then((data) => {
const rows = Array.isArray(data) ? data : [];
setLocations(
rows.map((l) => ({
id: l.id ?? l.Id,
name: l.name ?? l.Name ?? "",
}))
);
})
.catch(() => {});
}, []);
useEffect(() => {
if (!isEdit || !id) return;
getFollowUpById(id)
.then((data) => {
if (!data) return;
setFormData({
reason: data.reason ?? data.Reason ?? "",
scheduleDate: (data.scheduleDate ?? data.ScheduleDate ?? "").slice(0, 10),
scheduleStartTime: toTimeString(data.scheduleStartTime ?? data.ScheduleStartTime),
employeeId: data.employeeId ?? data.EmployeeId ?? "",
accountId: data.accountId ?? data.AccountId ?? "",
locationId: data.locationId ?? data.LocationId ?? "",
workOrderId: data.workOrderId ?? data.WorkOrderId ?? "",
});
})
.catch((err) => {
console.error("Failed to load follow up", err);
toast.error("Failed to load follow up");
});
}, [id, isEdit]);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSave = async (e) => {
e.preventDefault();
setLoading(true);
const payload = {
Id: isEdit ? parseInt(id, 10) : 0,
Reason: formData.reason,
ScheduleDate: formData.scheduleDate,
ScheduleStartTime: `${toTimeString(formData.scheduleStartTime)}:00`,
EmployeeId: normalizeId(formData.employeeId),
AccountId: normalizeId(formData.accountId),
LocationId: normalizeId(formData.locationId),
WorkOrderId: normalizeId(formData.workOrderId),
};
try {
if (isEdit) {
await editFollowUp(formData);
await editFollowUp(payload);
toast.success("Follow-up updated successfully!");
} else {
await addFollowUp(formData);
await addFollowUp(payload);
toast.success("Follow-up created successfully!");
}
navigate("/followups");
@ -83,7 +144,13 @@ export default function FollowUpFormPage() {
<div className="panel-body">
<ol className="breadcrumb breadcrumb-alt">
<li>
<a href="#" onClick={(e) => { e.preventDefault(); navigate("/followups"); }}>
<a
href="#"
onClick={(e) => {
e.preventDefault();
navigate("/followups");
}}
>
Follow-ups
</a>
</li>
@ -92,20 +159,20 @@ export default function FollowUpFormPage() {
<div className="container">
<form onSubmit={handleSave}>
{/* Page Header */}
<div className="form-group">
<div className="header">
<h3 className="heading thin">
<span className="fa-stack" style={{ fontSize: "0.9em" }}>
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}></i>
<i
className={`fa fa-${isEdit ? "edit" : "plus"} fa-stack-1x fa-inverse`}
></i>
</span>
<span>{isEdit ? "Edit a Follow-up" : "Create a Follow-up"}</span>
</h3>
</div>
</div>
{/* Follow-up Details Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
@ -115,25 +182,9 @@ export default function FollowUpFormPage() {
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="workOrder">Work Order</label>
<select
className="form-control"
id="workOrder"
name="workOrder"
value={formData.workOrder}
onChange={handleChange}
required
>
<option value="">Select Work Order</option>
<option value="WO-001">WO-001 - HVAC System Maintenance</option>
<option value="WO-002">WO-002 - Electrical Repair</option>
<option value="WO-003">WO-003 - Plumbing Issue</option>
</select>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="reason">Reason</label>
<label className="required-field" htmlFor="reason">
Reason
</label>
<input
className="form-control"
type="text"
@ -141,84 +192,63 @@ export default function FollowUpFormPage() {
name="reason"
value={formData.reason}
onChange={handleChange}
placeholder="Follow-up reason"
required
/>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="scheduledDate">Scheduled Date</label>
<input
className="form-control"
type="date"
id="scheduledDate"
name="scheduledDate"
value={formData.scheduledDate}
onChange={handleChange}
placeholder="What is this follow-up for?"
required
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="status">Status</label>
<label className="required-field" htmlFor="employeeId">
Assigned To
</label>
<select
className="form-control"
id="status"
name="status"
value={formData.status}
onChange={handleChange}
required
>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
<option value="Cancelled">Cancelled</option>
</select>
</div>
</div>
</div>
</div>
{/* Creator Information Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Creator Information</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="createdBy">Created By</label>
<select
className="form-control"
id="createdBy"
name="createdBy"
value={formData.createdBy}
id="employeeId"
name="employeeId"
value={formData.employeeId}
onChange={handleChange}
required
>
<option value="">Select Employee</option>
<option value="John Anderson">John Anderson</option>
<option value="Sarah Martinez">Sarah Martinez</option>
<option value="Mike Johnson">Mike Johnson</option>
{employees.map((e) => (
<option key={e.id} value={e.id}>
{e.name || `Employee #${e.id}`}
</option>
))}
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="scheduleDate">
Scheduled Date
</label>
<input
className="form-control"
type="date"
id="scheduleDate"
name="scheduleDate"
value={formData.scheduleDate}
onChange={handleChange}
required
/>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label className="required-field" htmlFor="createdDate">Created Date</label>
<label className="required-field" htmlFor="scheduleStartTime">
Start Time
</label>
<input
className="form-control"
type="date"
id="createdDate"
name="createdDate"
value={formData.createdDate}
type="time"
id="scheduleStartTime"
name="scheduleStartTime"
value={formData.scheduleStartTime}
onChange={handleChange}
required
/>
@ -227,32 +257,70 @@ export default function FollowUpFormPage() {
</div>
</div>
{/* Notes Section */}
<div className="form-group">
<div className="header">
<h5 className="heading text-uppercase">
<strong>Notes</strong>
<strong>Related (optional)</strong>
</h5>
</div>
<div className="row">
<div className="col-sm-12">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="notes">Notes</label>
<textarea
<label htmlFor="accountId">Account</label>
<select
className="form-control"
id="notes"
name="notes"
value={formData.notes}
id="accountId"
name="accountId"
value={formData.accountId}
onChange={handleChange}
rows="5"
placeholder="Additional notes or details"
>
<option value="">None</option>
{accounts.map((a) => (
<option key={a.id} value={a.id}>
{a.name || `Account #${a.id}`}
</option>
))}
</select>
</div>
</div>
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="locationId">Location</label>
<select
className="form-control"
id="locationId"
name="locationId"
value={formData.locationId}
onChange={handleChange}
>
<option value="">None</option>
{locations.map((l) => (
<option key={l.id} value={l.id}>
{l.name || `Location #${l.id}`}
</option>
))}
</select>
</div>
</div>
</div>
<div className="row">
<div className="col-sm-6">
<div className="form-group">
<label htmlFor="workOrderId">Work Order ID</label>
<input
className="form-control"
type="number"
id="workOrderId"
name="workOrderId"
value={formData.workOrderId}
onChange={handleChange}
placeholder="Numeric WO ID (optional)"
/>
</div>
</div>
</div>
</div>
{/* Form Actions */}
<div className="form-actions">
<button
type="button"
@ -262,11 +330,7 @@ export default function FollowUpFormPage() {
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
disabled={loading}
>
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? "Saving..." : isEdit ? "Update" : "Create"}
</button>
</div>

View file

@ -1,18 +1,32 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "react-toastify";
import usePaginatedList from "../../../hooks/usePaginatedList";
import { fetchFollowUpsPaged } from "./api";
import Table from "./Table";
import DeleteModal from "../../../components/DeleteModal";
import ActionBar from "../../../components/ActionBar";
import { deleteFollowUp } from "../api";
import "./styles.css";
const STATUSES = ["Open", "Completed", "Cancelled"];
export default function FollowUpList() {
const navigate = useNavigate();
const { rows, search, setSearch, page, setPage, pageSize, setPageSize, totalCount, loading, err, reload } = usePaginatedList(fetchFollowUpsPaged, { status: "Open" });
const [viewMode, setViewMode] = useState("list");
const [statusFilter, setStatusFilter] = useState("Open");
const extraParams = useMemo(() => ({ status: statusFilter }), [statusFilter]);
const {
rows,
search,
setSearch,
page,
setPage,
pageSize,
setPageSize,
totalCount,
loading,
err,
reload,
} = usePaginatedList(fetchFollowUpsPaged, extraParams);
const [deleteModal, setDeleteModal] = useState({ isOpen: false, item: null, loading: false });
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
@ -21,125 +35,73 @@ export default function FollowUpList() {
};
const handleDeleteConfirm = async () => {
setDeleteModal(prev => ({ ...prev, loading: true }));
setDeleteModal((prev) => ({ ...prev, loading: true }));
try {
await deleteFollowUp(deleteModal.item.id);
toast.success(`Follow-up deleted successfully!`);
toast.success("Follow-up deleted successfully!");
setDeleteModal({ isOpen: false, item: null, loading: false });
reload();
} catch (err) {
console.error(err);
toast.error("Failed to delete follow-up");
setDeleteModal(prev => ({ ...prev, loading: false }));
setDeleteModal((prev) => ({ ...prev, loading: false }));
}
};
return (
<>
<ActionBar
moduleName="Follow Ups"
moduleIcon="fa-hourglass"
onNew={() => navigate("/followups/new")}
onBulkEdit={() => console.log("Bulk edit")}
selectedCount={0}
<section className="listPage">
<div className="fuListToolbar">
<div className="fuListToolbarLeft">
<h2 className="fuListTitle">Follow Ups</h2>
<span className="fuListCount">{totalCount} total</span>
</div>
<div className="fuListToolbarRight">
<input
className="fuSearchInput"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
placeholder="Search by reason, account, or assignee..."
/>
<button className="fuNewBtn" onClick={() => navigate("/followups/new")}>
+ New Follow Up
</button>
</div>
</div>
<div className="fuFilterBar">
{STATUSES.map((s) => (
<button
key={s}
className={`fuFilterChip ${statusFilter === s ? "active" : ""}`}
onClick={() => { setStatusFilter(s); setPage(1); }}
>
{s}
</button>
))}
</div>
<Table
loading={loading}
error={err}
rows={rows}
onEdit={(item) => navigate(`/followups/edit/${item.id}`)}
onDelete={handleDeleteClick}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
totalPages={totalPages}
/>
<section className="listPage">
<div className="breadcrumb">
<span className="breadcrumbItem active">Follow Ups</span>
</div>
<div className="searchSection">
<div className="searchHeader">
<h3 className="searchHeading">
<span className="fa-stack iconStack">
<i className="fa fa-square fa-stack-2x text-primary"></i>
<i className="fa fa-hourglass fa-stack-1x fa-inverse"></i>
</span>
<span>Search for Follow Ups</span>
</h3>
<span className="searchSubheading">Use the settings menu to adjust how to filter the list, and which columns to display.</span>
</div>
<div className="searchControls">
<div className="searchLeft">
<div className="searchInputGroup">
<select className="searchDropdown" defaultValue="WorkOrder">
<option>Work Order</option>
<option>Reason</option>
<option>Status</option>
</select>
<input
className="searchInput"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Enter search criteria here..."
/>
<button className="searchButton">Search!</button>
</div>
<div className="viewToggle">
<button
className={`viewBtn ${viewMode === "list" ? "active" : ""}`}
title="List view"
onClick={() => setViewMode("list")}
>
<i className="fa fa-navicon"></i>
</button>
<button
className={`viewBtn ${viewMode === "card" ? "active" : ""}`}
title="Card view"
onClick={() => setViewMode("card")}
>
<i className="fa fa-th-large"></i>
</button>
</div>
</div>
<div className="searchRight">
<div className="paginationTop">
<button
className="pageBtn"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
«
</button>
<span className="pageNumber">{page}</span>
<button
className="pageBtn"
disabled={page >= totalPages}
onClick={() => setPage(page + 1)}
>
»
</button>
</div>
</div>
</div>
</div>
<Table
loading={loading}
error={err}
rows={rows}
onEdit={(item) => navigate(`/followups/edit/${item.id}`)}
onDelete={handleDeleteClick}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
totalPages={totalPages}
/>
<DeleteModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal({ isOpen: false, item: null, loading: false })}
onConfirm={handleDeleteConfirm}
title="Delete Follow-up"
message={`Are you sure you want to delete this follow-up for "${deleteModal.item?.workOrder}"? This action cannot be undone.`}
loading={deleteModal.loading}
/>
</section>
</>
<DeleteModal
isOpen={deleteModal.isOpen}
onClose={() => setDeleteModal({ isOpen: false, item: null, loading: false })}
onConfirm={handleDeleteConfirm}
title="Delete Follow-up"
message={`Are you sure you want to delete this follow-up: "${deleteModal.item?.reason || ""}"? This action cannot be undone.`}
loading={deleteModal.loading}
/>
</section>
);
}

View file

@ -1,16 +1,77 @@
import React from "react";
import SharedTable from "../../../components/SharedTable";
export default function Table({ loading, error, rows, onEdit, onDelete, page, setPage, pageSize, setPageSize, totalCount, totalPages }) {
function formatScheduled(date, time) {
if (!date) return "";
const d = new Date(date);
if (isNaN(d)) return "";
const dateStr = d.toISOString().slice(0, 10);
if (!time) return dateStr;
const m = String(time).match(/^(\d{2}:\d{2})/);
return m ? `${dateStr} ${m[1]}` : dateStr;
}
export default function Table({
loading,
error,
rows,
onEdit,
onDelete,
page,
setPage,
pageSize,
setPageSize,
totalCount,
totalPages,
}) {
const columns = [
{ key: "workOrder", label: "Work Order", render: (val, row) => <strong><a href="#" onClick={(e) => { e.preventDefault(); onEdit && onEdit(row); }}>{val}</a></strong> },
{ key: "reason", label: "Reason" },
{ key: "scheduledDate", label: "Scheduled Date" },
{
key: "reason",
label: "Reason",
render: (val, row) => (
<strong>
<a
href="#"
onClick={(e) => {
e.preventDefault();
onEdit && onEdit(row);
}}
>
{val || "(no reason)"}
</a>
</strong>
),
},
{ key: "assignedTo", label: "Assigned To" },
{
key: "scheduleDate",
label: "Scheduled",
render: (_, row) => formatScheduled(row.scheduleDate, row.scheduleStartTime),
},
{ key: "accountName", label: "Account" },
{ key: "locationName", label: "Location" },
{
key: "workOrderNumber",
label: "Work Order",
render: (val, row) => val || row.internalWONumber || "",
},
{ key: "createdByName", label: "Created By" },
{ key: "status", label: "Status" },
{ key: "createdBy", label: "Created By" },
];
return (
<SharedTable rows={rows} columns={columns} onEdit={onEdit} onDelete={onDelete} page={page} setPage={setPage} pageSize={pageSize} setPageSize={setPageSize} totalCount={totalCount} loading={loading} error={error} />
<SharedTable
rows={rows}
columns={columns}
onEdit={onEdit}
onDelete={onDelete}
page={page}
setPage={setPage}
pageSize={pageSize}
setPageSize={setPageSize}
totalCount={totalCount}
loading={loading}
error={error}
/>
);
}

View file

@ -1,5 +1,4 @@
// src/pages/followups/followUpApi.js
import { get, post } from "../../../apiUtil";
import { get } from "../../../apiUtil";
import { API_URL } from "../../../constants";
export const fetchFollowUpsPaged = async ({
@ -11,20 +10,26 @@ export const fetchFollowUpsPaged = async ({
const res = await get(
`${API_URL}/FollowUp/GetFollowUpList?search=${encodeURIComponent(
search
)}&page=${page}&pageSize=${pageSize}&status=${status}`
)}&page=${page}&pageSize=${pageSize}&status=${encodeURIComponent(status)}`
);
const rows = (res?.data || res?.Data || []).map(x => ({
id: x.id ?? x.Id,
reason: x.reason ?? x.Reason ?? "",
scheduleDate: x.scheduleDate ?? x.ScheduleDate ?? "",
scheduleTime: x.scheduleStartTime ?? x.ScheduleStartTime ?? "",
account: x.accountName ?? x.AccountName ?? "",
assignedTo: x.employeeName ?? x.EmployeeName ?? "",
locationNumber: x.locationNumber ?? x.LocationNumber ?? "",
scheduleStartTime: x.scheduleStartTime ?? x.ScheduleStartTime ?? "",
employeeId: x.employeeId ?? x.EmployeeId ?? null,
workOrderId: x.workOrderId ?? x.WorkOrderId ?? null,
accountId: x.accountId ?? x.AccountId ?? null,
locationId: x.locationId ?? x.LocationId ?? null,
accountName: x.accountName ?? x.AccountName ?? "",
assignedTo: x.assignedTo ?? x.AssignedTo ?? "",
locationName: x.locationName ?? x.LocationName ?? "",
workOrderCode: x.workOrderCode ?? x.WorkOrderCode ?? "",
status: x.status ?? x.Status ?? ""
workOrderNumber: x.workOrderNumber ?? x.WorkOrderNumber ?? "",
internalWONumber: x.internalWONumber ?? x.InternalWONumber ?? "",
status: x.status ?? x.Status ?? "",
createdDate: x.createdDate ?? x.CreatedDate ?? "",
createdByName: x.createdByName ?? x.CreatedByName ?? ""
}));
return {
@ -35,15 +40,3 @@ export const fetchFollowUpsPaged = async ({
totalPages: res?.totalPages ?? res?.TotalPages ?? 1
};
};
export const createFollowUp = data =>
post(`${API_URL}/FollowUp/Create`, data);
export const updateFollowUp = data =>
post(`${API_URL}/FollowUp/Update`, data);
export const completeFollowUp = id =>
post(`${API_URL}/FollowUp/Complete?id=${id}`);
export const cancelFollowUp = id =>
post(`${API_URL}/FollowUp/Cancel?id=${id}`);

View file

@ -10,6 +10,100 @@
min-height: auto !important;
}
/* Toolbar header (mirrors work orders list look) */
.fuListToolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e5e5e5;
}
.fuListToolbarLeft {
display: flex;
align-items: baseline;
gap: 12px;
}
.fuListTitle {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
.fuListCount {
font-size: 13px;
color: #999;
}
.fuListToolbarRight {
display: flex;
align-items: center;
gap: 12px;
}
.fuSearchInput {
height: 38px;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
outline: none;
width: 260px;
box-sizing: border-box;
}
.fuSearchInput:focus {
border-color: #66afe9;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
}
.fuNewBtn {
padding: 8px 16px;
background: #007bff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.fuNewBtn:hover {
background: #0069d9;
}
.fuFilterBar {
display: flex;
gap: 6px;
padding: 10px 20px;
border-bottom: 1px solid #e5e5e5;
background: #fafafa;
}
.fuFilterChip {
padding: 5px 14px;
border: 1px solid #ddd;
border-radius: 16px;
background: #fff;
font-size: 12px;
color: #666;
cursor: pointer;
}
.fuFilterChip:hover {
border-color: #007bff;
color: #007bff;
}
.fuFilterChip.active {
background: #007bff;
color: #fff;
border-color: #007bff;
}
/* Breadcrumb */
.breadcrumb {
background: white;
@ -71,6 +165,34 @@
margin-left: 45px;
}
.followUpStatusTabs {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid #e5e5e5;
}
.followUpStatusTab {
padding: 8px 18px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: #666;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: color .15s ease, border-color .15s ease;
}
.followUpStatusTab:hover {
color: #0088cc;
}
.followUpStatusTab.active {
color: #0088cc;
border-bottom-color: #0088cc;
}
.searchControls {
display: flex;
justify-content: space-between;