admin login fixed.
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
This commit is contained in:
@@ -1,32 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ADMIN_API_BASE } from '@/lib/api'
|
||||
|
||||
interface NotificationItem {
|
||||
id: string
|
||||
type: string
|
||||
title: string
|
||||
body: string
|
||||
channel: string
|
||||
status: string
|
||||
locale: string
|
||||
sentAt: string | null
|
||||
createdAt: string
|
||||
company: { name: string } | null
|
||||
companyId: string | null
|
||||
renterId: string | null
|
||||
}
|
||||
|
||||
interface Paginated {
|
||||
data: NotificationItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
import {
|
||||
fetchAdminNotifications,
|
||||
formatNotificationDate,
|
||||
type NotificationsPageResult,
|
||||
} from '@/lib/adminNotifications'
|
||||
|
||||
const CHANNELS = ['EMAIL', 'SMS', 'WHATSAPP', 'IN_APP', 'PUSH']
|
||||
const STATUSES = ['PENDING', 'SENT', 'DELIVERED', 'FAILED', 'READ']
|
||||
const STATUSES = ['PENDING', 'QUEUED', 'SENT', 'DELIVERED', 'FAILED', 'SKIPPED', 'DEAD_LETTER', 'READ']
|
||||
|
||||
const CHANNEL_BADGE: Record<string, string> = {
|
||||
EMAIL: 'text-sky-400 bg-sky-950/40',
|
||||
@@ -41,11 +23,26 @@ const STATUS_BADGE: Record<string, string> = {
|
||||
SENT: 'text-emerald-400 bg-emerald-950/40',
|
||||
DELIVERED: 'text-emerald-400 bg-emerald-950/40',
|
||||
FAILED: 'text-red-400 bg-red-950/40',
|
||||
QUEUED: 'text-sky-400 bg-sky-950/40',
|
||||
SKIPPED: 'text-zinc-400 bg-zinc-800',
|
||||
DEAD_LETTER: 'text-red-300 bg-red-950/60',
|
||||
READ: 'text-zinc-400 bg-zinc-800',
|
||||
}
|
||||
|
||||
function formatRecipient(item: {
|
||||
recipientType: string | null
|
||||
recipientName: string | null
|
||||
recipientEmail: string | null
|
||||
employeeId: string | null
|
||||
renterId: string | null
|
||||
}) {
|
||||
const fallbackId = item.employeeId ?? item.renterId
|
||||
const label = item.recipientName || item.recipientEmail || (fallbackId ? `${fallbackId.slice(0, 10)}...` : '-')
|
||||
return item.recipientType ? `${item.recipientType}: ${label}` : label
|
||||
}
|
||||
|
||||
export default function AdminNotificationsPage() {
|
||||
const [result, setResult] = useState<Paginated | null>(null)
|
||||
const [result, setResult] = useState<NotificationsPageResult | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [filterChannel, setFilterChannel] = useState('')
|
||||
@@ -53,27 +50,30 @@ export default function AdminNotificationsPage() {
|
||||
const [filterCompany, setFilterCompany] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
function load(p: number) {
|
||||
function load(p: number, signal?: AbortSignal) {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const params = new URLSearchParams({ page: String(p), pageSize: '50' })
|
||||
if (filterChannel) params.set('channel', filterChannel)
|
||||
if (filterStatus) params.set('status', filterStatus)
|
||||
if (filterCompany) params.set('companyId', filterCompany)
|
||||
|
||||
fetch(`${ADMIN_API_BASE}/admin/notifications?${params.toString()}`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((json) => setResult(json.data ?? null))
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
fetchAdminNotifications(
|
||||
p,
|
||||
{ channel: filterChannel, status: filterStatus, companyId: filterCompany },
|
||||
signal,
|
||||
)
|
||||
.then((data) => setResult(data))
|
||||
.catch((err) => {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
setResult(null)
|
||||
setError(err instanceof Error ? err.message : 'Failed to load notifications')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!signal?.aborted) setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
setPage(1)
|
||||
load(1)
|
||||
load(1, controller.signal)
|
||||
return () => controller.abort()
|
||||
}, [filterChannel, filterStatus, filterCompany])
|
||||
|
||||
function goToPage(p: number) {
|
||||
@@ -81,7 +81,7 @@ export default function AdminNotificationsPage() {
|
||||
load(p)
|
||||
}
|
||||
|
||||
const totalPages = result ? Math.ceil(result.total / result.pageSize) : 0
|
||||
const totalPages = result ? (result.totalPages ?? Math.ceil(result.total / result.pageSize)) : 0
|
||||
|
||||
return (
|
||||
<div className="shell py-8 space-y-6">
|
||||
@@ -118,6 +118,12 @@ export default function AdminNotificationsPage() {
|
||||
<option value="">All statuses</option>
|
||||
{STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
<input
|
||||
value={filterCompany}
|
||||
onChange={(e) => setFilterCompany(e.target.value)}
|
||||
placeholder="Company ID"
|
||||
className="w-64 rounded-xl border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="panel p-4 text-sm text-red-400">{error}</div>}
|
||||
@@ -129,6 +135,7 @@ export default function AdminNotificationsPage() {
|
||||
<tr className="border-b border-zinc-800">
|
||||
<th className="px-5 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">Date</th>
|
||||
<th className="px-5 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">Company</th>
|
||||
<th className="px-5 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">Recipient</th>
|
||||
<th className="px-5 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">Event</th>
|
||||
<th className="px-5 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">Channel</th>
|
||||
<th className="px-5 py-3 text-left text-xs font-medium uppercase tracking-wider text-zinc-500">Title</th>
|
||||
@@ -138,17 +145,23 @@ export default function AdminNotificationsPage() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800/60">
|
||||
{loading ? (
|
||||
<tr><td colSpan={7} className="px-5 py-12 text-center text-zinc-500">Loading…</td></tr>
|
||||
<tr><td colSpan={8} className="px-5 py-12 text-center text-zinc-500">Loading…</td></tr>
|
||||
) : !result || result.data.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-5 py-12 text-center text-zinc-500">No notifications found.</td></tr>
|
||||
<tr><td colSpan={8} className="px-5 py-12 text-center text-zinc-500">No notifications found.</td></tr>
|
||||
) : result.data.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-zinc-800/30 transition-colors">
|
||||
<td className="whitespace-nowrap px-5 py-3 text-xs text-zinc-500">
|
||||
{new Date(item.createdAt).toLocaleString()}
|
||||
{formatNotificationDate(item.createdAt)}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-5 py-3 text-xs text-zinc-300">
|
||||
{item.company?.name ?? (item.companyId ? item.companyId.slice(0, 10) + '…' : '—')}
|
||||
</td>
|
||||
<td className="max-w-[220px] px-5 py-3 text-xs text-zinc-300">
|
||||
<p className="truncate">{formatRecipient(item)}</p>
|
||||
{item.recipientEmail && item.recipientName ? (
|
||||
<p className="truncate text-zinc-500">{item.recipientEmail}</p>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-5 py-3 text-xs font-medium text-zinc-300">
|
||||
{item.type.replaceAll('_', ' ')}
|
||||
</td>
|
||||
@@ -167,7 +180,7 @@ export default function AdminNotificationsPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-5 py-3 text-xs text-zinc-500">
|
||||
{item.sentAt ? new Date(item.sentAt).toLocaleString() : '—'}
|
||||
{formatNotificationDate(item.sentAt)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user