feat(api): update category fetch requests to include tree structure

This commit is contained in:
Nils-Johan Gynther
2026-04-19 19:38:49 +02:00
parent 43fb31d0b9
commit afe89439c1
3 changed files with 35 additions and 50 deletions
+33 -48
View File
@@ -173,7 +173,7 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
matchSource: 'none',
categorySuggestion: item.categorySuggestion,
productSearch: '',
selectedCategoryId: '',
selectedCategoryId: item.categorySuggestion?.categoryId ?? '',,
};
}),
);
@@ -209,8 +209,8 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
const product = await res.json();
// Sätt kategori: AI-förslag har prioritet, annars manuellt val
const categoryId = row.categorySuggestion?.categoryId ?? (row.selectedCategoryId !== '' ? row.selectedCategoryId : null);
// Sätt kategori: manuellt val har prioritet, annars AI-förslag
const categoryId = row.selectedCategoryId !== '' ? row.selectedCategoryId : row.categorySuggestion?.categoryId ?? null;
if (categoryId) {
const patchRes = await fetch(`/api/admin/update-product/${product.id}`, {
method: 'PATCH',
@@ -263,7 +263,7 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
const product = await createRes.json() as { id: number; name: string; canonicalName: string | null };
// Sätt kategori om vald/föreslagen
const categoryId = row.categorySuggestion?.categoryId ?? (row.selectedCategoryId !== '' ? row.selectedCategoryId : null);
const categoryId = row.selectedCategoryId !== '' ? row.selectedCategoryId : row.categorySuggestion?.categoryId ?? null;
if (categoryId) {
await fetch(`/api/products/${product.id}`, {
method: 'PATCH',
@@ -350,12 +350,18 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
const checkedCount = rows.filter((r) => r.checked && r.selectedProductId !== '').length;
// Bygg en lista av kategorier under "Övrigt" + "Övrigt" självt som fallback
const ovrigtOptions = (() => {
const ovrigt = allCategories.find((c) => c.name === 'Övrigt' && c.parentId === null);
if (!ovrigt) return allCategories.slice(0, 20); // fallback: visa alla
const children = allCategories.filter((c) => c.parentId === ovrigt.id).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
return [ovrigt, ...children];
// Bygg flat lista av alla kategorier med hierarki: förälder → indragna barn
const flatCategoryOptions = (() => {
const roots = allCategories.filter((c) => c.parentId === null).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
const result: { id: number; label: string }[] = [];
for (const root of roots) {
result.push({ id: root.id, label: root.name });
const children = allCategories.filter((c) => c.parentId === root.id).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
for (const child of children) {
result.push({ id: child.id, label: `\u00a0\u00a0↳ ${child.name}` });
}
}
return result;
})();
const sourceLabel = (src: RowState['matchSource']) => {
@@ -512,13 +518,23 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
style={{ width: '100%', padding: '0.3rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.82rem', color: '#555', boxSizing: 'border-box' }}
/>
</div>
{row.categorySuggestion && row.matchSource === 'none' && (
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
<div style={{ fontSize: '0.8rem', color: '#7c3aed', background: '#f5f3ff', border: '1px solid #ddd6fe', borderRadius: '5px', padding: '4px 8px', display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
<span></span>
<span>AI-förslag: <strong>{row.categorySuggestion.path}</strong></span>
{row.categorySuggestion.usedFallback && <span style={{ color: '#b45309' }}>(osäker)</span>}
</div>
{row.matchSource === 'none' && (
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<select
value={row.selectedCategoryId}
onChange={(e) => updateRow(i, { selectedCategoryId: e.target.value === '' ? '' : Number(e.target.value) })}
style={{ fontSize: '0.8rem', padding: '3px 6px', border: '1px solid #d1d5db', borderRadius: '5px', color: '#374151', maxWidth: '260px' }}
>
<option value=""> Välj kategori </option>
{flatCategoryOptions.map((c) => (
<option key={c.id} value={c.id}>{c.label}</option>
))}
</select>
{row.categorySuggestion && (
<span style={{ fontSize: '0.75rem', color: '#7c3aed', background: '#f5f3ff', border: '1px solid #ddd6fe', borderRadius: '5px', padding: '2px 7px', display: 'inline-flex', alignItems: 'center', gap: '0.3rem' }}>
AI: {row.categorySuggestion.path}{row.categorySuggestion.usedFallback && <span style={{ color: '#b45309' }}> (osäker)</span>}
</span>
)}
{isAdmin ? (
<button
onClick={() => handleCreateProduct(i)}
@@ -538,37 +554,6 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
)}
</div>
)}
{row.matchSource === 'none' && !row.categorySuggestion && (
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<select
value={row.selectedCategoryId}
onChange={(e) => updateRow(i, { selectedCategoryId: e.target.value === '' ? '' : Number(e.target.value) })}
style={{ fontSize: '0.8rem', padding: '3px 6px', border: '1px solid #d1d5db', borderRadius: '5px', color: '#374151', maxWidth: '220px' }}
>
<option value=""> Välj kategori (Övrigt) </option>
{ovrigtOptions.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
{isAdmin ? (
<button
onClick={() => handleCreateProduct(i)}
disabled={creatingProduct === i}
style={{ fontSize: '0.8rem', padding: '3px 10px', background: creatingProduct === i ? '#e5e7eb' : '#f9fafb', color: creatingProduct === i ? '#9ca3af' : '#374151', border: '1px solid #d1d5db', borderRadius: '5px', cursor: creatingProduct === i ? 'not-allowed' : 'pointer' }}
>
{creatingProduct === i ? '⏳ Skapar...' : '+ Skapa ny produkt'}
</button>
) : (
<button
onClick={() => handleSuggestProduct(i)}
disabled={creatingProduct === i}
style={{ fontSize: '0.8rem', padding: '3px 10px', background: creatingProduct === i ? '#e5e7eb' : '#fefce8', color: creatingProduct === i ? '#9ca3af' : '#854d0e', border: '1px solid #fde68a', borderRadius: '5px', cursor: creatingProduct === i ? 'not-allowed' : 'pointer' }}
>
{creatingProduct === i ? '⏳ Skickar...' : '+ Föreslå ny vara'}
</button>
)}
</div>
)}
{row.selectedProductId !== '' && row.matchSource !== 'alias' && (
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginTop: '0.5rem', fontSize: '0.82rem', color: '#555', cursor: 'pointer' }}>
<input type="checkbox" checked={row.saveAlias} onChange={(e) => updateRow(i, { saveAlias: e.target.checked })} />