fix billing and 2fa admin
Build & Push / Pipeline Tests (push) Failing after 1m58s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 58s
Test / API Unit Tests (push) Successful in 1m9s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 41s
Test / Dashboard Unit Tests (push) Successful in 45s
Test / API Integration Tests (push) Failing after 1m9s
Build & Push / Pipeline Tests (push) Failing after 1m58s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 58s
Test / API Unit Tests (push) Successful in 1m9s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 41s
Test / Dashboard Unit Tests (push) Successful in 45s
Test / API Integration Tests (push) Failing after 1m9s
This commit is contained in:
@@ -6,6 +6,14 @@ import {
|
||||
formatNotificationDate,
|
||||
type NotificationsPageResult,
|
||||
} from '@/lib/adminNotifications'
|
||||
import { ADMIN_API_BASE } from '@/lib/api'
|
||||
|
||||
interface AdminInboxItem {
|
||||
id: string
|
||||
readAt: string | null
|
||||
createdAt: string
|
||||
notificationEvent: { title: string; body: string; type: string; locale: string; data?: Record<string, unknown> }
|
||||
}
|
||||
|
||||
const CHANNELS = ['EMAIL', 'SMS', 'WHATSAPP', 'IN_APP', 'PUSH']
|
||||
const STATUSES = ['PENDING', 'QUEUED', 'SENT', 'DELIVERED', 'FAILED', 'SKIPPED', 'DEAD_LETTER', 'READ']
|
||||
@@ -49,6 +57,14 @@ export default function AdminNotificationsPage() {
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [filterCompany, setFilterCompany] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [inbox, setInbox] = useState<{ data: AdminInboxItem[]; unread: number }>({ data: [], unread: 0 })
|
||||
|
||||
function loadInbox() {
|
||||
fetch(`${ADMIN_API_BASE}/admin/notifications/me`, { credentials: 'include', cache: 'no-store' })
|
||||
.then((response) => response.ok ? response.json() : Promise.reject(new Error('Failed to load personal inbox')))
|
||||
.then((json) => setInbox(json.data))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function load(p: number, signal?: AbortSignal) {
|
||||
setLoading(true)
|
||||
@@ -73,6 +89,7 @@ export default function AdminNotificationsPage() {
|
||||
const controller = new AbortController()
|
||||
setPage(1)
|
||||
load(1, controller.signal)
|
||||
loadInbox()
|
||||
return () => controller.abort()
|
||||
}, [filterChannel, filterStatus, filterCompany])
|
||||
|
||||
@@ -83,6 +100,11 @@ export default function AdminNotificationsPage() {
|
||||
|
||||
const totalPages = result ? (result.totalPages ?? Math.ceil(result.total / result.pageSize)) : 0
|
||||
|
||||
async function markRead(recipientId: string) {
|
||||
const response = await fetch(`${ADMIN_API_BASE}/admin/notifications/me/${recipientId}/read`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
||||
if (response.ok) loadInbox()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="shell py-8 space-y-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
@@ -100,6 +122,27 @@ export default function AdminNotificationsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className="panel p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-orange-400">My inbox</p>
|
||||
<h2 className="mt-1 text-lg font-semibold text-zinc-100">Assigned operational notices</h2>
|
||||
</div>
|
||||
<span className="rounded-full bg-red-500/15 px-3 py-1 text-xs font-semibold text-red-300">{inbox.unread} unread</span>
|
||||
</div>
|
||||
{inbox.data.length === 0 ? <p className="mt-4 text-sm text-zinc-500">No assigned notices.</p> : (
|
||||
<div className="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
{inbox.data.map((item) => (
|
||||
<button key={item.id} type="button" onClick={() => markRead(item.id)} className={`rounded-xl border p-4 text-left ${item.readAt ? 'border-zinc-800 bg-zinc-950/50' : 'border-orange-500/40 bg-orange-500/5'}`}>
|
||||
<p className="text-xs uppercase tracking-wide text-zinc-500">{item.notificationEvent.type.replaceAll('_', ' ')} · {item.notificationEvent.locale}</p>
|
||||
<p className="mt-2 font-semibold text-zinc-100">{item.notificationEvent.title}</p>
|
||||
<p className="mt-1 text-sm text-zinc-400">{item.notificationEvent.body}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<select
|
||||
|
||||
Reference in New Issue
Block a user