Add notification preferences and admin governance
This commit is contained in:
@@ -115,5 +115,65 @@
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded border bg-white p-6">
|
||||
<h3 class="font-semibold">Moderation reports</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">Reports are triage records for profiles, jobs, and attachments. They do not delete or hide content by themselves.</p>
|
||||
<div class="mt-4 space-y-4">
|
||||
@forelse ($moderationReports as $report)
|
||||
<div class="rounded border bg-gray-50 p-4 text-sm">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<p class="font-semibold">{{ class_basename($report->reportable_type) }} #{{ $report->reportable_id }} | {{ \App\Models\ModerationReport::REASON_CODES[$report->reason_code] ?? $report->reason_code }}</p>
|
||||
<p class="mt-1 text-gray-600">Reported by {{ $report->reporter->name }} | {{ $report->status }} | {{ $report->created_at->diffForHumans() }}</p>
|
||||
<p class="mt-2 whitespace-pre-line text-gray-700">{{ $report->details }}</p>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('moderation-reports.moderate', $report) }}" class="grid min-w-72 gap-2">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<label class="text-xs font-semibold text-gray-600" for="report_status_{{ $report->id }}">Report status</label>
|
||||
<select id="report_status_{{ $report->id }}" name="status" class="rounded-md border-gray-300 text-sm">
|
||||
@foreach (\App\Models\ModerationReport::STATUSES as $status)
|
||||
<option value="{{ $status }}" @selected($report->status === $status)>{{ str_replace('_', ' ', ucfirst($status)) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<textarea name="moderation_notes" rows="2" class="rounded-md border-gray-300 text-sm" placeholder="Moderation notes">{{ $report->moderation_notes }}</textarea>
|
||||
<x-primary-button>Save report</x-primary-button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-gray-600">No moderation reports are waiting.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded border bg-white p-6">
|
||||
<h3 class="font-semibold">Recent audit log</h3>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead>
|
||||
<tr class="text-left text-xs uppercase text-gray-500">
|
||||
<th class="py-2 pr-4">When</th>
|
||||
<th class="py-2 pr-4">Actor</th>
|
||||
<th class="py-2 pr-4">Action</th>
|
||||
<th class="py-2 pr-4">Target</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@forelse ($auditLogs as $log)
|
||||
<tr>
|
||||
<td class="py-2 pr-4">{{ $log->created_at->format('M j, g:i A') }}</td>
|
||||
<td class="py-2 pr-4">{{ $log->actor?->name ?? 'System' }}</td>
|
||||
<td class="py-2 pr-4">{{ str_replace('.', ' ', $log->action) }}</td>
|
||||
<td class="py-2 pr-4">{{ $log->auditable_type ? class_basename($log->auditable_type).' #'.$log->auditable_id : 'n/a' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="py-3 text-gray-600">No audit records yet.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
<x-attachments :attachments="$profile->attachments" />
|
||||
<x-rating-summary :ratings="$profile->ratings" />
|
||||
<x-rating-form type="buyer_profile" :id="$profile->id" category="buyer_overall" />
|
||||
<x-moderation-report-form type="buyer_profile" :id="$profile->id" />
|
||||
</section>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
@if ($attachment->caption)
|
||||
<p class="mt-2 text-slate-600 dark:text-slate-400">{{ $attachment->caption }}</p>
|
||||
@endif
|
||||
<x-moderation-report-form type="attachment" :id="$attachment->id" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
@props(['type', 'id'])
|
||||
|
||||
@auth
|
||||
<details class="mt-4 rounded-md border border-slate-200 p-4 text-sm dark:border-slate-800">
|
||||
<summary class="cursor-pointer font-semibold text-slate-950 dark:text-white">Report for moderation</summary>
|
||||
<form method="POST" action="{{ route('moderation-reports.store') }}" class="mt-3 space-y-3">
|
||||
@csrf
|
||||
<input type="hidden" name="reportable_type" value="{{ $type }}">
|
||||
<input type="hidden" name="reportable_id" value="{{ $id }}">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-800 dark:text-slate-200">Reason</label>
|
||||
<select name="reason_code" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
|
||||
@foreach (\App\Models\ModerationReport::REASON_CODES as $code => $label)
|
||||
<option value="{{ $code }}">{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<x-field name="details" label="Details" textarea />
|
||||
<x-secondary-button type="submit">Submit report</x-secondary-button>
|
||||
</form>
|
||||
</details>
|
||||
@endauth
|
||||
@@ -86,6 +86,7 @@
|
||||
<x-attachments :attachments="$job->attachments" />
|
||||
<x-rating-summary :ratings="$job->ratings" />
|
||||
<x-rating-form type="job_post" :id="$job->id" category="job_quality" />
|
||||
<x-moderation-report-form type="job_post" :id="$job->id" />
|
||||
@auth
|
||||
@if (auth()->id() === $job->buyer_id)
|
||||
<x-attachment-form type="job_post" :id="$job->id" kind="job" />
|
||||
|
||||
@@ -1,27 +1,96 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header"><h2 class="text-xl font-semibold">Notifications</h2></x-slot>
|
||||
<div class="mx-auto max-w-5xl space-y-4 p-6">
|
||||
<form method="POST" action="{{ route('notifications.read-all') }}">
|
||||
@csrf
|
||||
<x-secondary-button>Mark all read</x-secondary-button>
|
||||
</form>
|
||||
<x-slot name="header">
|
||||
<x-page-header
|
||||
title="Notifications"
|
||||
description="Control in-app alert categories now, and store explicit email/push intent for future delivery channels."
|
||||
/>
|
||||
</x-slot>
|
||||
<div class="mx-auto max-w-5xl space-y-6 p-6">
|
||||
<section class="tse-panel p-6">
|
||||
<h3 class="font-semibold text-slate-950 dark:text-white">Alert preferences</h3>
|
||||
<form method="POST" action="{{ route('notifications.preferences') }}" class="mt-4 space-y-5">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<label class="rounded-md border border-slate-200 p-3 text-sm dark:border-slate-800">
|
||||
<input type="checkbox" name="in_app_enabled" value="1" @checked($preference->in_app_enabled) class="rounded border-slate-300 text-sky-600">
|
||||
<span class="ml-2 font-semibold">In-app alerts</span>
|
||||
</label>
|
||||
<label class="rounded-md border border-slate-200 p-3 text-sm dark:border-slate-800">
|
||||
<input type="checkbox" name="email_enabled" value="1" @checked($preference->email_enabled) class="rounded border-slate-300 text-sky-600">
|
||||
<span class="ml-2 font-semibold">Email intent</span>
|
||||
</label>
|
||||
<label class="rounded-md border border-slate-200 p-3 text-sm dark:border-slate-800">
|
||||
<input type="checkbox" name="push_enabled" value="1" @checked($preference->push_enabled) class="rounded border-slate-300 text-sky-600">
|
||||
<span class="ml-2 font-semibold">Push intent</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="digest_frequency" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Digest preference</label>
|
||||
<select id="digest_frequency" name="digest_frequency" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
|
||||
@foreach (['immediate' => 'Immediate', 'daily' => 'Daily digest', 'weekly' => 'Weekly digest'] as $value => $label)
|
||||
<option value="{{ $value }}" @selected($preference->digest_frequency === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<x-field name="quiet_hours_start" label="Quiet hours start" type="time" :value="$preference->quiet_hours_start" />
|
||||
<x-field name="quiet_hours_end" label="Quiet hours end" type="time" :value="$preference->quiet_hours_end" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-slate-800 dark:text-slate-200">Category controls</p>
|
||||
<div class="mt-2 grid gap-3 md:grid-cols-2">
|
||||
@foreach ($eventCategories as $field => $label)
|
||||
<label class="rounded-md border border-slate-200 p-3 text-sm dark:border-slate-800">
|
||||
<input type="checkbox" name="{{ $field }}" value="1" @checked($preference->{$field}) class="rounded border-slate-300 text-sky-600">
|
||||
<span class="ml-2">{{ $label }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<details class="rounded-md border border-slate-200 p-4 dark:border-slate-800">
|
||||
<summary class="cursor-pointer font-semibold text-slate-950 dark:text-white">Fine tune event types</summary>
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||
@foreach ($eventTypes as $event => $category)
|
||||
<label class="rounded-md border border-slate-200 p-3 text-sm dark:border-slate-800">
|
||||
<input type="checkbox" name="event_preferences[{{ $event }}]" value="1" @checked(($preference->event_preferences[$event] ?? $preference->{$category}) === true) class="rounded border-slate-300 text-sky-600">
|
||||
<span class="ml-2">{{ str_replace('_', ' ', $event) }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
<x-primary-button>Save preferences</x-primary-button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="tse-panel p-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h3 class="font-semibold text-slate-950 dark:text-white">Inbox</h3>
|
||||
<form method="POST" action="{{ route('notifications.read-all') }}">
|
||||
@csrf
|
||||
<x-secondary-button>Mark all read</x-secondary-button>
|
||||
</form>
|
||||
</div>
|
||||
@foreach ($notifications as $notification)
|
||||
<div class="rounded border bg-white p-4 {{ $notification->read_at ? '' : 'border-indigo-400' }}">
|
||||
<p class="font-semibold">{{ $notification->data['title'] ?? 'Notification' }}</p>
|
||||
<p class="mt-1 text-sm text-gray-600">{{ $notification->data['body'] ?? '' }}</p>
|
||||
<div class="mt-4 rounded-md border bg-white p-4 text-sm dark:bg-slate-900 {{ $notification->read_at ? 'border-slate-200 dark:border-slate-800' : 'border-sky-400' }}">
|
||||
<p class="font-semibold text-slate-950 dark:text-white">{{ $notification->data['title'] ?? 'Notification' }}</p>
|
||||
<p class="mt-1 text-slate-600 dark:text-slate-400">{{ $notification->data['body'] ?? '' }}</p>
|
||||
<div class="mt-3 flex gap-3 text-sm">
|
||||
@if (! empty($notification->data['url']))
|
||||
<a class="text-indigo-600" href="{{ $notification->data['url'] }}">Open</a>
|
||||
<a class="font-semibold text-sky-700 dark:text-sky-300" href="{{ $notification->data['url'] }}">Open</a>
|
||||
@endif
|
||||
@unless ($notification->read_at)
|
||||
<form method="POST" action="{{ route('notifications.read', $notification->id) }}">
|
||||
@csrf
|
||||
<button class="text-gray-600" type="submit">Mark read</button>
|
||||
<button class="text-slate-600 dark:text-slate-400" type="submit">Mark read</button>
|
||||
</form>
|
||||
@endunless
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
{{ $notifications->links() }}
|
||||
</section>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<x-attachments :attachments="$profile->attachments" />
|
||||
<x-rating-summary :ratings="$profile->ratings" />
|
||||
<x-rating-form type="provider_profile" :id="$profile->id" category="provider_overall" />
|
||||
<x-moderation-report-form type="provider_profile" :id="$profile->id" />
|
||||
</section>
|
||||
|
||||
<section class="tse-panel p-6">
|
||||
|
||||
Reference in New Issue
Block a user