feat(web): admin discovery review queue with accept/reject
This commit is contained in:
76
web/src/routes/admin/discovery/+page.server.ts
Normal file
76
web/src/routes/admin/discovery/+page.server.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { PageServerLoad, Actions } from './$types.js';
|
||||
import { redirect, fail } from '@sveltejs/kit';
|
||||
import { serverFetch } from '$lib/api/client.server.js';
|
||||
|
||||
type DiscoveredMarket = {
|
||||
id: string;
|
||||
markt_name: string;
|
||||
stadt: string;
|
||||
bundesland: string;
|
||||
land: string;
|
||||
start_datum: string | null;
|
||||
end_datum: string | null;
|
||||
website: string;
|
||||
quellen: string[];
|
||||
extraktion: string;
|
||||
hinweis: string;
|
||||
matched_series_id: string | null;
|
||||
discovered_at: string;
|
||||
};
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const limit = Number(url.searchParams.get('limit') ?? 50);
|
||||
const offset = Number(url.searchParams.get('offset') ?? 0);
|
||||
const res = await serverFetch<DiscoveredMarket[]>(
|
||||
`/admin/discovery/queue?limit=${limit}&offset=${offset}`,
|
||||
cookies
|
||||
);
|
||||
return { queue: res.data, limit, offset };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
accept: async ({ request, cookies, fetch }) => {
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? '');
|
||||
if (!id) return fail(400, { error: 'missing id' });
|
||||
|
||||
try {
|
||||
const res = await serverFetch<{ series_id: string; edition_id: string }>(
|
||||
`/admin/discovery/queue/${id}/accept`,
|
||||
cookies,
|
||||
{ method: 'POST', fetch }
|
||||
);
|
||||
// Fire research asynchronously — don't await, the edit page handles streaming suggestions.
|
||||
void serverFetch(`/admin/markets/${res.data.edition_id}/research`, cookies, {
|
||||
method: 'POST',
|
||||
fetch
|
||||
}).catch(() => {
|
||||
// Silent: the edit page shows a toast if research hasn't completed.
|
||||
});
|
||||
redirect(303, `/admin/maerkte/${res.data.edition_id}/edit`);
|
||||
} catch (err) {
|
||||
if (err instanceof Response || (err as { status?: number })?.status === 303) throw err;
|
||||
const message = err instanceof Error ? err.message : 'Accept fehlgeschlagen.';
|
||||
return fail(500, { error: message });
|
||||
}
|
||||
},
|
||||
|
||||
reject: async ({ request, cookies, fetch }) => {
|
||||
const form = await request.formData();
|
||||
const id = String(form.get('id') ?? '');
|
||||
const reason = String(form.get('reason') ?? '');
|
||||
if (!id) return fail(400, { error: 'missing id' });
|
||||
|
||||
try {
|
||||
await serverFetch(`/admin/discovery/queue/${id}/reject`, cookies, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason }),
|
||||
fetch
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Reject fehlgeschlagen.';
|
||||
return fail(500, { error: message });
|
||||
}
|
||||
}
|
||||
};
|
||||
96
web/src/routes/admin/discovery/+page.svelte
Normal file
96
web/src/routes/admin/discovery/+page.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Admin · Discovery Queue</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-7xl px-4 py-8">
|
||||
<h1 class="text-2xl font-bold">Discovery Queue</h1>
|
||||
<p class="mt-1 text-sm text-stone-500">
|
||||
{data.queue.length} pending · showing from offset {data.offset}
|
||||
</p>
|
||||
|
||||
{#if data.queue.length === 0}
|
||||
<p class="mt-8 rounded border border-stone-200 bg-stone-50 p-6 text-center text-stone-600">
|
||||
Keine Einträge in der Warteschlange.
|
||||
</p>
|
||||
{:else}
|
||||
<table class="mt-6 w-full text-left text-sm">
|
||||
<thead class="border-b border-stone-200 text-stone-500">
|
||||
<tr>
|
||||
<th class="py-2">Land</th>
|
||||
<th>Region</th>
|
||||
<th>Markt</th>
|
||||
<th>Stadt</th>
|
||||
<th>Datum</th>
|
||||
<th>Website</th>
|
||||
<th>Quellen</th>
|
||||
<th>Extraktion</th>
|
||||
<th class="text-right">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.queue as row (row.id)}
|
||||
<tr class="border-b border-stone-100">
|
||||
<td class="py-2">{row.land}</td>
|
||||
<td>{row.bundesland}</td>
|
||||
<td class="font-medium">{row.markt_name}</td>
|
||||
<td>{row.stadt}</td>
|
||||
<td>
|
||||
{#if row.start_datum}
|
||||
{row.start_datum}{row.end_datum ? ` – ${row.end_datum}` : ''}
|
||||
{:else}
|
||||
<span class="text-stone-400">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if row.website}
|
||||
<a
|
||||
href={row.website}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="text-blue-600 underline">link</a
|
||||
>
|
||||
{:else}
|
||||
<span class="text-stone-400">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{row.quellen?.length ?? 0}</td>
|
||||
<td>
|
||||
<span
|
||||
class="inline-block rounded px-2 py-0.5 text-xs {row.extraktion === 'verbatim'
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
: 'bg-amber-100 text-amber-700'}"
|
||||
>
|
||||
{row.extraktion || '—'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<form method="POST" action="?/accept" use:enhance class="inline">
|
||||
<input type="hidden" name="id" value={row.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-emerald-600 px-2 py-1 text-xs text-white hover:bg-emerald-700"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="?/reject" use:enhance class="inline">
|
||||
<input type="hidden" name="id" value={row.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="ml-1 rounded bg-stone-200 px-2 py-1 text-xs text-stone-700 hover:bg-stone-300"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user