fix: handle duplicate file hash on re-import and improve alert colors

- importedFileService now upserts: if file hash already exists for a
  source (e.g. from a previous failed import), it updates the existing
  record instead of hitting the UNIQUE constraint.
- Replaced Tailwind amber/red/emerald colors with the app's CSS
  variables (--negative, --positive, --accent) for proper contrast
  on the cream background theme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Le-King-Fu 2026-02-08 22:27:49 +00:00
parent 764fdad6db
commit 273a17c0ac
5 changed files with 36 additions and 23 deletions

View file

@ -25,13 +25,13 @@ export default function DuplicateCheckPanel({
{/* File-level duplicate */}
{result.fileAlreadyImported && (
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800">
<FileWarning size={20} className="text-amber-500 shrink-0 mt-0.5" />
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--card)] border-2 border-[var(--accent)]">
<FileWarning size={20} className="text-[var(--accent)] shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
<p className="text-sm font-medium text-[var(--foreground)]">
{t("import.duplicates.fileAlreadyImported")}
</p>
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
<p className="text-xs text-[var(--muted-foreground)] mt-1">
{t("import.duplicates.fileAlreadyImportedDesc")}
</p>
</div>
@ -41,18 +41,18 @@ export default function DuplicateCheckPanel({
{/* Row-level duplicates */}
{result.duplicateRows.length > 0 ? (
<div>
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800 mb-4">
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--card)] border-2 border-[var(--accent)] mb-4">
<AlertTriangle
size={20}
className="text-amber-500 shrink-0 mt-0.5"
className="text-[var(--accent)] shrink-0 mt-0.5"
/>
<div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
<p className="text-sm font-medium text-[var(--foreground)]">
{t("import.duplicates.rowsFound", {
count: result.duplicateRows.length,
})}
</p>
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1">
<p className="text-xs text-[var(--muted-foreground)] mt-1">
{t("import.duplicates.rowsFoundDesc")}
</p>
</div>
@ -105,7 +105,7 @@ export default function DuplicateCheckPanel({
{result.duplicateRows.map((row) => (
<tr
key={row.rowIndex}
className="bg-amber-50/50 dark:bg-amber-950/10"
className="bg-[var(--muted)]"
>
<td className="px-3 py-2 text-[var(--muted-foreground)]">
{row.rowIndex + 1}
@ -125,9 +125,9 @@ export default function DuplicateCheckPanel({
</div>
) : (
!result.fileAlreadyImported && (
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-50 dark:bg-emerald-950/20 border border-emerald-200 dark:border-emerald-800">
<CheckCircle size={20} className="text-emerald-500" />
<p className="text-sm font-medium text-emerald-800 dark:text-emerald-200">
<div className="flex items-center gap-3 p-4 rounded-xl bg-[var(--card)] border-2 border-[var(--positive)]">
<CheckCircle size={20} className="text-[var(--positive)]" />
<p className="text-sm font-medium text-[var(--foreground)]">
{t("import.duplicates.noneFound")}
</p>
</div>

View file

@ -32,7 +32,7 @@ export default function FilePreviewTable({
{t("import.preview.rowCount", { count: rows.length })}
</span>
{errorCount > 0 && (
<span className="flex items-center gap-1 text-red-500">
<span className="flex items-center gap-1 text-[var(--negative)]">
<AlertCircle size={14} />
{t("import.preview.errorCount", { count: errorCount })}
</span>
@ -67,7 +67,7 @@ export default function FilePreviewTable({
key={row.rowIndex}
className={
row.error
? "bg-red-50 dark:bg-red-950/20"
? "bg-[color-mix(in_srgb,var(--negative)_10%,var(--card))]"
: "hover:bg-[var(--muted)]"
}
>
@ -76,7 +76,7 @@ export default function FilePreviewTable({
</td>
<td className="px-3 py-2">
{row.parsed?.date || (
<span className="text-red-500 text-xs">
<span className="text-[var(--negative)] text-xs">
{row.error || "—"}
</span>
)}

View file

@ -30,19 +30,19 @@ export default function ImportReportPanel({
icon: CheckCircle,
label: t("import.report.imported"),
value: report.importedCount,
color: "text-emerald-500",
color: "text-[var(--positive)]",
},
{
icon: AlertTriangle,
label: t("import.report.skippedDuplicates"),
value: report.skippedDuplicates,
color: "text-amber-500",
color: "text-[var(--accent)]",
},
{
icon: XCircle,
label: t("import.report.errors"),
value: report.errorCount,
color: "text-red-500",
color: "text-[var(--negative)]",
},
{
icon: Tag,
@ -85,7 +85,7 @@ export default function ImportReportPanel({
{/* Errors list */}
{report.errors.length > 0 && (
<div>
<h3 className="text-sm font-semibold mb-2 text-red-500">
<h3 className="text-sm font-semibold mb-2 text-[var(--negative)]">
{t("import.report.errorDetails")}
</h3>
<div className="max-h-48 overflow-y-auto rounded-xl border border-[var(--border)]">
@ -104,7 +104,7 @@ export default function ImportReportPanel({
{report.errors.map((err, i) => (
<tr key={i}>
<td className="px-3 py-2">{err.rowIndex + 1}</td>
<td className="px-3 py-2 text-red-500">{err.message}</td>
<td className="px-3 py-2 text-[var(--negative)]">{err.message}</td>
</tr>
))}
</tbody>

View file

@ -35,9 +35,9 @@ export default function ImportPage() {
{/* Error banner */}
{state.error && (
<div className="mb-4 p-3 rounded-xl bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 flex items-center gap-2">
<AlertCircle size={16} className="text-red-500 shrink-0" />
<p className="text-sm text-red-700 dark:text-red-300">
<div className="mb-4 p-3 rounded-xl bg-[var(--card)] border-2 border-[var(--negative)] flex items-center gap-2">
<AlertCircle size={16} className="text-[var(--negative)] shrink-0" />
<p className="text-sm text-[var(--foreground)]">
{state.error}
</p>
</div>

View file

@ -32,6 +32,19 @@ export async function createImportedFile(file: {
notes?: string;
}): Promise<number> {
const db = await getDb();
// Check if file already exists (e.g. from a previous failed import)
const existing = await db.select<ImportedFile[]>(
"SELECT id FROM imported_files WHERE source_id = $1 AND file_hash = $2",
[file.source_id, file.file_hash]
);
if (existing.length > 0) {
await db.execute(
`UPDATE imported_files SET filename = $1, row_count = $2, status = $3, notes = $4, import_date = CURRENT_TIMESTAMP WHERE id = $5`,
[file.filename, file.row_count, file.status, file.notes || null, existing[0].id]
);
return existing[0].id;
}
const result = await db.execute(
`INSERT INTO imported_files (source_id, filename, file_hash, row_count, status, notes)
VALUES ($1, $2, $3, $4, $5, $6)`,