mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 12:43:13 +00:00
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:
parent
a349a58822
commit
bbcd1eb4e1
6 changed files with 485 additions and 327 deletions
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue