This commit is contained in:
2025-12-01 16:19:18 -05:00
parent 309d164139
commit e4d25c3ea8
31 changed files with 271 additions and 13 deletions
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8080";
@@ -50,6 +50,29 @@ export default function CategoryMappingsPage() {
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [showOnlyUnmapped, setShowOnlyUnmapped] = useState(false);
const sortedMappings = useMemo(() => {
// Mapped rows first, then unmapped; within each group, alpha by rawCategory
return [...mappings].sort((a, b) => {
const aMapped = !!a.mappedPartRole;
const bMapped = !!b.mappedPartRole;
if (aMapped !== bMapped) {
return aMapped ? -1 : 1; // mapped first
}
return a.rawCategory.localeCompare(b.rawCategory);
});
}, [mappings]);
const visibleMappings = useMemo(
() =>
showOnlyUnmapped
? sortedMappings.filter((m) => !m.mappedPartRole)
: sortedMappings,
[sortedMappings, showOnlyUnmapped],
);
// Load merchants
useEffect(() => {
@@ -232,10 +255,22 @@ export default function CategoryMappingsPage() {
</p>
) : (
<>
<div className="mb-3 text-xs text-zinc-500">
Mappings for <span className="text-zinc-300">{selectedMerchantName}</span>.{" "}
Set a part role for each raw category. Unmapped categories will
be skipped by the importer.
<div className="mb-3 flex items-center justify-between gap-3 text-xs text-zinc-500">
<div>
Mappings for{" "}
<span className="text-zinc-300">{selectedMerchantName}</span>.{" "}
Set a part role for each raw category. Unmapped categories will
be skipped by the importer.
</div>
<label className="inline-flex items-center gap-1 text-[0.7rem] text-zinc-400">
<input
type="checkbox"
className="h-3 w-3 rounded border-zinc-600 bg-zinc-900"
checked={showOnlyUnmapped}
onChange={(e) => setShowOnlyUnmapped(e.target.checked)}
/>
<span>Show only unmapped</span>
</label>
</div>
<div className="overflow-x-auto">
@@ -244,10 +279,11 @@ export default function CategoryMappingsPage() {
<tr className="border-b border-zinc-800 text-xs uppercase tracking-[0.16em] text-zinc-500">
<th className="py-2 text-left pr-4">Raw Category</th>
<th className="py-2 text-left pr-4">Mapped Part Role</th>
<th className="py-2 text-left pr-4">Status</th>
</tr>
</thead>
<tbody>
{mappings.map((m) => (
{visibleMappings.map((m) => (
<tr
key={m.rawCategory}
className="border-b border-zinc-900 last:border-b-0"
@@ -273,6 +309,17 @@ export default function CategoryMappingsPage() {
))}
</select>
</td>
<td className="py-2 pr-4 align-top text-sm">
{m.mappedPartRole ? (
<span className="inline-flex items-center rounded-full bg-emerald-500/10 px-2 py-0.5 text-[0.7rem] font-medium text-emerald-300">
Mapped
</span>
) : (
<span className="inline-flex items-center rounded-full bg-zinc-700/40 px-2 py-0.5 text-[0.7rem] font-medium text-zinc-300">
Unmapped / Ignored
</span>
)}
</td>
</tr>
))}
</tbody>