Files
recipe-app/frontend/app/admin/products/DeletedProductsView.tsx
T

181 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useCallback } from 'react';
type DeletedProduct = {
id: number;
name: string;
normalizedName: string;
canonicalName?: string | null;
category?: string | null;
brand?: string | null;
deletedAt?: string | null;
};
export default function DeletedProductsView() {
const [products, setProducts] = useState<DeletedProduct[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pendingId, setPendingId] = useState<number | null>(null);
const [search, setSearch] = useState('');
const fetchDeleted = useCallback(() => {
setLoading(true);
setError(null);
fetch('/api/admin/deleted-products')
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data)) setProducts(data);
else setError('Kunde inte ladda raderade produkter.');
})
.catch(() => setError('Nätverksfel. Försök igen.'))
.finally(() => setLoading(false));
}, []);
useEffect(() => { fetchDeleted(); }, [fetchDeleted]);
const handleRestore = async (id: number) => {
if (!confirm('Återställ produkten?')) return;
setPendingId(id);
try {
const res = await fetch(`/api/admin/deleted-products/${id}`, { method: 'POST' });
if (!res.ok) {
const d = await res.json().catch(() => ({}));
setError(d?.error ?? 'Kunde inte återställa produkten.');
} else {
setProducts((prev) => prev.filter((p) => p.id !== id));
}
} catch {
setError('Nätverksfel. Försök igen.');
} finally {
setPendingId(null);
}
};
const handlePermanentDelete = async (id: number, name: string) => {
if (!confirm(`Radera "${name}" permanent? Detta kan inte ångras.`)) return;
setPendingId(id);
try {
const res = await fetch(`/api/admin/deleted-products/${id}`, { method: 'DELETE' });
if (!res.ok) {
const d = await res.json().catch(() => ({}));
setError(d?.error ?? 'Kunde inte radera produkten permanent.');
} else {
setProducts((prev) => prev.filter((p) => p.id !== id));
}
} catch {
setError('Nätverksfel. Försök igen.');
} finally {
setPendingId(null);
}
};
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(search.toLowerCase()) ||
(p.canonicalName ?? '').toLowerCase().includes(search.toLowerCase())
);
if (loading) return <p style={{ color: '#888' }}>Laddar raderade produkter...</p>;
return (
<div>
<p style={{ color: '#555', marginBottom: '1rem' }}>
Här visas produkter som mjukraderats. Du kan återställa dem eller radera dem permanent.
</p>
{error && (
<div style={{ color: '#c00', background: '#fff0f0', border: '1px solid #fcc', borderRadius: 6, padding: '0.5rem 1rem', marginBottom: '1rem' }}>
{error}
</div>
)}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem', alignItems: 'center' }}>
<input
type="text"
placeholder="Sök på namn..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ padding: '0.4rem 0.75rem', borderRadius: 6, border: '1px solid #ccc', fontSize: '0.9rem', minWidth: 220 }}
/>
<span style={{ color: '#888', fontSize: '0.85rem' }}>
{filtered.length} av {products.length} produkter
</span>
<button
onClick={fetchDeleted}
style={{ marginLeft: 'auto', padding: '0.35rem 0.75rem', borderRadius: 6, border: '1px solid #ccc', background: '#f8f8f8', cursor: 'pointer', fontSize: '0.85rem' }}
>
Uppdatera
</button>
</div>
{filtered.length === 0 ? (
<p style={{ color: '#888', fontStyle: 'italic' }}>Inga raderade produkter hittades.</p>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.9rem' }}>
<thead>
<tr style={{ background: '#f2f2f2', textAlign: 'left' }}>
<th style={{ padding: '0.5rem 0.75rem', borderBottom: '2px solid #ddd' }}>ID</th>
<th style={{ padding: '0.5rem 0.75rem', borderBottom: '2px solid #ddd' }}>Namn</th>
<th style={{ padding: '0.5rem 0.75rem', borderBottom: '2px solid #ddd' }}>Canonical name</th>
<th style={{ padding: '0.5rem 0.75rem', borderBottom: '2px solid #ddd' }}>Kategori</th>
<th style={{ padding: '0.5rem 0.75rem', borderBottom: '2px solid #ddd' }}>Raderades</th>
<th style={{ padding: '0.5rem 0.75rem', borderBottom: '2px solid #ddd' }}>Åtgärder</th>
</tr>
</thead>
<tbody>
{filtered.map((p) => (
<tr key={p.id} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '0.5rem 0.75rem', color: '#888' }}>{p.id}</td>
<td style={{ padding: '0.5rem 0.75rem', fontWeight: 500 }}>{p.name}</td>
<td style={{ padding: '0.5rem 0.75rem', color: '#666' }}>{p.canonicalName || ''}</td>
<td style={{ padding: '0.5rem 0.75rem', color: '#666' }}>{p.category || ''}</td>
<td style={{ padding: '0.5rem 0.75rem', color: '#999', fontSize: '0.8rem' }}>
{p.deletedAt ? new Date(p.deletedAt).toLocaleDateString('sv-SE') : ''}
</td>
<td style={{ padding: '0.5rem 0.75rem' }}>
<div style={{ display: 'flex', gap: '0.4rem' }}>
<button
disabled={pendingId === p.id}
onClick={() => handleRestore(p.id)}
style={{
padding: '0.25rem 0.6rem',
borderRadius: 5,
border: '1px solid #3a7d44',
background: '#eafaf1',
color: '#276032',
cursor: pendingId === p.id ? 'not-allowed' : 'pointer',
fontSize: '0.8rem',
fontWeight: 500,
}}
>
Återställ
</button>
<button
disabled={pendingId === p.id}
onClick={() => handlePermanentDelete(p.id, p.name)}
style={{
padding: '0.25rem 0.6rem',
borderRadius: 5,
border: '1px solid #c00',
background: '#fff0f0',
color: '#c00',
cursor: pendingId === p.id ? 'not-allowed' : 'pointer',
fontSize: '0.8rem',
fontWeight: 500,
}}
>
Radera permanent
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}