lomavuokraus/app/admin/pending/page.tsx
Tero Halla-aho cb92a17f1d
Some checks failed
CI / checks (push) Has been cancelled
CI / checks (pull_request) Has been cancelled
Add on-site EV charging amenity
2025-12-17 13:40:47 +02:00

191 lines
7.4 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useI18n } from '../../components/I18nProvider';
type PendingUser = { id: string; email: string; status: string; emailVerifiedAt: string | null; approvedAt: string | null; role: string };
type PendingListing = {
id: string;
status: string;
createdAt: string;
owner: { email: string };
translations: { title: string; slug: string; locale: string }[];
evChargingAvailable: boolean;
evChargingOnSite: boolean;
wheelchairAccessible: boolean;
};
export default function PendingAdminPage() {
const { t } = useI18n();
const [pendingUsers, setPendingUsers] = useState<PendingUser[]>([]);
const [pendingListings, setPendingListings] = useState<PendingListing[]>([]);
const [role, setRole] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function loadPending() {
setMessage(null);
setError(null);
try {
const res = await fetch('/api/admin/pending', { cache: 'no-store' });
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to load');
return;
}
setRole(data.role ?? null);
setPendingUsers(data.users ?? []);
setPendingListings(data.listings ?? []);
} catch (e) {
setError('Failed to load');
}
}
useEffect(() => {
fetch('/api/auth/me', { cache: 'no-store' })
.then((res) => res.json())
.then((data) => {
setRole(data.user?.role ?? null);
if (!data.user?.role) {
setError(t('adminRequired'));
return;
}
if (['ADMIN', 'USER_MODERATOR', 'LISTING_MODERATOR'].includes(data.user.role)) {
loadPending();
return;
}
setError(t('adminRequired'));
})
.catch(() => setError(t('adminRequired')));
}, [t]);
async function approveUser(userId: string, makeAdmin: boolean) {
setMessage(null);
setError(null);
const res = await fetch('/api/admin/users/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, makeAdmin }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to approve user');
} else {
setMessage(t('userUpdated'));
loadPending();
}
}
async function approveListing(listingId: string, action: 'approve' | 'reject' | 'remove') {
setMessage(null);
setError(null);
const reason =
action === 'reject'
? window.prompt(`${t('reject')}? (optional)`)
: action === 'remove'
? window.prompt(`${t('remove')}? (optional)`)
: null;
const res = await fetch('/api/admin/listings/approve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listingId, action, reason }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to update listing');
} else {
setMessage(t('approvalsMessage'));
loadPending();
}
}
async function rejectUser(userId: string) {
setMessage(null);
setError(null);
const reason = window.prompt(`${t('reject')}? (optional)`);
const res = await fetch('/api/admin/users/reject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, reason }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error || 'Failed to reject user');
} else {
setMessage(t('userUpdated'));
loadPending();
}
}
return (
<main className="panel" style={{ maxWidth: 960, margin: '40px auto' }}>
<h1>{t('pendingAdminTitle')}</h1>
<div style={{ display: 'grid', gap: 16 }}>
<section>
<h3>{t('pendingUsersTitle')}</h3>
{pendingUsers.length === 0 ? (
<p>{t('noPendingUsers')}</p>
) : (
<ul style={{ display: 'grid', gap: 8, padding: 0, listStyle: 'none' }}>
{pendingUsers.map((u) => (
<li key={u.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 12 }}>
<div>
<strong>{u.email}</strong> {t('statusLabel')}: {u.status} {t('verifiedLabel')}: {u.emailVerifiedAt ? t('yes') : t('no')}
</div>
<div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
<button className="button" onClick={() => approveUser(u.id, false)} disabled={role !== 'ADMIN' && role !== 'USER_MODERATOR'}>
{t('approve')}
</button>
<button className="button secondary" onClick={() => approveUser(u.id, true)} disabled={role !== 'ADMIN'}>
{t('approveAdmin')}
</button>
<button className="button secondary" onClick={() => rejectUser(u.id)} disabled={role !== 'ADMIN' && role !== 'USER_MODERATOR'}>
{t('reject')}
</button>
</div>
</li>
))}
</ul>
)}
</section>
<section>
<h3>{t('pendingListingsTitle')}</h3>
{pendingListings.length === 0 ? (
<p>{t('noPendingListings')}</p>
) : (
<ul style={{ display: 'grid', gap: 8, padding: 0, listStyle: 'none' }}>
{pendingListings.map((l) => (
<li key={l.id} style={{ border: '1px solid #ddd', borderRadius: 8, padding: 12 }}>
<div>
<strong>{l.translations[0]?.title ?? 'Listing'}</strong> owner: {l.owner.email}
</div>
<div style={{ marginTop: 6, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{l.evChargingOnSite ? <span className="badge">{t('amenityEvOnSite')}</span> : null}
{l.evChargingAvailable && !l.evChargingOnSite ? <span className="badge">{t('amenityEvNearby')}</span> : null}
{l.wheelchairAccessible ? <span className="badge">{t('amenityWheelchairAccessible')}</span> : null}
</div>
<div style={{ fontSize: 12, color: '#666' }}>
{t('slugsLabel')}: {l.translations.map((t) => `${t.slug} (${t.locale})`).join(', ')}
</div>
<div style={{ marginTop: 8, display: 'flex', gap: 8 }}>
<button className="button" onClick={() => approveListing(l.id, 'approve')} disabled={role !== 'ADMIN' && role !== 'LISTING_MODERATOR'}>
{t('publish')}
</button>
<button className="button secondary" onClick={() => approveListing(l.id, 'reject')} disabled={role !== 'ADMIN' && role !== 'LISTING_MODERATOR'}>
{t('reject')}
</button>
<button className="button secondary" onClick={() => approveListing(l.id, 'remove')} disabled={role !== 'ADMIN' && role !== 'LISTING_MODERATOR'}>
{t('remove')}
</button>
</div>
</li>
))}
</ul>
)}
</section>
</div>
{message ? <p style={{ marginTop: 12, color: 'green' }}>{message}</p> : null}
{error ? <p style={{ marginTop: 12, color: 'red' }}>{error}</p> : null}
</main>
);
}