From 5fb1debe84dbac8e5555d803491553c84e24d269 Mon Sep 17 00:00:00 2001 From: azahjessica49-commits Date: Thu, 30 Jul 2026 07:27:39 +0100 Subject: [PATCH] Add admin queue assignment --- dongle/app/admin/page.tsx | 295 ++++++++++++++++-- dongle/services/audit/audit-log.service.ts | 4 + .../services/review/review-report.service.ts | 55 ++++ .../services/stellar/verification.service.ts | 91 ++++++ dongle/types/audit-log.ts | 10 +- dongle/types/review.ts | 2 + 6 files changed, 424 insertions(+), 33 deletions(-) diff --git a/dongle/app/admin/page.tsx b/dongle/app/admin/page.tsx index 5f6cacc..f3af61c 100644 --- a/dongle/app/admin/page.tsx +++ b/dongle/app/admin/page.tsx @@ -8,13 +8,14 @@ import WalletStatePanel, { } from "@/components/wallet/WalletStatePanel"; import { useAdminAccess } from "@/hooks/useAdminAccess"; import { formatDate } from "@/lib/date"; -import { AlertCircle, Flag, Shield, CheckCircle, XCircle, Clock, MessageSquare, ScrollText } from "lucide-react"; +import { AlertCircle, Flag, Shield, CheckCircle, XCircle, Clock, MessageSquare, ScrollText, User, UserMinus } from "lucide-react"; import { reviewReportService } from "@/services/review/review-report.service"; import { projectReportService } from "@/services/project/project-report.service"; import { projectClaimService } from "@/services/project/project-claim.service"; import { projectService } from "@/services/project/project.service"; import { reviewService } from "@/services/review/review.service"; import { auditLogService } from "@/services/audit/audit-log.service"; +import { verificationService } from "@/services/stellar/verification.service"; import { ReviewReport, ModerationAction, Review } from "@/types/review"; import AuditLogViewer from "@/components/admin/AuditLogViewer"; @@ -47,6 +48,8 @@ export default function AdminDashboard() { const [projectModerationLog, setProjectModerationLog] = useState([]); const [moderationReason, setModerationReason] = useState>({}); const [expandedReport, setExpandedReport] = useState(null); + const [verificationFilter, setVerificationFilter] = useState<"all" | "assigned-to-me" | "unassigned">("all"); + const [reportFilter, setReportFilter] = useState<"all" | "assigned-to-me" | "unassigned">("all"); // Load reports and moderation log useEffect(() => { @@ -57,7 +60,7 @@ export default function AdminDashboard() { setClaimRequests(projectClaimService.getRequests()); setModerationLog(reviewReportService.getModerationLog()); setProjectModerationLog(projectReportService.getModerationLog()); - } + }, 0); }, [isAdmin]); const handleAction = (id: string, status: "approved" | "rejected") => { @@ -156,6 +159,84 @@ export default function AdminDashboard() { } }; + const handleAssignVerification = async (requestId: string, assignedTo: string) => { + if (!gate.publicKey) return; + + try { + await verificationService.assignRequest(requestId, gate.publicKey, assignedTo); + auditLogService.append({ + actor: gate.publicKey, + action: "verification_assigned", + targetId: requestId, + targetLabel: `Verification Request ${requestId}`, + metadata: { assignedTo }, + }); + toast.success("Request assigned"); + setRequests((prev) => prev.map((r: any) => + r.id === requestId ? { ...r, assignedTo, assignedAt: new Date().toISOString() } : r + )); + } catch (error) { + toast.error("Failed to assign request"); + } + }; + + const handleUnassignVerification = async (requestId: string) => { + if (!gate.publicKey) return; + + try { + await verificationService.unassignRequest(requestId, gate.publicKey); + auditLogService.append({ + actor: gate.publicKey, + action: "verification_unassigned", + targetId: requestId, + targetLabel: `Verification Request ${requestId}`, + }); + toast.success("Request unassigned"); + setRequests((prev) => prev.map((r: any) => + r.id === requestId ? { ...r, assignedTo: undefined, assignedAt: undefined } : r + )); + } catch (error) { + toast.error("Failed to unassign request"); + } + }; + + const handleAssignReport = (reportId: string, assignedTo: string) => { + if (!gate.publicKey) return; + + const result = reviewReportService.assignReport(reportId, gate.publicKey, assignedTo); + if (result.success) { + auditLogService.append({ + actor: gate.publicKey, + action: "report_assigned", + targetId: reportId, + targetLabel: `Report ${reportId}`, + metadata: { assignedTo }, + }); + toast.success("Report assigned"); + setReports(reviewReportService.getReports()); + } else { + toast.error(result.error || "Failed to assign report"); + } + }; + + const handleUnassignReport = (reportId: string) => { + if (!gate.publicKey) return; + + const result = reviewReportService.unassignReport(reportId, gate.publicKey); + if (result.success) { + auditLogService.append({ + actor: gate.publicKey, + action: "report_unassigned", + targetId: reportId, + targetLabel: `Report ${reportId}`, + }); + toast.success("Report unassigned"); + setReports(reviewReportService.getReports()); + } else { + toast.error(result.error || "Failed to unassign report"); + } + }; + const getReviewForReport = (reviewId: string): Review | undefined => { return reviewsById[reviewId]; }; @@ -273,18 +354,59 @@ export default function AdminDashboard() { {activeTab === "verification" && (
-

- - Verification Requests -

+
+

+ + Verification Requests +

+
+ + + +
+
- {requests.map((req) => ( + {requests + .filter((req: any) => { + if (verificationFilter === "all") return true; + if (verificationFilter === "assigned-to-me") return req.assignedTo === gate.publicKey; + if (verificationFilter === "unassigned") return !req.assignedTo; + return true; + }) + .map((req: any) => (
-
+

{req.projectName}

Submitted by: @@ -292,7 +414,7 @@ export default function AdminDashboard() { {formatDate(req.timestamp, "short")}
-
+
{req.status} + {req.assignedTo && ( + + Assigned to + + )}
- {req.status === "pending" && ( -
- +
+ {req.assignedTo ? ( + req.assignedTo === gate.publicKey ? ( + + ) : ( + + ) + ) : ( -
- )} + )} + {req.status === "pending" && ( + <> + + + + )} +
))}
@@ -380,15 +536,49 @@ export default function AdminDashboard() { {activeTab === "reports" && (
-

- - Pending Reports - {(pendingReports.length + pendingProjectReports.length) > 0 && ( - - ({pendingReports.length + pendingProjectReports.length} pending) - - )} -

+
+

+ + Pending Reports + {(pendingReports.length + pendingProjectReports.length) > 0 && ( + + ({pendingReports.length + pendingProjectReports.length} pending) + + )} +

+
+ + + +
+
@@ -467,7 +657,14 @@ export default function AdminDashboard() {
) : (
- {pendingReports.map((report) => { + {pendingReports + .filter((report: any) => { + if (reportFilter === "all") return true; + if (reportFilter === "assigned-to-me") return report.assignedTo === gate.publicKey; + if (reportFilter === "unassigned") return !report.assignedTo; + return true; + }) + .map((report: any) => { const review = getReviewForReport(report.reviewId); return (
{report.reason} + {report.assignedTo && ( + + Assigned to + + )}
Reported by • {formatDate(report.createdAt, "relative")}
+
+ {report.assignedTo ? ( + report.assignedTo === gate.publicKey ? ( + + ) : ( + + ) + ) : ( + + )} +
{review && ( diff --git a/dongle/services/audit/audit-log.service.ts b/dongle/services/audit/audit-log.service.ts index 60d3892..667609a 100644 --- a/dongle/services/audit/audit-log.service.ts +++ b/dongle/services/audit/audit-log.service.ts @@ -24,9 +24,13 @@ export const AUDIT_LOG_STORAGE_KEY = "dongle_audit_log"; const VALID_ACTIONS: ReadonlySet = new Set([ "verification_approved", "verification_rejected", + "verification_assigned", + "verification_unassigned", "fee_updated", "report_resolved", "report_dismissed", + "report_assigned", + "report_unassigned", ]); /** Hydrate, validate, and return all stored entries (SSR-safe). */ diff --git a/dongle/services/review/review-report.service.ts b/dongle/services/review/review-report.service.ts index e778032..7557326 100644 --- a/dongle/services/review/review-report.service.ts +++ b/dongle/services/review/review-report.service.ts @@ -89,6 +89,8 @@ export const reviewReportService = { explanation: record.explanation, status: record.status as ReviewReportStatus, createdAt: record.createdAt, + assignedTo: typeof record.assignedTo === "string" ? record.assignedTo : undefined, + assignedAt: typeof record.assignedAt === "string" ? record.assignedAt : undefined, }; validatedReports.push(report); @@ -113,6 +115,59 @@ export const reviewReportService = { return this.getReports().filter((r) => r.status === "pending"); }, + assignReport( + reportId: string, + assignedBy: string, + assignedTo: string, + ): { success: boolean; error?: string } { + const reports = this.getReports(); + const index = reports.findIndex((r) => r.id === reportId); + + if (index === -1) { + return { success: false, error: "Report not found" }; + } + + // Update report assignment + reports[index] = { + ...reports[index], + assignedTo, + assignedAt: new Date().toISOString(), + }; + localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(reports)); + + return { success: true }; + }, + + unassignReport( + reportId: string, + unassignedBy: string, + ): { success: boolean; error?: string } { + const reports = this.getReports(); + const index = reports.findIndex((r) => r.id === reportId); + + if (index === -1) { + return { success: false, error: "Report not found" }; + } + + // Remove assignment + reports[index] = { + ...reports[index], + assignedTo: undefined, + assignedAt: undefined, + }; + localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(reports)); + + return { success: true }; + }, + + getReportsAssignedTo(adminAddress: string): ReviewReport[] { + return this.getReports().filter((r) => r.assignedTo === adminAddress); + }, + + getUnassignedReports(): ReviewReport[] { + return this.getReports().filter((r) => !r.assignedTo); + }, + hasUserReportedReview(reviewId: string, userAddress: string): boolean { return this.getReports().some( (r) => r.reviewId === reviewId && r.reporterAddress === userAddress diff --git a/dongle/services/stellar/verification.service.ts b/dongle/services/stellar/verification.service.ts index f091bcb..dffab27 100644 --- a/dongle/services/stellar/verification.service.ts +++ b/dongle/services/stellar/verification.service.ts @@ -16,6 +16,8 @@ export interface VerificationRequest { statusUpdatedAt: string; statusUpdatedBy?: string; rejectionReason?: string; + assignedTo?: string; + assignedAt?: string; } const VERIFICATION_STORAGE_KEY = "dongle_verification_requests"; @@ -126,6 +128,95 @@ class VerificationService { } } + /** + * Assigns a verification request to an admin. + */ + async assignRequest( + projectId: string, + assignedBy: string, + assignedTo: string, + ): Promise { + try { + const request = await this.getVerificationRequest(projectId); + + if (!request) { + throw new Error("Verification request not found"); + } + + // Update request assignment + request.assignedTo = assignedTo; + request.assignedAt = new Date().toISOString(); + + // Persist updated request + await this.persistRequest(request); + + console.log(`[VerificationService] Request assigned: ${projectId} to ${assignedTo}`); + return request; + } catch (error) { + console.error("[VerificationService] Error assigning request:", error); + throw error; + } + } + + /** + * Unassigns a verification request (removes assignment). + */ + async unassignRequest( + projectId: string, + unassignedBy: string, + ): Promise { + try { + const request = await this.getVerificationRequest(projectId); + + if (!request) { + throw new Error("Verification request not found"); + } + + // Remove assignment + delete request.assignedTo; + delete request.assignedAt; + + // Persist updated request + await this.persistRequest(request); + + console.log(`[VerificationService] Request unassigned: ${projectId}`); + return request; + } catch (error) { + console.error("[VerificationService] Error unassigning request:", error); + throw error; + } + } + + /** + * Gets verification requests assigned to a specific admin. + */ + async getRequestsAssignedTo(adminAddress: string): Promise { + try { + const requests = this.loadRequests(); + return requests + .filter((r) => r.assignedTo === adminAddress) + .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()); + } catch (error) { + console.error("[VerificationService] Error getting assigned requests:", error); + return []; + } + } + + /** + * Gets unassigned verification requests. + */ + async getUnassignedRequests(): Promise { + try { + const requests = this.loadRequests(); + return requests + .filter((r) => !r.assignedTo) + .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()); + } catch (error) { + console.error("[VerificationService] Error getting unassigned requests:", error); + return []; + } + } + /** * Approves a verification request (admin only). */ diff --git a/dongle/types/audit-log.ts b/dongle/types/audit-log.ts index 6343c8f..8f05da0 100644 --- a/dongle/types/audit-log.ts +++ b/dongle/types/audit-log.ts @@ -9,17 +9,25 @@ export type AuditAction = | "verification_approved" | "verification_rejected" + | "verification_assigned" + | "verification_unassigned" | "fee_updated" | "report_resolved" - | "report_dismissed"; + | "report_dismissed" + | "report_assigned" + | "report_unassigned"; /** Human-readable labels for each action. */ export const AUDIT_ACTION_LABELS: Record = { verification_approved: "Verification Approved", verification_rejected: "Verification Rejected", + verification_assigned: "Verification Assigned", + verification_unassigned: "Verification Unassigned", fee_updated: "Fee Updated", report_resolved: "Report Resolved", report_dismissed: "Report Dismissed", + report_assigned: "Report Assigned", + report_unassigned: "Report Unassigned", }; /** diff --git a/dongle/types/review.ts b/dongle/types/review.ts index 318280f..b43f624 100644 --- a/dongle/types/review.ts +++ b/dongle/types/review.ts @@ -60,6 +60,8 @@ export interface ReviewReport { explanation: string; status: ReviewReportStatus; createdAt: string; + assignedTo?: string; + assignedAt?: string; } export interface ModerationAction {