feat: Implement mobile application and lead processing utilities.
This commit is contained in:
23
innungsapp/apps/admin/app/api/push-token/route.ts
Normal file
23
innungsapp/apps/admin/app/api/push-token/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/lib/auth'
|
||||
import { prisma } from '@innungsapp/shared'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await auth.api.getSession({ headers: req.headers })
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { token } = await req.json()
|
||||
if (!token || typeof token !== 'string') {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Store push token on the member record
|
||||
await prisma.member.updateMany({
|
||||
where: { userId: session.user.id },
|
||||
data: { pushToken: token },
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
155
innungsapp/apps/admin/app/dashboard/mitglieder/[id]/page.tsx
Normal file
155
innungsapp/apps/admin/app/dashboard/mitglieder/[id]/page.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
|
||||
import { use } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc-client'
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { SPARTEN, MEMBER_STATUS_LABELS } from '@innungsapp/shared'
|
||||
|
||||
export default function MitgliedEditPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = use(params)
|
||||
const router = useRouter()
|
||||
const { data: member, isLoading } = trpc.members.byId.useQuery({ id })
|
||||
const updateMutation = trpc.members.update.useMutation({
|
||||
onSuccess: () => router.push('/dashboard/mitglieder'),
|
||||
})
|
||||
const resendMutation = trpc.members.resendInvite.useMutation()
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
betrieb: '',
|
||||
sparte: '',
|
||||
ort: '',
|
||||
telefon: '',
|
||||
email: '',
|
||||
status: 'aktiv' as 'aktiv' | 'ruhend' | 'ausgetreten',
|
||||
istAusbildungsbetrieb: false,
|
||||
seit: undefined as number | undefined,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (member) {
|
||||
setForm({
|
||||
name: member.name,
|
||||
betrieb: member.betrieb,
|
||||
sparte: member.sparte,
|
||||
ort: member.ort,
|
||||
telefon: member.telefon ?? '',
|
||||
email: member.email,
|
||||
status: member.status,
|
||||
istAusbildungsbetrieb: member.istAusbildungsbetrieb,
|
||||
seit: member.seit ?? undefined,
|
||||
})
|
||||
}
|
||||
}, [member])
|
||||
|
||||
if (isLoading) return <div className="text-gray-500">Wird geladen...</div>
|
||||
if (!member) return null
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
updateMutation.mutate({ id, data: form })
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
'w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500'
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/dashboard/mitglieder" className="text-gray-400 hover:text-gray-600">
|
||||
← Zurück
|
||||
</Link>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Mitglied bearbeiten</h1>
|
||||
</div>
|
||||
|
||||
{/* Invite Status */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700">App-Zugang</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{member.userId
|
||||
? '✓ Mitglied hat sich eingeloggt'
|
||||
: 'Noch nicht eingeladen / eingeloggt'}
|
||||
</p>
|
||||
</div>
|
||||
{!member.userId && (
|
||||
<button
|
||||
onClick={() => resendMutation.mutate({ memberId: id })}
|
||||
disabled={resendMutation.isPending}
|
||||
className="text-sm text-brand-600 hover:underline disabled:opacity-50"
|
||||
>
|
||||
{resendMutation.isPending ? 'Sende...' : resendMutation.isSuccess ? '✓ Gesendet' : 'Einladung senden'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="bg-white rounded-xl border shadow-sm p-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
||||
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputClass} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Betrieb</label>
|
||||
<input value={form.betrieb} onChange={(e) => setForm({ ...form, betrieb: e.target.value })} className={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Sparte</label>
|
||||
<select value={form.sparte} onChange={(e) => setForm({ ...form, sparte: e.target.value })} className={inputClass}>
|
||||
{SPARTEN.map((s) => <option key={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Ort</label>
|
||||
<input value={form.ort} onChange={(e) => setForm({ ...form, ort: e.target.value })} className={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">E-Mail</label>
|
||||
<input type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Telefon</label>
|
||||
<input type="tel" value={form.telefon} onChange={(e) => setForm({ ...form, telefon: e.target.value })} className={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select value={form.status} onChange={(e) => setForm({ ...form, status: e.target.value as typeof form.status })} className={inputClass}>
|
||||
{(['aktiv', 'ruhend', 'ausgetreten'] as const).map((s) => (
|
||||
<option key={s} value={s}>{MEMBER_STATUS_LABELS[s]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Mitglied seit</label>
|
||||
<input type="number" value={form.seit ?? ''} onChange={(e) => setForm({ ...form, seit: e.target.value ? Number(e.target.value) : undefined })} className={inputClass} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={form.istAusbildungsbetrieb} onChange={(e) => setForm({ ...form, istAusbildungsbetrieb: e.target.checked })} className="rounded border-gray-300 text-brand-500 focus:ring-brand-500" />
|
||||
<span className="text-sm text-gray-700">Ausbildungsbetrieb</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{updateMutation.error && (
|
||||
<p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg">{updateMutation.error.message}</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-2 border-t">
|
||||
<button type="submit" disabled={updateMutation.isPending} className="bg-brand-500 text-white px-6 py-2 rounded-lg text-sm font-medium hover:bg-brand-600 disabled:opacity-60 transition-colors">
|
||||
{updateMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
|
||||
</button>
|
||||
<Link href="/dashboard/mitglieder" className="px-6 py-2 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-100 transition-colors">
|
||||
Abbrechen
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
"name": "@innungsapp/admin",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./server/routers/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
Reference in New Issue
Block a user