refactor(admin): share the dev notification tester between desktop and phone

Both panels loaded the same trips and users, built the same twelve test payloads and sent them the same way. That now lives in useDevNotifications; each panel keeps its own markup and the phone keeps its own wording of the failure toast.
This commit is contained in:
Maurice
2026-10-09 09:57:41 +02:00
parent 5abe5f6519
commit 67f20829f7
4 changed files with 396 additions and 266 deletions
@@ -1,24 +1,14 @@
import React, { useState, useEffect } from 'react'
import React from 'react'
import {
Bell, Zap, CheckCircle, Navigation, Calendar, Clock, Image,
MessageSquare, Tag, UserPlus, Download, MapPin, Loader2,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { adminApi, tripsApi } from '../../../api/client'
import { useAuthStore } from '../../../store/authStore'
import { useToast } from '../../../components/shared/Toast'
import { useDevNotifications } from '../../../components/Admin/useDevNotifications'
import { MAdminCard, MAdminCardHead } from './MAdminUi'
interface Trip {
id: number
title: string
}
interface AppUser {
id: number
username: string
email: string
}
/** The phone panel toasts the thrown error's own message rather than the server's error field. */
const errorMessage = (err: unknown) => (err instanceof Error ? err.message : 'Failed')
const SELECT_CLASS =
'mb-2 h-[42px] w-full rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] px-3 text-[0.84375rem] text-m-ink outline-none focus:border-[color:var(--m-faint)]'
@@ -57,46 +47,13 @@ function Btn({
}
// Dev-only notification testing panel, re-skinned to the mobile admin system.
// All state, fetches and fire() payloads are ported verbatim from the desktop
// DevNotificationsPanel — only the presentation layer changes.
// All state, fetches and payloads come from useDevNotifications, shared
// with the desktop DevNotificationsPanel; only the presentation layer differs.
export default function MAdminDevNotificationsPanel(): React.ReactElement {
const toast = useToast()
const user = useAuthStore(s => s.user)
const [sending, setSending] = useState<string | null>(null)
const [trips, setTrips] = useState<Trip[]>([])
const [selectedTripId, setSelectedTripId] = useState<number | null>(null)
const [users, setUsers] = useState<AppUser[]>([])
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
useEffect(() => {
tripsApi.list().then(data => {
const list = (data.trips || data || []) as Trip[]
setTrips(list)
if (list.length > 0) setSelectedTripId(list[0].id)
}).catch(() => {})
adminApi.users().then(data => {
const list = (data.users || data || []) as AppUser[]
setUsers(list)
if (list.length > 0) setSelectedUserId(list[0].id)
}).catch(() => {})
}, [])
const fire = async (label: string, payload: Record<string, unknown>) => {
setSending(label)
try {
await adminApi.sendTestNotification(payload)
toast.success(`Sent: ${label}`)
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : 'Failed')
} finally {
setSending(null)
}
}
const selectedTrip = trips.find(t => t.id === selectedTripId)
const selectedUser = users.find(u => u.id === selectedUserId)
const username = user?.username || 'Admin'
const tripTitle = selectedTrip?.title || 'Test Trip'
const {
sending, trips, selectedTripId, setSelectedTripId, users, selectedUserId, setSelectedUserId,
tripInviteId, vacayInviteId, send,
} = useDevNotifications({ errorMessage })
return (
<div className="space-y-3">
@@ -118,41 +75,16 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement {
/>
<div className="mt-2 grid grid-cols-1 gap-2">
<Btn sending={sending} id="simple-me" label="Simple → Me" sub="test_simple · user" icon={Bell} color="#6366f1"
onClick={() => fire('simple-me', {
event: 'test_simple',
scope: 'user',
targetId: user?.id,
params: {},
})}
onClick={send.simpleMe}
/>
<Btn sending={sending} id="boolean-me" label="Boolean → Me" sub="test_boolean · user" icon={CheckCircle} color="#10b981"
onClick={() => fire('boolean-me', {
event: 'test_boolean',
scope: 'user',
targetId: user?.id,
params: {},
inApp: {
type: 'boolean',
positiveCallback: { action: 'test_approve', payload: {} },
negativeCallback: { action: 'test_deny', payload: {} },
},
})}
onClick={send.booleanMe}
/>
<Btn sending={sending} id="navigate-me" label="Navigate → Me" sub="test_navigate · user" icon={Navigation} color="#f59e0b"
onClick={() => fire('navigate-me', {
event: 'test_navigate',
scope: 'user',
targetId: user?.id,
params: {},
})}
onClick={send.navigateMe}
/>
<Btn sending={sending} id="simple-admins" label="Simple → All Admins" sub="test_simple · admin" icon={Zap} color="#ef4444"
onClick={() => fire('simple-admins', {
event: 'test_simple',
scope: 'admin',
targetId: 0,
params: {},
})}
onClick={send.simpleAdmins}
/>
</div>
</MAdminCard>
@@ -174,44 +106,19 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement {
</select>
<div className="grid grid-cols-1 gap-2">
<Btn sending={sending} id="booking_change" label="booking_change" sub="navigate · trip" icon={Calendar} color="#6366f1"
onClick={() => selectedTripId && fire('booking_change', {
event: 'booking_change',
scope: 'trip',
targetId: selectedTripId,
params: { actor: username, trip: tripTitle, booking: 'Test Hotel', type: 'hotel', tripId: String(selectedTripId) },
})}
onClick={send.bookingChange}
/>
<Btn sending={sending} id="trip_reminder" label="trip_reminder" sub="navigate · trip" icon={Clock} color="#10b981"
onClick={() => selectedTripId && fire('trip_reminder', {
event: 'trip_reminder',
scope: 'trip',
targetId: selectedTripId,
params: { trip: tripTitle, tripId: String(selectedTripId) },
})}
onClick={send.tripReminder}
/>
<Btn sending={sending} id="photos_shared" label="photos_shared" sub="navigate · trip" icon={Image} color="#f59e0b"
onClick={() => selectedTripId && fire('photos_shared', {
event: 'photos_shared',
scope: 'trip',
targetId: selectedTripId,
params: { actor: username, trip: tripTitle, count: '5', tripId: String(selectedTripId) },
})}
onClick={send.photosShared}
/>
<Btn sending={sending} id="collab_message" label="collab_message" sub="navigate · trip" icon={MessageSquare} color="#8b5cf6"
onClick={() => selectedTripId && fire('collab_message', {
event: 'collab_message',
scope: 'trip',
targetId: selectedTripId,
params: { actor: username, trip: tripTitle, preview: 'This is a test message preview.', tripId: String(selectedTripId) },
})}
onClick={send.collabMessage}
/>
<Btn sending={sending} id="packing_tagged" label="packing_tagged" sub="navigate · trip" icon={Tag} color="#ec4899"
onClick={() => selectedTripId && fire('packing_tagged', {
event: 'packing_tagged',
scope: 'trip',
targetId: selectedTripId,
params: { actor: username, trip: tripTitle, category: 'Clothing', tripId: String(selectedTripId) },
})}
onClick={send.packingTagged}
/>
</div>
</div>
@@ -236,31 +143,21 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement {
<div className="grid grid-cols-1 gap-2">
<Btn
sending={sending}
id={`trip_invite-${selectedUserId}`}
id={tripInviteId}
label="trip_invite"
sub="navigate · user"
icon={UserPlus}
color="#06b6d4"
onClick={() => selectedUserId && fire(`trip_invite-${selectedUserId}`, {
event: 'trip_invite',
scope: 'user',
targetId: selectedUserId,
params: { actor: username, trip: tripTitle, invitee: selectedUser?.email || '', tripId: String(selectedTripId ?? 0) },
})}
onClick={send.tripInvite}
/>
<Btn
sending={sending}
id={`vacay_invite-${selectedUserId}`}
id={vacayInviteId}
label="vacay_invite"
sub="navigate · user"
icon={MapPin}
color="#f97316"
onClick={() => selectedUserId && fire(`vacay_invite-${selectedUserId}`, {
event: 'vacay_invite',
scope: 'user',
targetId: selectedUserId,
params: { actor: username, planId: '1' },
})}
onClick={send.vacayInvite}
/>
</div>
</div>
@@ -275,12 +172,7 @@ export default function MAdminDevNotificationsPanel(): React.ReactElement {
/>
<div className="mt-2 grid grid-cols-1 gap-2">
<Btn sending={sending} id="version_available" label="version_available" sub="navigate · admin" icon={Download} color="#64748b"
onClick={() => fire('version_available', {
event: 'version_available',
scope: 'admin',
targetId: 0,
params: { version: '9.9.9-test' },
})}
onClick={send.versionAvailable}
/>
</div>
</MAdminCard>