generated from nhcarrigan/template
feat: add comment report management to admin interface
Updated the admin-reports component to handle both profile and comment reports: - Added report type toggle to switch between profile and comment reports - Duplicated report display logic for comment reports - Comment reports show the comment content with truncation - Added separate review modals for profile and comment reports - Comment reports display comment author instead of reported user - Maintains all existing functionality for profile reports Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,17 +9,39 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ReportService } from '../../services/report.service';
|
import { ReportService } from '../../services/report.service';
|
||||||
|
import { CommentReportService } from '../../services/comment-report.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/shared-types';
|
import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportReason } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-admin-reports',
|
selector: 'app-admin-reports',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule],
|
imports: [CommonModule, FormsModule],
|
||||||
template: `
|
template: `
|
||||||
|
<div class="admin-reports-wrapper">
|
||||||
<div class="admin-reports-container">
|
<div class="admin-reports-container">
|
||||||
<h1>Profile Reports</h1>
|
<div class="header-section">
|
||||||
|
<h1>Reports</h1>
|
||||||
|
<div class="report-type-toggle">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toggle-btn"
|
||||||
|
[class.active]="reportType() === 'profile'"
|
||||||
|
(click)="setReportType('profile')"
|
||||||
|
>
|
||||||
|
Profile Reports
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toggle-btn"
|
||||||
|
[class.active]="reportType() === 'comment'"
|
||||||
|
(click)="setReportType('comment')"
|
||||||
|
>
|
||||||
|
Comment Reports
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="loading" role="status" aria-live="polite">
|
<div class="loading" role="status" aria-live="polite">
|
||||||
@@ -40,7 +62,7 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
[attr.aria-selected]="activeFilter() === 'all'"
|
[attr.aria-selected]="activeFilter() === 'all'"
|
||||||
(click)="setFilter('all')"
|
(click)="setFilter('all')"
|
||||||
>
|
>
|
||||||
All ({{ allReports().length }})
|
All ({{ reportType() === 'profile' ? allProfileReports().length : allCommentReports().length }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -86,7 +108,8 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
|
|
||||||
<!-- Reports List -->
|
<!-- Reports List -->
|
||||||
<div class="reports-list" role="region" aria-label="Reports list">
|
<div class="reports-list" role="region" aria-label="Reports list">
|
||||||
@for (report of filteredReports(); track report.id) {
|
@if (reportType() === 'profile') {
|
||||||
|
@for (report of filteredProfileReports(); track report.id) {
|
||||||
<div class="report-card">
|
<div class="report-card">
|
||||||
<!-- Report Header -->
|
<!-- Report Header -->
|
||||||
<div class="report-header">
|
<div class="report-header">
|
||||||
@@ -182,7 +205,7 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-review"
|
class="btn btn-review"
|
||||||
(click)="openReviewModal(report)"
|
(click)="openProfileReviewModal(report)"
|
||||||
[attr.aria-label]="'Review report for ' + report.reportedUser.username"
|
[attr.aria-label]="'Review report for ' + report.reportedUser.username"
|
||||||
>
|
>
|
||||||
Review
|
Review
|
||||||
@@ -191,21 +214,133 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
</div>
|
</div>
|
||||||
} @empty {
|
} @empty {
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>No reports found.</p>
|
<p>No profile reports found.</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
} @else {
|
||||||
|
@for (report of filteredCommentReports(); track report.id) {
|
||||||
|
<div class="report-card">
|
||||||
|
<!-- Report Header -->
|
||||||
|
<div class="report-header">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-section">
|
||||||
|
<h3>Comment Author</h3>
|
||||||
|
<div class="user-details">
|
||||||
|
@if (report.reportedComment.user.avatar) {
|
||||||
|
<img
|
||||||
|
[src]="report.reportedComment.user.avatar"
|
||||||
|
[alt]="report.reportedComment.user.username + ' avatar'"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<div class="avatar-placeholder" [attr.aria-label]="report.reportedComment.user.username + ' avatar'">
|
||||||
|
{{ report.reportedComment.user.username.charAt(0).toUpperCase() }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<div class="user-names">
|
||||||
|
<span class="username">{{ report.reportedComment.user.username }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Review Modal -->
|
<div class="user-section">
|
||||||
@if (reviewingReport()) {
|
<h3>Reporter</h3>
|
||||||
|
<div class="user-details">
|
||||||
|
@if (report.reporter.avatar) {
|
||||||
|
<img
|
||||||
|
[src]="report.reporter.avatar"
|
||||||
|
[alt]="report.reporter.username + ' avatar'"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
} @else {
|
||||||
|
<div class="avatar-placeholder" [attr.aria-label]="report.reporter.username + ' avatar'">
|
||||||
|
{{ report.reporter.username.charAt(0).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="user-names">
|
||||||
|
<span class="username">{{ report.reporter.username }}</span>
|
||||||
|
@if (report.reporter.displayName) {
|
||||||
|
<span class="display-name">({{ report.reporter.displayName }})</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="report-meta">
|
||||||
|
<span
|
||||||
|
class="status-badge"
|
||||||
|
[class.pending]="report.status === 'PENDING'"
|
||||||
|
[class.reviewed]="report.status === 'REVIEWED'"
|
||||||
|
[class.dismissed]="report.status === 'DISMISSED'"
|
||||||
|
[class.action-taken]="report.status === 'ACTION_TAKEN'"
|
||||||
|
[attr.aria-label]="'Status: ' + formatStatus(report.status)"
|
||||||
|
>
|
||||||
|
{{ formatStatus(report.status) }}
|
||||||
|
</span>
|
||||||
|
<span class="report-date">
|
||||||
|
{{ formatDate(report.createdAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Content -->
|
||||||
|
<div class="report-content">
|
||||||
|
<div class="comment-content-preview">
|
||||||
|
<strong>Comment:</strong>
|
||||||
|
<p class="comment-text">{{ truncateComment(report.reportedComment.content) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="reason">
|
||||||
|
<strong>Reason:</strong> {{ formatReason(report.reason) }}
|
||||||
|
</div>
|
||||||
|
<div class="details">
|
||||||
|
<strong>Details:</strong>
|
||||||
|
<p>{{ report.details }}</p>
|
||||||
|
</div>
|
||||||
|
@if (report.reviewNotes) {
|
||||||
|
<div class="review-notes">
|
||||||
|
<strong>Review Notes:</strong>
|
||||||
|
<p>{{ report.reviewNotes }}</p>
|
||||||
|
@if (report.reviewer) {
|
||||||
|
<small class="reviewer">
|
||||||
|
Reviewed by {{ report.reviewer.username }}
|
||||||
|
</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Actions -->
|
||||||
|
<div class="report-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-review"
|
||||||
|
(click)="openCommentReviewModal(report)"
|
||||||
|
[attr.aria-label]="'Review report for comment by ' + report.reportedComment.user.username"
|
||||||
|
>
|
||||||
|
Review
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No comment reports found.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Review Modal -->
|
||||||
|
@if (reviewingProfileReport()) {
|
||||||
<div
|
<div
|
||||||
class="modal-overlay"
|
class="modal-overlay"
|
||||||
(click)="closeReviewModal()"
|
(click)="closeProfileReviewModal()"
|
||||||
(keydown.escape)="closeReviewModal()"
|
(keydown.escape)="closeProfileReviewModal()"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-title"
|
aria-labelledby="profile-modal-title"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="modal-content"
|
class="modal-content"
|
||||||
@@ -215,11 +350,11 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-title">Review Report</h2>
|
<h2 id="profile-modal-title">Review Profile Report</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="modal-close"
|
class="modal-close"
|
||||||
(click)="closeReviewModal()"
|
(click)="closeProfileReviewModal()"
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -231,29 +366,29 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
<div class="review-summary">
|
<div class="review-summary">
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<strong>Reported User:</strong>
|
<strong>Reported User:</strong>
|
||||||
<span>{{ reviewingReport()!.reportedUser.username }}</span>
|
<span>{{ reviewingProfileReport()!.reportedUser.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<strong>Reporter:</strong>
|
<strong>Reporter:</strong>
|
||||||
<span>{{ reviewingReport()!.reporter.username }}</span>
|
<span>{{ reviewingProfileReport()!.reporter.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<strong>Reason:</strong>
|
<strong>Reason:</strong>
|
||||||
<span>{{ formatReason(reviewingReport()!.reason) }}</span>
|
<span>{{ formatReason(reviewingProfileReport()!.reason) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<strong>Reported On:</strong>
|
<strong>Reported On:</strong>
|
||||||
<span>{{ formatDate(reviewingReport()!.createdAt) }}</span>
|
<span>{{ formatDate(reviewingProfileReport()!.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="details-section">
|
<div class="details-section">
|
||||||
<strong>Details:</strong>
|
<strong>Details:</strong>
|
||||||
<p class="details-text">{{ reviewingReport()!.details }}</p>
|
<p class="details-text">{{ reviewingProfileReport()!.details }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Review Form -->
|
<!-- Review Form -->
|
||||||
<form (ngSubmit)="submitReview()" class="review-form">
|
<form (ngSubmit)="submitProfileReview()" class="review-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="status">Update Status</label>
|
<label for="status">Update Status</label>
|
||||||
<select
|
<select
|
||||||
@@ -287,7 +422,121 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
(click)="closeReviewModal()"
|
(click)="closeProfileReviewModal()"
|
||||||
|
[disabled]="submitting()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
[disabled]="submitting()"
|
||||||
|
>
|
||||||
|
{{ submitting() ? 'Submitting...' : 'Submit Review' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Comment Review Modal -->
|
||||||
|
@if (reviewingCommentReport()) {
|
||||||
|
<div
|
||||||
|
class="modal-overlay"
|
||||||
|
(click)="closeCommentReviewModal()"
|
||||||
|
(keydown.escape)="closeCommentReviewModal()"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="comment-modal-title"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-content"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
(keydown)="$event.stopPropagation()"
|
||||||
|
role="document"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="comment-modal-title">Review Comment Report</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal-close"
|
||||||
|
(click)="closeCommentReviewModal()"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Report Summary -->
|
||||||
|
<div class="review-summary">
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>Comment Author:</strong>
|
||||||
|
<span>{{ reviewingCommentReport()!.reportedComment.user.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>Reporter:</strong>
|
||||||
|
<span>{{ reviewingCommentReport()!.reporter.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>Reason:</strong>
|
||||||
|
<span>{{ formatReason(reviewingCommentReport()!.reason) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-item">
|
||||||
|
<strong>Reported On:</strong>
|
||||||
|
<span>{{ formatDate(reviewingCommentReport()!.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comment-full-content">
|
||||||
|
<strong>Full Comment:</strong>
|
||||||
|
<p class="comment-text">{{ reviewingCommentReport()!.reportedComment.content }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-section">
|
||||||
|
<strong>Report Details:</strong>
|
||||||
|
<p class="details-text">{{ reviewingCommentReport()!.details }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Review Form -->
|
||||||
|
<form (ngSubmit)="submitCommentReview()" class="review-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comment-status">Update Status</label>
|
||||||
|
<select
|
||||||
|
id="comment-status"
|
||||||
|
name="status"
|
||||||
|
[(ngModel)]="reviewForm.status"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
aria-required="true"
|
||||||
|
>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="REVIEWED">Reviewed</option>
|
||||||
|
<option value="DISMISSED">Dismissed</option>
|
||||||
|
<option value="ACTION_TAKEN">Action Taken</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comment-reviewNotes">Review Notes (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
id="comment-reviewNotes"
|
||||||
|
name="reviewNotes"
|
||||||
|
[(ngModel)]="reviewForm.reviewNotes"
|
||||||
|
class="form-control"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Add any notes about your review..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
(click)="closeCommentReviewModal()"
|
||||||
[disabled]="submitting()"
|
[disabled]="submitting()"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -308,18 +557,66 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
|
.admin-reports-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-reports-container {
|
.admin-reports-container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: var(--witch-purple);
|
color: var(--witch-purple);
|
||||||
margin-bottom: 2rem;
|
margin: 0;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-type-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--witch-lavender);
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--witch-mauve);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
color: var(--witch-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn.active {
|
||||||
|
background: var(--witch-purple);
|
||||||
|
color: var(--witch-moon);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:focus {
|
||||||
|
outline: 2px solid var(--witch-purple);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.loading, .error, .empty-state {
|
.loading, .error, .empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem 2rem;
|
padding: 3rem 2rem;
|
||||||
@@ -535,6 +832,41 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-content-preview {
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--witch-lavender);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-content-preview strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--witch-purple);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-text {
|
||||||
|
color: var(--witch-mauve);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-full-content {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--witch-lavender);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-full-content strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--witch-purple);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* Report Actions */
|
/* Report Actions */
|
||||||
.report-actions {
|
.report-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -578,17 +910,21 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
|
|
||||||
/* Modal */
|
/* Modal */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed !important;
|
||||||
top: 0;
|
top: 0 !important;
|
||||||
left: 0;
|
left: 0 !important;
|
||||||
right: 0;
|
right: 0 !important;
|
||||||
bottom: 0;
|
bottom: 0 !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1000;
|
z-index: 9999 !important;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
margin: 0 !important;
|
||||||
|
transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@@ -813,6 +1149,7 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
|
|||||||
})
|
})
|
||||||
export class AdminReportsComponent implements OnInit {
|
export class AdminReportsComponent implements OnInit {
|
||||||
private reportService = inject(ReportService);
|
private reportService = inject(ReportService);
|
||||||
|
private commentReportService = inject(CommentReportService);
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
private toastService = inject(ToastService);
|
private toastService = inject(ToastService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
@@ -820,12 +1157,15 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
// Make ReportStatus accessible in template
|
// Make ReportStatus accessible in template
|
||||||
protected readonly ReportStatus = ReportStatus;
|
protected readonly ReportStatus = ReportStatus;
|
||||||
|
|
||||||
allReports = signal<ProfileReportWithUsers[]>([]);
|
reportType = signal<'profile' | 'comment'>('profile');
|
||||||
|
allProfileReports = signal<ProfileReportWithUsers[]>([]);
|
||||||
|
allCommentReports = signal<CommentReportWithDetails[]>([]);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
activeFilter = signal<'all' | ReportStatus>('all');
|
activeFilter = signal<'all' | ReportStatus>('all');
|
||||||
|
|
||||||
reviewingReport = signal<ProfileReportWithUsers | null>(null);
|
reviewingProfileReport = signal<ProfileReportWithUsers | null>(null);
|
||||||
|
reviewingCommentReport = signal<CommentReportWithDetails | null>(null);
|
||||||
submitting = signal(false);
|
submitting = signal(false);
|
||||||
|
|
||||||
reviewForm = {
|
reviewForm = {
|
||||||
@@ -833,29 +1173,49 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
reviewNotes: ''
|
reviewNotes: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
filteredReports = computed(() => {
|
filteredProfileReports = computed(() => {
|
||||||
const filter = this.activeFilter();
|
const filter = this.activeFilter();
|
||||||
if (filter === 'all') {
|
if (filter === 'all') {
|
||||||
return this.allReports();
|
return this.allProfileReports();
|
||||||
}
|
}
|
||||||
return this.allReports().filter(report => report.status === filter);
|
return this.allProfileReports().filter(report => report.status === filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
pendingCount = computed(() =>
|
filteredCommentReports = computed(() => {
|
||||||
this.allReports().filter(r => r.status === ReportStatus.PENDING).length
|
const filter = this.activeFilter();
|
||||||
);
|
if (filter === 'all') {
|
||||||
|
return this.allCommentReports();
|
||||||
|
}
|
||||||
|
return this.allCommentReports().filter(report => report.status === filter);
|
||||||
|
});
|
||||||
|
|
||||||
reviewedCount = computed(() =>
|
pendingCount = computed(() => {
|
||||||
this.allReports().filter(r => r.status === ReportStatus.REVIEWED).length
|
if (this.reportType() === 'profile') {
|
||||||
);
|
return this.allProfileReports().filter(r => r.status === ReportStatus.PENDING).length;
|
||||||
|
}
|
||||||
|
return this.allCommentReports().filter(r => r.status === ReportStatus.PENDING).length;
|
||||||
|
});
|
||||||
|
|
||||||
dismissedCount = computed(() =>
|
reviewedCount = computed(() => {
|
||||||
this.allReports().filter(r => r.status === ReportStatus.DISMISSED).length
|
if (this.reportType() === 'profile') {
|
||||||
);
|
return this.allProfileReports().filter(r => r.status === ReportStatus.REVIEWED).length;
|
||||||
|
}
|
||||||
|
return this.allCommentReports().filter(r => r.status === ReportStatus.REVIEWED).length;
|
||||||
|
});
|
||||||
|
|
||||||
actionTakenCount = computed(() =>
|
dismissedCount = computed(() => {
|
||||||
this.allReports().filter(r => r.status === ReportStatus.ACTION_TAKEN).length
|
if (this.reportType() === 'profile') {
|
||||||
);
|
return this.allProfileReports().filter(r => r.status === ReportStatus.DISMISSED).length;
|
||||||
|
}
|
||||||
|
return this.allCommentReports().filter(r => r.status === ReportStatus.DISMISSED).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
actionTakenCount = computed(() => {
|
||||||
|
if (this.reportType() === 'profile') {
|
||||||
|
return this.allProfileReports().filter(r => r.status === ReportStatus.ACTION_TAKEN).length;
|
||||||
|
}
|
||||||
|
return this.allCommentReports().filter(r => r.status === ReportStatus.ACTION_TAKEN).length;
|
||||||
|
});
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (!this.authService.isAdmin()) {
|
if (!this.authService.isAdmin()) {
|
||||||
@@ -870,34 +1230,54 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.error.set(null);
|
this.error.set(null);
|
||||||
|
|
||||||
|
if (this.reportType() === 'profile') {
|
||||||
this.reportService.getAllReports().subscribe({
|
this.reportService.getAllReports().subscribe({
|
||||||
next: (reports) => {
|
next: (reports) => {
|
||||||
this.allReports.set(reports);
|
this.allProfileReports.set(reports);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.error.set(err.message ?? 'Failed to load reports');
|
this.error.set(err.message ?? 'Failed to load profile reports');
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.toastService.error('Failed to load reports');
|
this.toastService.error('Failed to load profile reports');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.commentReportService.getAllReports().subscribe({
|
||||||
|
next: (reports) => {
|
||||||
|
this.allCommentReports.set(reports);
|
||||||
|
this.loading.set(false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.error.set(err.message ?? 'Failed to load comment reports');
|
||||||
|
this.loading.set(false);
|
||||||
|
this.toastService.error('Failed to load comment reports');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setReportType(type: 'profile' | 'comment'): void {
|
||||||
|
this.reportType.set(type);
|
||||||
|
this.activeFilter.set('all');
|
||||||
|
this.loadReports();
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(filter: 'all' | ReportStatus): void {
|
setFilter(filter: 'all' | ReportStatus): void {
|
||||||
this.activeFilter.set(filter);
|
this.activeFilter.set(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
openReviewModal(report: ProfileReportWithUsers): void {
|
openProfileReviewModal(report: ProfileReportWithUsers): void {
|
||||||
this.reviewingReport.set(report);
|
this.reviewingProfileReport.set(report);
|
||||||
this.reviewForm = {
|
this.reviewForm = {
|
||||||
status: report.status,
|
status: report.status,
|
||||||
reviewNotes: report.reviewNotes || ''
|
reviewNotes: report.reviewNotes || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
closeReviewModal(): void {
|
closeProfileReviewModal(): void {
|
||||||
if (!this.submitting()) {
|
if (!this.submitting()) {
|
||||||
this.reviewingReport.set(null);
|
this.reviewingProfileReport.set(null);
|
||||||
this.reviewForm = {
|
this.reviewForm = {
|
||||||
status: ReportStatus.PENDING,
|
status: ReportStatus.PENDING,
|
||||||
reviewNotes: ''
|
reviewNotes: ''
|
||||||
@@ -905,8 +1285,8 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitReview(): void {
|
submitProfileReview(): void {
|
||||||
const report = this.reviewingReport();
|
const report = this.reviewingProfileReport();
|
||||||
if (!report) {
|
if (!report) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -919,20 +1299,73 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
this.reviewForm.reviewNotes || undefined
|
this.reviewForm.reviewNotes || undefined
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: (updatedReport) => {
|
next: (updatedReport) => {
|
||||||
this.allReports.update(reports =>
|
this.allProfileReports.update(reports =>
|
||||||
reports.map(r => r.id === updatedReport.id ? updatedReport : r)
|
reports.map(r => r.id === updatedReport.id ? updatedReport : r)
|
||||||
);
|
);
|
||||||
this.toastService.success('Report updated successfully');
|
this.toastService.success('Profile report updated successfully');
|
||||||
this.submitting.set(false);
|
this.submitting.set(false);
|
||||||
this.closeReviewModal();
|
this.closeProfileReviewModal();
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.toastService.error(err.message ?? 'Failed to update report');
|
this.toastService.error(err.message ?? 'Failed to update profile report');
|
||||||
this.submitting.set(false);
|
this.submitting.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openCommentReviewModal(report: CommentReportWithDetails): void {
|
||||||
|
this.reviewingCommentReport.set(report);
|
||||||
|
this.reviewForm = {
|
||||||
|
status: report.status,
|
||||||
|
reviewNotes: report.reviewNotes || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCommentReviewModal(): void {
|
||||||
|
if (!this.submitting()) {
|
||||||
|
this.reviewingCommentReport.set(null);
|
||||||
|
this.reviewForm = {
|
||||||
|
status: ReportStatus.PENDING,
|
||||||
|
reviewNotes: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submitCommentReview(): void {
|
||||||
|
const report = this.reviewingCommentReport();
|
||||||
|
if (!report) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting.set(true);
|
||||||
|
|
||||||
|
this.commentReportService.updateReport(report.id, {
|
||||||
|
status: this.reviewForm.status,
|
||||||
|
reviewNotes: this.reviewForm.reviewNotes || undefined
|
||||||
|
}).subscribe({
|
||||||
|
next: (updatedReport) => {
|
||||||
|
this.allCommentReports.update(reports =>
|
||||||
|
reports.map(r => r.id === updatedReport.id ? updatedReport : r)
|
||||||
|
);
|
||||||
|
this.toastService.success('Comment report updated successfully');
|
||||||
|
this.submitting.set(false);
|
||||||
|
this.closeCommentReviewModal();
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.toastService.error(err.message ?? 'Failed to update comment report');
|
||||||
|
this.submitting.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
truncateComment(content: string, maxLength = 200): string {
|
||||||
|
const stripped = content.replace(/<[^>]*>/g, '');
|
||||||
|
if (stripped.length <= maxLength) {
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
return stripped.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
formatStatus(status: ReportStatus): string {
|
formatStatus(status: ReportStatus): string {
|
||||||
const statusMap: Record<ReportStatus, string> = {
|
const statusMap: Record<ReportStatus, string> = {
|
||||||
[ReportStatus.PENDING]: 'Pending',
|
[ReportStatus.PENDING]: 'Pending',
|
||||||
|
|||||||
Reference in New Issue
Block a user