diff --git a/.husky/pre-commit b/.husky/pre-commit index 589d19328..41071e5be 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,10 +1,5 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -# Run Prettier formatting only on staged files (using lint-staged) +# Run checks only on staged files. npx lint-staged || exit 1 - -# Run lint, if it fails, the commit will be aborted -npm run lint || exit 1 - -git add . diff --git a/package.json b/package.json index 236751ab4..ba201fc2e 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,7 @@ "lint-staged": { "*.{js,jsx}": [ "prettier --write", - "eslint --fix", - "git add" + "eslint --fix" ] } } 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 000000000..6061bec0e Binary files /dev/null and b/src/Modules/ComplaintManagement.zip differ 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..81bbbfb04 --- /dev/null +++ b/src/Modules/ComplaintManagement/ComplaintManagement.module.css @@ -0,0 +1,254 @@ +/* ────────────────────────────────────────────────────────── + Complaint Management – Module Styles + Aligned with the Fusion design system (#15abff accent, #F5F7F8 canvas) + ────────────────────────────────────────────────────────── */ + +.page { + background: #ffffff; + border: 1px solid #e8eef3; + border-radius: 8px; + padding: 1.25rem 1.5rem; +} + +/* ── Global Mantine overrides (scoped to .page) ───────── */ + +.page :global(.mantine-Button-root) { + border-radius: 6px; + font-weight: 500; + font-size: 0.875rem; + transition: all 0.15s ease; +} + +.page :global(.mantine-Button-root):hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(21, 171, 255, 0.18); +} + +.page :global(.mantine-Input-input), +.page :global(.mantine-Select-input), +.page :global(.mantine-Textarea-input) { + border-radius: 6px; + border: 1px solid #d5dce4; + background: #ffffff; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.page :global(.mantine-Input-input):focus, +.page :global(.mantine-Select-input):focus, +.page :global(.mantine-Textarea-input):focus { + border-color: #15abff; + box-shadow: 0 0 0 3px rgba(21, 171, 255, 0.12); +} + +/* ── Page header ──────────────────────────────────────── */ + +.headerBlock { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.title { + color: #1a1a2e; + font-weight: 600; + font-size: 1.35rem; + letter-spacing: -0.01em; +} + +.subtitle { + color: #6b7a8d; + font-weight: 400; + font-size: 0.875rem; + margin-top: 2px; +} + +.actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.statusNote { + color: #94a3b8; + font-size: 0.8rem; + margin-top: 0.35rem; +} + +/* ── Card & panel surfaces ────────────────────────────── */ + +.tablePanel, +.oversightHeader, +.reportHeader, +.notificationPanel, +.moduleCard { + background: #ffffff; + border: 1px solid #e8eef3; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + padding: 1rem; +} + +/* ── Summary stat cards ───────────────────────────────── */ + +.summaryCard { + background: #ffffff; + border: 1px solid #e8eef3; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + padding: 1rem 1.25rem; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.summaryCard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #15abff, #6dc8ff); + opacity: 0; + transition: opacity 0.2s ease; +} + +.summaryCard:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(21, 171, 255, 0.1); + border-color: #c4e2f5; +} + +.summaryCard:hover::before { + opacity: 1; +} + +/* ── Toolbars ─────────────────────────────────────────── */ + +.notificationToolbar, +.reportToolbar { + background: #F5F7F8; + border: 1px solid #e8eef3; + border-radius: 6px; + padding: 0.875rem; +} + +/* ── Notification cards ───────────────────────────────── */ + +.notificationEmpty { + background: #F5F7F8; + border: 1px dashed #cbd5e1; + border-radius: 8px; + padding: 2rem 1rem; + text-align: center; + color: #6b7a8d; +} + +.notificationCardUnread { + background: #f0f9ff; + border-left: 3px solid #15abff; + transition: background 0.15s ease; +} + +.notificationCardRead { + background: #ffffff; + border-left: 3px solid #d5dce4; + transition: background 0.15s ease; +} + +.notificationCardUnread:hover { + background: #e6f4ff; +} + +.notificationCardRead:hover { + background: #F5F7F8; +} + +/* ── Data tables ──────────────────────────────────────── */ + +.tableSurface { + border: 1px solid #e8eef3; + border-radius: 8px; + overflow: hidden; +} + +.tableClean :global(thead th) { + background: #F5F7F8; + color: #3d4f5f; + font-weight: 600; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; + border-bottom: 1px solid #e8eef3; + padding: 0.75rem 1rem; +} + +.tableClean :global(tbody td) { + vertical-align: middle; + padding: 0.75rem 1rem; + border-bottom: 1px solid #f1f5f8; + color: #3d4f5f; + font-size: 0.875rem; +} + +.tableClean :global(tbody tr) { + transition: background 0.1s ease; +} + +.tableClean :global(tbody tr:hover) { + background: #f8fbff; +} + +/* ── Utilities ────────────────────────────────────────── */ + +.cellClamp { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.monoRef { + font-family: 'SFMono-Regular', Menlo, Consolas, 'Courier New', monospace; + color: #3d4f5f; + font-size: 0.8rem; + font-weight: 500; + background: #F5F7F8; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #e8eef3; +} + +.statusBadge { + font-weight: 600; + font-size: 0.75rem; + padding: 3px 10px; + border-radius: 999px; +} + +.pillInfo { + background: #15abff12; + border: 1px solid #15abff30; + color: #0d8ddb; + border-radius: 999px; + padding: 3px 10px; + font-size: 0.78rem; + font-weight: 500; + display: inline-block; +} + +/* ── Responsive ───────────────────────────────────────── */ + +@media (max-width: 768px) { + .headerBlock { + flex-direction: column; + align-items: stretch; + } + + .cellClamp { + max-width: 140px; + } +} 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/ComplaintBulkActionModal.jsx b/src/Modules/ComplaintManagement/components/ComplaintBulkActionModal.jsx new file mode 100644 index 000000000..9b8da3a89 --- /dev/null +++ b/src/Modules/ComplaintManagement/components/ComplaintBulkActionModal.jsx @@ -0,0 +1,231 @@ +import PropTypes from "prop-types"; +import { useEffect, useMemo, useState } from "react"; +import { + Alert, + Button, + Group, + Modal, + Select, + Stack, + Text, + TextInput, + Textarea, +} from "@mantine/core"; +import { IconAlertCircle } from "@tabler/icons-react"; + +const STATUS_OPTIONS = [ + { value: "1", label: "In Progress" }, + { value: "2", label: "Resolved" }, +]; + +const initialForm = { + assigned_to: "", + assigned_team: "", + remarks: "", + status: "1", + progress_notes: "", + estimated_resolution_time: "", +}; + +export default function ComplaintBulkActionModal({ + opened, + mode, + selectedCount, + workers, + onClose, + onSubmit, + isLoading = false, +}) { + const [form, setForm] = useState(initialForm); + const [error, setError] = useState(""); + + useEffect(() => { + if (opened) { + setForm(initialForm); + setError(""); + } + }, [opened, mode]); + + const workerOptions = useMemo( + () => + workers.map((worker) => ({ + value: String(worker.id), + label: `${worker.name || `Worker ${worker.id}`} (${worker.worker_type || "general"})`, + })), + [workers], + ); + + const handleChange = (field, value) => { + setForm((prev) => ({ ...prev, [field]: value })); + if (error) { + setError(""); + } + }; + + const handleSubmit = () => { + if (mode === "reassign") { + if (!form.assigned_to) { + setError("Select a worker to reassign the complaint batch."); + return; + } + onSubmit({ + assigned_to: form.assigned_to, + assigned_team: form.assigned_team, + remarks: form.remarks, + }); + return; + } + + if (!form.status) { + setError("Select the intervention status."); + return; + } + + if (!form.remarks.trim()) { + setError("Remarks are required for intervention actions."); + return; + } + + onSubmit({ + status: Number(form.status), + remarks: form.remarks, + progress_notes: form.progress_notes || form.remarks, + estimated_resolution_time: form.estimated_resolution_time || null, + }); + }; + + let title = "Bulk Intervention"; + if (mode === "reassign") { + title = "Bulk Reassign Complaints"; + } + const helperText = + mode === "reassign" + ? "Assign the selected complaints to a new worker and optionally add a team note." + : "Move the selected complaints forward with one status update and shared remarks."; + + return ( + + + } + color="blue" + variant="light" + > + {selectedCount} complaint{selectedCount === 1 ? "" : "s"} selected. + + + + {helperText} + + + {mode === "reassign" ? ( + <> +