feat(api): update category fetch requests to include tree structure
This commit is contained in:
@@ -80,7 +80,7 @@ export default function AdminProductList() {
|
|||||||
}, [refetchProducts]);
|
}, [refetchProducts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/categories')
|
fetch('/api/categories?tree')
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => { if (Array.isArray(data)) setCategoryTree(data); })
|
.then((data) => { if (Array.isArray(data)) setCategoryTree(data); })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function EditProductForm({ product, onSaved, onDeleted }: Props)
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && categoryTree.length === 0) {
|
if (isOpen && categoryTree.length === 0) {
|
||||||
fetch('/api/categories')
|
fetch('/api/categories?tree')
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data: unknown) => {
|
.then((data: unknown) => {
|
||||||
if (Array.isArray(data)) setCategoryTree(data as CategoryNode[]);
|
if (Array.isArray(data)) setCategoryTree(data as CategoryNode[]);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
|
|||||||
matchSource: 'none',
|
matchSource: 'none',
|
||||||
categorySuggestion: item.categorySuggestion,
|
categorySuggestion: item.categorySuggestion,
|
||||||
productSearch: '',
|
productSearch: '',
|
||||||
selectedCategoryId: '',
|
selectedCategoryId: item.categorySuggestion?.categoryId ?? '',,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -209,8 +209,8 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
|
|||||||
|
|
||||||
const product = await res.json();
|
const product = await res.json();
|
||||||
|
|
||||||
// Sätt kategori: AI-förslag har prioritet, annars manuellt val
|
// Sätt kategori: manuellt val har prioritet, annars AI-förslag
|
||||||
const categoryId = row.categorySuggestion?.categoryId ?? (row.selectedCategoryId !== '' ? row.selectedCategoryId : null);
|
const categoryId = row.selectedCategoryId !== '' ? row.selectedCategoryId : row.categorySuggestion?.categoryId ?? null;
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
const patchRes = await fetch(`/api/admin/update-product/${product.id}`, {
|
const patchRes = await fetch(`/api/admin/update-product/${product.id}`, {
|
||||||
method: 'PATCH',
|
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 };
|
const product = await createRes.json() as { id: number; name: string; canonicalName: string | null };
|
||||||
|
|
||||||
// Sätt kategori om vald/föreslagen
|
// 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) {
|
if (categoryId) {
|
||||||
await fetch(`/api/products/${product.id}`, {
|
await fetch(`/api/products/${product.id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -350,12 +350,18 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
|
|||||||
|
|
||||||
const checkedCount = rows.filter((r) => r.checked && r.selectedProductId !== '').length;
|
const checkedCount = rows.filter((r) => r.checked && r.selectedProductId !== '').length;
|
||||||
|
|
||||||
// Bygg en lista av kategorier under "Övrigt" + "Övrigt" självt som fallback
|
// Bygg flat lista av alla kategorier med hierarki: förälder → indragna barn
|
||||||
const ovrigtOptions = (() => {
|
const flatCategoryOptions = (() => {
|
||||||
const ovrigt = allCategories.find((c) => c.name === 'Övrigt' && c.parentId === null);
|
const roots = allCategories.filter((c) => c.parentId === null).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
|
||||||
if (!ovrigt) return allCategories.slice(0, 20); // fallback: visa alla
|
const result: { id: number; label: string }[] = [];
|
||||||
const children = allCategories.filter((c) => c.parentId === ovrigt.id).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
|
for (const root of roots) {
|
||||||
return [ovrigt, ...children];
|
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']) => {
|
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' }}
|
style={{ width: '100%', padding: '0.3rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.82rem', color: '#555', boxSizing: 'border-box' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{row.categorySuggestion && row.matchSource === 'none' && (
|
{row.matchSource === 'none' && (
|
||||||
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.5rem', 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' }}>
|
<select
|
||||||
<span>✨</span>
|
value={row.selectedCategoryId}
|
||||||
<span>AI-förslag: <strong>{row.categorySuggestion.path}</strong></span>
|
onChange={(e) => updateRow(i, { selectedCategoryId: e.target.value === '' ? '' : Number(e.target.value) })}
|
||||||
{row.categorySuggestion.usedFallback && <span style={{ color: '#b45309' }}>(osäker)</span>}
|
style={{ fontSize: '0.8rem', padding: '3px 6px', border: '1px solid #d1d5db', borderRadius: '5px', color: '#374151', maxWidth: '260px' }}
|
||||||
</div>
|
>
|
||||||
|
<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 ? (
|
{isAdmin ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCreateProduct(i)}
|
onClick={() => handleCreateProduct(i)}
|
||||||
@@ -538,37 +554,6 @@ export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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' && (
|
{row.selectedProductId !== '' && row.matchSource !== 'alias' && (
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginTop: '0.5rem', fontSize: '0.82rem', color: '#555', cursor: 'pointer' }}>
|
<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 })} />
|
<input type="checkbox" checked={row.saveAlias} onChange={(e) => updateRow(i, { saveAlias: e.target.checked })} />
|
||||||
|
|||||||
Reference in New Issue
Block a user