diff --git a/src/Modules/ComplaintManagement/components/ActivityTimeline.jsx b/src/Modules/ComplaintManagement/components/ActivityTimeline.jsx new file mode 100644 index 000000000..da63b1ea9 --- /dev/null +++ b/src/Modules/ComplaintManagement/components/ActivityTimeline.jsx @@ -0,0 +1,121 @@ +import React from "react"; +import { Flex, Text, Badge, Timeline, ThemeIcon } from "@mantine/core"; +import { + ClockClockwise, + ArrowUp, + CheckCircle, + XCircle, + ArrowCounterClockwise, + Warning, + UserCircle, +} from "@phosphor-icons/react"; +import PropTypes from "prop-types"; + +const ACTION_CONFIG = { + COMPLAINT_SUBMITTED: { color: "blue", icon: ClockClockwise, label: "Submitted" }, + COMPLAINT_UPDATED: { color: "cyan", icon: ClockClockwise, label: "Updated" }, + PROGRESS_UPDATE: { color: "indigo", icon: ArrowUp, label: "Progress Update" }, + MANUAL_ESCALATION: { color: "orange", icon: Warning, label: "Escalated (Manual)" }, + AUTO_ESCALATION: { color: "red", icon: Warning, label: "Escalated (Auto)" }, + COMPLAINT_CLOSED: { color: "green", icon: CheckCircle, label: "Closed" }, + COMPLAINT_REOPENED: { color: "yellow", icon: ArrowCounterClockwise, label: "Reopened" }, + FEEDBACK_SUBMITTED: { color: "teal", icon: UserCircle, label: "Feedback" }, + SLA_REMINDER: { color: "orange", icon: Warning, label: "SLA Reminder" }, + ADMIN_ASSIGN: { color: "grape", icon: UserCircle, label: "Admin Reassigned" }, + REOPEN_REQUESTED: { color: "yellow", icon: ArrowCounterClockwise, label: "Reopen Requested" }, + REOPEN_DENIED: { color: "red", icon: XCircle, label: "Reopen Denied" }, + AUTO_ESCALATION_FAILED: { color: "red", icon: Warning, label: "Escalation Failed" }, +}; + +const STATUS_MAP = { + 0: "Pending", + 1: "In Progress", + 2: "Resolved", + 3: "Declined", + 4: "Escalated", + 5: "Closed", + 6: "Reopened", +}; + +function formatTimestamp(ts) { + if (!ts) return ""; + const d = new Date(ts); + return d.toLocaleString("en-IN", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +function ActivityTimeline({ activityLogs }) { + if (!activityLogs || activityLogs.length === 0) { + return ( + + No activity recorded yet. + + ); + } + + return ( + + {activityLogs.map((log) => { + const config = ACTION_CONFIG[log.action] || { + color: "gray", + icon: ClockClockwise, + label: log.action, + }; + const IconComp = config.icon; + + return ( + + + + } + title={ + + + {config.label} + + {log.previous_status !== null && log.new_status !== null && ( + + {STATUS_MAP[log.previous_status] || log.previous_status} →{" "} + {STATUS_MAP[log.new_status] || log.new_status} + + )} + + } + > + + {formatTimestamp(log.timestamp)} + + {log.details && ( + + {log.details} + + )} + + ); + })} + + ); +} + +ActivityTimeline.propTypes = { + activityLogs: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.number, + action: PropTypes.string, + previous_status: PropTypes.number, + new_status: PropTypes.number, + details: PropTypes.string, + timestamp: PropTypes.string, + }), + ), +}; + +export default ActivityTimeline; diff --git a/src/Modules/ComplaintManagement/components/AdminDashboard.jsx b/src/Modules/ComplaintManagement/components/AdminDashboard.jsx new file mode 100644 index 000000000..e6ff81bb7 --- /dev/null +++ b/src/Modules/ComplaintManagement/components/AdminDashboard.jsx @@ -0,0 +1,387 @@ +import React, { useState, useEffect } from "react"; +import { + Paper, Text, Button, Flex, Grid, Badge, Select, + Loader, Center, Divider, Title, +} from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { getAdminComplaints, adminAssign, getCaretakers, getSupervisors, extractApiErrorMessage } from "../routes/api"; +import ComplaintDetails from "./ComplaintDetails"; + +const STATUS_MAP = { + 0: { label: "Pending", color: "blue" }, + 1: { label: "In Progress", color: "cyan" }, + 2: { label: "Resolved", color: "green" }, + 3: { label: "Declined", color: "red" }, + 4: { label: "Escalated", color: "orange" }, + 5: { label: "Closed", color: "teal" }, + 6: { label: "Reopened", color: "yellow" }, +}; + +function AdminDashboard() { + const [complaints, setComplaints] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [errorMessage, setErrorMessage] = useState("Failed to load complaints."); + const [selectedComplaint, setSelectedComplaint] = useState(null); + const [showDetails, setShowDetails] = useState(false); + const [assigningId, setAssigningId] = useState(null); + const [caretakerId, setCaretakerId] = useState(""); + const [supervisorId, setSupervisorId] = useState(""); + const [assignLoading, setAssignLoading] = useState(false); + const [caretakerOptions, setCaretakerOptions] = useState([]); + const [supervisorOptions, setSupervisorOptions] = useState([]); + const [scope, setScope] = useState("overdue_escalated"); + + const token = localStorage.getItem("authToken"); + + const isOverdueComplaint = (complaint) => ( + !!complaint.sla_deadline + && new Date(complaint.sla_deadline) < new Date() + && [0, 1, 6].includes(complaint.status) + ); + + const stats = { + total: complaints.length, + escalated: complaints.filter((complaint) => complaint.status === 4).length, + overdue: complaints.filter((complaint) => isOverdueComplaint(complaint)).length, + needsAssignment: complaints.filter((complaint) => !complaint.assigned_caretaker || !complaint.assigned_supervisor).length, + }; + + const fetchComplaints = async () => { + setIsLoading(true); + setIsError(false); + setErrorMessage("Failed to load complaints."); + const response = await getAdminComplaints(token, scope); + if (response.success) { + setComplaints(response.data.results || []); + } else { + setIsError(true); + const message = extractApiErrorMessage(response.error, "Failed to load complaints."); + setErrorMessage(message); + notifications.show({ + title: "Error", + message, + color: "red", + }); + } + setIsLoading(false); + }; + + const fetchAssignees = async () => { + const [caretakersResponse, supervisorsResponse] = await Promise.all([ + getCaretakers(token), + getSupervisors(token), + ]); + if (caretakersResponse.success) { + const caretakers = caretakersResponse.data.caretakers || []; + setCaretakerOptions(caretakers.map((caretaker) => ({ + value: String(caretaker.id), + label: `#${caretaker.id} - ${caretaker.area}`, + }))); + } + if (supervisorsResponse.success) { + const supervisors = supervisorsResponse.data.supervisors || []; + setSupervisorOptions(supervisors.map((supervisor) => ({ + value: String(supervisor.id), + label: `#${supervisor.id} - ${supervisor.area}`, + }))); + } + }; + + useEffect(() => { + fetchComplaints(); + fetchAssignees(); + }, [scope]); + + const handleAssign = async (complaintId) => { + if (!caretakerId && !supervisorId) { + notifications.show({ + title: "Error", + message: "Please enter at least one of Caretaker ID or Supervisor ID.", + color: "red", + }); + return; + } + setAssignLoading(true); + const caretakerValue = caretakerId ? Number(caretakerId) : null; + const supervisorValue = supervisorId ? Number(supervisorId) : null; + const response = await adminAssign(complaintId, caretakerValue, supervisorValue, token); + setAssignLoading(false); + if (response.success) { + notifications.show({ + title: "Intervention Applied", + message: "Complaint assignment has been updated and moved to active handling.", + color: "blue", + }); + setAssigningId(null); + setCaretakerId(""); + setSupervisorId(""); + fetchComplaints(); + } else { + const msg = + response?.error?.message || + response?.error?.detail || + (typeof response?.error === "string" + ? response.error + : JSON.stringify(response?.error || {})); + notifications.show({ + title: "Error", + message: msg || "Failed to assign complaint.", + color: "red", + }); + } + }; + + const formatDateTime = (datetimeStr) => { + const date = new Date(datetimeStr); + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes()).padStart(2, "0"); + return `${day}-${month}-${year}, ${hours}:${minutes}`; + }; + + const severityLabel = (priority) => { + const value = String(priority || "").toUpperCase(); + if (value === "EMERGENCY") return "Critical"; + if (value === "URGENT") return "High"; + if (value === "LOW") return "Low"; + return "Medium"; + }; + + const severityColor = (priority) => { + const value = String(priority || "").toUpperCase(); + if (value === "EMERGENCY") return "grape"; + if (value === "URGENT") return "red"; + if (value === "LOW") return "green"; + return "yellow"; + }; + + if (showDetails && selectedComplaint) { + return ( + + + setShowDetails(false)} + /> + + + ); + } + + return ( + + + Admin Dashboard + + + Monitor overdue and escalated complaints, review details, and intervene by reassigning caretaker/supervisor. + + + + + + + + + + + In Scope + {stats.total} + + + + + Escalated + {stats.escalated} + + + + + Overdue + {stats.overdue} + + + + + Needs Assignment + {stats.needsAssignment} + + + + + {isLoading ? ( +
+ +
+ ) : isError ? ( +
+ {errorMessage} +
+ ) : complaints.length === 0 ? ( +
+ No complaints found in this scope. +
+ ) : ( + complaints.map((complaint) => { + const statusInfo = STATUS_MAP[complaint.status] || { label: "Unknown", color: "gray" }; + const isOverdue = isOverdueComplaint(complaint); + const hasFeedback = !!complaint.has_feedback || (complaint.feedback || "").trim().length > 0; + + return ( + + + + + + #{complaint.id} + + + {statusInfo.label} + + {complaint.complaint_type} + {isOverdue && ( + + OVERDUE + + )} + {complaint.priority && ( + + {complaint.priority} + + )} + {hasFeedback && ( + + Feedback Available + + )} + + + + + + Date: {formatDateTime(complaint.complaint_date)} + + + Location: {complaint.specific_location}, {complaint.location} + + + + SLA Deadline: {complaint.sla_deadline ? formatDateTime(complaint.sla_deadline) : "N/A"} + + + {severityLabel(complaint.priority)} + + + + Assigned Caretaker: {complaint.assigned_caretaker || "Not assigned"} + + + Assigned Supervisor: {complaint.assigned_supervisor || "Not assigned"} + + + + + + + + Details: {complaint.details} + + + + {assigningId === complaint.id ? ( + + + + + + ) : ( + + )} + + + + + ); + }) + )} +
+
+ ); +} + +export default AdminDashboard; diff --git a/src/Modules/ComplaintManagement/components/CloseVerifyForm.jsx b/src/Modules/ComplaintManagement/components/CloseVerifyForm.jsx new file mode 100644 index 000000000..650b10c4e --- /dev/null +++ b/src/Modules/ComplaintManagement/components/CloseVerifyForm.jsx @@ -0,0 +1,147 @@ +import React, { useState } from "react"; +import { Text, Button, Flex, Textarea, Loader } from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import PropTypes from "prop-types"; +import { closeComplaint, createReopenRequest, extractApiErrorMessage } from "../routes/api"; + +function CloseVerifyForm({ complaint, onBack }) { + const [action, setAction] = useState(null); // "accept" or "reject" + const [rejectReason, setRejectReason] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const token = localStorage.getItem("authToken"); + + const handleAccept = async () => { + setIsLoading(true); + const response = await closeComplaint(complaint.id, true, token); + setIsLoading(false); + + if (response.success) { + notifications.show({ + title: "Closed", + message: "Complaint has been verified and closed. Please leave feedback.", + color: "green", + }); + onBack(); + } else { + const msg = extractApiErrorMessage(response.error, "Failed to close complaint."); + notifications.show({ + title: "Error", + message: msg || "Failed to close complaint.", + color: "red", + }); + } + }; + + const handleReject = async () => { + if (!rejectReason || rejectReason.length < 10) { + notifications.show({ + title: "Validation Error", + message: "Please provide a reason (min 10 characters).", + color: "red", + }); + return; + } + setIsLoading(true); + const response = await createReopenRequest(complaint.id, rejectReason, token); + setIsLoading(false); + + if (response.success) { + notifications.show({ + title: "Request Submitted", + message: + "Resolution rejection submitted. Reopen request sent to supervisor for approval.", + color: "yellow", + }); + onBack(); + } else { + const msg = extractApiErrorMessage(response.error, "Failed to submit reopen request."); + notifications.show({ + title: "Error", + message: msg || "Failed to submit reopen request.", + color: "red", + }); + } + }; + + return ( + + + Verify Resolution + + + Complaint ID: {complaint.id} + + + Type: {complaint.complaint_type} + + + Location: {complaint.specific_location}, {complaint.location} + + + Issue: {complaint.details} + + + This complaint has been marked as resolved. Do you accept the resolution? + + + {action === "reject" && ( + + + Reason for Rejection * + +