From 40fbcca26eae3b75f7ba80701a955e73c030efc7 Mon Sep 17 00:00:00 2001 From: Mayank Jain Date: Mon, 6 Apr 2026 16:44:12 +0530 Subject: [PATCH 1/6] feat: add complaint management module --- src/App.jsx | 9 + src/Modules/ComplaintManagement.zip | Bin 0 -> 8449 bytes .../ComplaintManagement/ComplaintCreate.jsx | 6 + .../ComplaintManagement/ComplaintList.jsx | 5 + .../ComplaintManagement.module.css | 44 +++ src/Modules/ComplaintManagement/api.js | 16 + .../components/ComplaintDetailModal.jsx | 255 ++++++++++++++ .../components/ComplaintFormModal.jsx | 161 +++++++++ .../components/ComplaintManager.jsx | 324 ++++++++++++++++++ .../components/ComplaintTable.jsx | 111 ++++++ .../components/EscalationModal.jsx | 127 +++++++ .../components/ResolutionModal.jsx | 137 ++++++++ src/Modules/ComplaintManagement/index.jsx | 13 + src/Modules/ComplaintManagement/selectors.js | 11 + src/Modules/ComplaintManagement/services.js | 40 +++ src/components/sidebarContent.jsx | 9 +- src/routes/complaintRoutes/index.jsx | 12 + src/routes/globalRoutes/index.jsx | 2 + 18 files changed, 1279 insertions(+), 3 deletions(-) create mode 100644 src/Modules/ComplaintManagement.zip create mode 100644 src/Modules/ComplaintManagement/ComplaintCreate.jsx create mode 100644 src/Modules/ComplaintManagement/ComplaintList.jsx create mode 100644 src/Modules/ComplaintManagement/ComplaintManagement.module.css create mode 100644 src/Modules/ComplaintManagement/api.js create mode 100644 src/Modules/ComplaintManagement/components/ComplaintDetailModal.jsx create mode 100644 src/Modules/ComplaintManagement/components/ComplaintFormModal.jsx create mode 100644 src/Modules/ComplaintManagement/components/ComplaintManager.jsx create mode 100644 src/Modules/ComplaintManagement/components/ComplaintTable.jsx create mode 100644 src/Modules/ComplaintManagement/components/EscalationModal.jsx create mode 100644 src/Modules/ComplaintManagement/components/ResolutionModal.jsx create mode 100644 src/Modules/ComplaintManagement/index.jsx create mode 100644 src/Modules/ComplaintManagement/selectors.js create mode 100644 src/Modules/ComplaintManagement/services.js create mode 100644 src/routes/complaintRoutes/index.jsx diff --git a/src/App.jsx b/src/App.jsx index 99d55a675..48fdf09c7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,6 +15,7 @@ import InactivityHandler from "./helper/inactivityhandler"; import Examination from "./Modules/Examination/examination"; import Database from "./Modules/Database/database"; import ProgrammeCurriculumRoutes from "./Modules/Program_curriculum/programmCurriculum"; +import ComplaintManagementModule from "./Modules/ComplaintManagement"; import NotFoundPage from "./components/NotFoundPage"; const theme = createTheme({ @@ -82,6 +83,14 @@ export default function App() { } /> } /> } /> + + + + } + /> } /> diff --git a/src/Modules/ComplaintManagement.zip b/src/Modules/ComplaintManagement.zip new file mode 100644 index 0000000000000000000000000000000000000000..6061bec0e9b929057f3fb17fa931a8c6dbab4518 GIT binary patch literal 8449 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 literal 0 HcmV?d00001 diff --git a/src/Modules/ComplaintManagement/ComplaintCreate.jsx b/src/Modules/ComplaintManagement/ComplaintCreate.jsx new file mode 100644 index 000000000..f338f3c68 --- /dev/null +++ b/src/Modules/ComplaintManagement/ComplaintCreate.jsx @@ -0,0 +1,6 @@ +import ComplaintManager from "./components/ComplaintManager"; + +// Thin view that composes micro-components through ComplaintManager. +export default function ComplaintCreate() { + return ; +} diff --git a/src/Modules/ComplaintManagement/ComplaintList.jsx b/src/Modules/ComplaintManagement/ComplaintList.jsx new file mode 100644 index 000000000..0f0e923a9 --- /dev/null +++ b/src/Modules/ComplaintManagement/ComplaintList.jsx @@ -0,0 +1,5 @@ +import ComplaintManager from "./components/ComplaintManager"; + +export default function ComplaintList() { + return ; +} diff --git a/src/Modules/ComplaintManagement/ComplaintManagement.module.css b/src/Modules/ComplaintManagement/ComplaintManagement.module.css new file mode 100644 index 000000000..9b366db6f --- /dev/null +++ b/src/Modules/ComplaintManagement/ComplaintManagement.module.css @@ -0,0 +1,44 @@ +.page { + background: linear-gradient(180deg, #f8fbff 0%, #ffffff 45%, #f7fbff 100%); + border: 1px solid #dcecff; + border-radius: 12px; + padding: 1rem; +} + +.headerBlock { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; +} + +.title { + color: #1c2e45; + letter-spacing: 0.2px; +} + +.subtitle { + color: #6d7f93; +} + +.actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.tablePanel { + background: #ffffff; + border: 1px solid #d7e9ff; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(46, 105, 167, 0.06); + padding: 0.75rem; +} + +@media (max-width: 768px) { + .headerBlock { + flex-direction: column; + align-items: stretch; + } +} diff --git a/src/Modules/ComplaintManagement/api.js b/src/Modules/ComplaintManagement/api.js new file mode 100644 index 000000000..f2ef6d4f1 --- /dev/null +++ b/src/Modules/ComplaintManagement/api.js @@ -0,0 +1,16 @@ +import axios from "axios"; +import { COMPLAINT_API_BASE } from "../../routes/complaintRoutes"; + +const complaintApi = axios.create({ + baseURL: COMPLAINT_API_BASE, +}); + +complaintApi.interceptors.request.use((config) => { + const token = localStorage.getItem("authToken"); + if (token) { + config.headers.Authorization = `Token ${token}`; + } + return config; +}); + +export default complaintApi; diff --git a/src/Modules/ComplaintManagement/components/ComplaintDetailModal.jsx b/src/Modules/ComplaintManagement/components/ComplaintDetailModal.jsx new file mode 100644 index 000000000..d984356a6 --- /dev/null +++ b/src/Modules/ComplaintManagement/components/ComplaintDetailModal.jsx @@ -0,0 +1,255 @@ +import PropTypes from "prop-types"; +import { + Modal, + Stack, + Text, + Button, + Group, + Badge, + Card, + Divider, + Grid, +} from "@mantine/core"; + +const STATUS_LABELS = new Map([ + [0, "Pending"], + [1, "In Progress"], + [2, "Completed"], +]); + +const STATUS_COLORS = new Map([ + [0, "gray"], + [1, "yellow"], + [2, "green"], +]); + +const complaintDetailsShape = PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + complaint_type: PropTypes.string, + status: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + is_escalated: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + escalation_reason: PropTypes.string, + escalated_date: PropTypes.string, + location: PropTypes.string, + specific_location: PropTypes.string, + details: PropTypes.string, + remarks: PropTypes.string, + feedback: PropTypes.string, +}); + +export default function ComplaintDetailModal({ + opened, + onClose, + detail, + canResolve = false, + onResolve = () => {}, + onEscalate = () => {}, +}) { + const handleResolveClick = () => { + onResolve(detail?.complaint_details); + }; + + const handleEscalateClick = () => { + onEscalate(detail?.complaint_details); + }; + + const status = Number(detail?.complaint_details?.status); + const statusLabel = STATUS_LABELS.get(status) || "Unknown"; + const statusColor = STATUS_COLORS.get(status) || "gray"; + + return ( + + {!detail ? ( + No detail loaded. + ) : ( + + {/* Header Card with Status */} + + +
+ + Complaint #{detail.complaint_details?.id} + + + {detail.complaint_details?.complaint_type} + +
+ + {statusLabel} + +
+
+ + {/* Escalation Status Card */} + {detail.complaint_details?.is_escalated === 1 && ( + + + + Escalated to Supervisor + + + {detail.complaint_details?.escalation_reason && ( + + Reason:{" "} + {detail.complaint_details?.escalation_reason} + + )} + {detail.complaint_details?.escalated_date && ( + + Escalated on:{" "} + {new Date( + detail.complaint_details?.escalated_date, + ).toLocaleString()} + + )} + + )} + + {/* Main Details Grid */} + + + + +
+ + Location + + {detail.complaint_details?.location} +
+ {detail.complaint_details?.specific_location && ( +
+ + Specific Location + + + {detail.complaint_details?.specific_location} + +
+ )} +
+
+
+ + + + +
+ + Complainer + + {detail.complainer?.username} +
+
+ + Assigned Worker + + + {detail.worker_details?.name || "Not assigned"} + +
+
+
+
+
+ + {/* Description */} + + +
+ + Issue Description + + {detail.complaint_details?.details} +
+ + + +
+ + Current Remarks + + + {detail.complaint_details?.remarks || "No remarks yet"} + +
+ + {detail.complaint_details?.feedback && ( + <> + +
+ + Feedback + + {detail.complaint_details?.feedback} +
+ + )} +
+
+ + {/* Action Buttons */} + {canResolve && ( + + + + + + )} +
+ )} +
+ ); +} + +ComplaintDetailModal.propTypes = { + opened: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + detail: PropTypes.shape({ + complaint_details: complaintDetailsShape, + complainer: PropTypes.shape({ + username: PropTypes.string, + }), + worker_details: PropTypes.shape({ + name: PropTypes.string, + }), + }), + canResolve: PropTypes.bool, + onResolve: PropTypes.func, + onEscalate: PropTypes.func, +}; + +ComplaintDetailModal.defaultProps = { + detail: null, + canResolve: false, + onResolve: () => {}, + onEscalate: () => {}, +}; diff --git a/src/Modules/ComplaintManagement/components/ComplaintFormModal.jsx b/src/Modules/ComplaintManagement/components/ComplaintFormModal.jsx new file mode 100644 index 000000000..bc5802a31 --- /dev/null +++ b/src/Modules/ComplaintManagement/components/ComplaintFormModal.jsx @@ -0,0 +1,161 @@ +import PropTypes from "prop-types"; +import { useEffect, useState } from "react"; +import { + Button, + Group, + Modal, + NumberInput, + Select, + Stack, + TextInput, + Textarea, +} from "@mantine/core"; + +const complaintTypeOptions = [ + "Electricity", + "carpenter", + "plumber", + "garbage", + "dustbin", + "internet", + "other", +].map((value) => ({ value, label: value })); + +const areaOptions = [ + "hall-1", + "hall-3", + "hall-4", + "library", + "computer center", + "core_lab", + "LHTC", + "NR2", + "NR3", + "Admin building", + "Rewa_Residency", + "Maa Saraswati Hostel", + "Nagarjun Hostel", + "Panini Hostel", +].map((value) => ({ value, label: value })); + +const initialForm = { + complaint_type: "internet", + location: "hall-3", + specific_location: "", + details: "", + status: 0, + remarks: "Pending", + reason: "None", + comment: "None", +}; + +export default function ComplaintFormModal({ + opened, + mode, + initialData, + canChangeStatus, + onClose, + onSubmit, + loading, +}) { + const [form, setForm] = useState(initialForm); + + useEffect(() => { + if (initialData) { + setForm({ + ...initialForm, + ...initialData, + }); + return; + } + setForm(initialForm); + }, [initialData, opened]); + + const handleChange = (field, value) => { + setForm((prev) => ({ ...prev, [field]: value })); + }; + + return ( + + + handleChange("location", value)} + required + /> + + handleChange("specific_location", event.currentTarget.value) + } + /> +