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:
parent
764fdad6db
commit
273a17c0ac
5 changed files with 36 additions and 23 deletions
|
|
@ -25,13 +25,13 @@ export default function DuplicateCheckPanel({
|
||||||
|
|
||||||
{/* File-level duplicate */}
|
{/* File-level duplicate */}
|
||||||
{result.fileAlreadyImported && (
|
{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">
|
<div className="flex items-start gap-3 p-4 rounded-xl bg-[var(--card)] border-2 border-[var(--accent)]">
|
||||||
<FileWarning size={20} className="text-amber-500 shrink-0 mt-0.5" />
|
<FileWarning size={20} className="text-[var(--accent)] shrink-0 mt-0.5" />
|
||||||
<div>
|
<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")}
|
{t("import.duplicates.fileAlreadyImported")}
|
||||||
</p>
|
</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")}
|
{t("import.duplicates.fileAlreadyImportedDesc")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -41,18 +41,18 @@ export default function DuplicateCheckPanel({
|
||||||
{/* Row-level duplicates */}
|
{/* Row-level duplicates */}
|
||||||
{result.duplicateRows.length > 0 ? (
|
{result.duplicateRows.length > 0 ? (
|
||||||
<div>
|
<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
|
<AlertTriangle
|
||||||
size={20}
|
size={20}
|
||||||
className="text-amber-500 shrink-0 mt-0.5"
|
className="text-[var(--accent)] shrink-0 mt-0.5"
|
||||||
/>
|
/>
|
||||||
<div>
|
<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", {
|
{t("import.duplicates.rowsFound", {
|
||||||
count: result.duplicateRows.length,
|
count: result.duplicateRows.length,
|
||||||
})}
|
})}
|
||||||
</p>
|
</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")}
|
{t("import.duplicates.rowsFoundDesc")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,7 +105,7 @@ export default function DuplicateCheckPanel({
|
||||||
{result.duplicateRows.map((row) => (
|
{result.duplicateRows.map((row) => (
|
||||||
<tr
|
<tr
|
||||||
key={row.rowIndex}
|
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)]">
|
<td className="px-3 py-2 text-[var(--muted-foreground)]">
|
||||||
{row.rowIndex + 1}
|
{row.rowIndex + 1}
|
||||||
|
|
@ -125,9 +125,9 @@ export default function DuplicateCheckPanel({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
!result.fileAlreadyImported && (
|
!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">
|
<div className="flex items-center gap-3 p-4 rounded-xl bg-[var(--card)] border-2 border-[var(--positive)]">
|
||||||
<CheckCircle size={20} className="text-emerald-500" />
|
<CheckCircle size={20} className="text-[var(--positive)]" />
|
||||||
<p className="text-sm font-medium text-emerald-800 dark:text-emerald-200">
|
<p className="text-sm font-medium text-[var(--foreground)]">
|
||||||
{t("import.duplicates.noneFound")}
|
{t("import.duplicates.noneFound")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function FilePreviewTable({
|
||||||
{t("import.preview.rowCount", { count: rows.length })}
|
{t("import.preview.rowCount", { count: rows.length })}
|
||||||
</span>
|
</span>
|
||||||
{errorCount > 0 && (
|
{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} />
|
<AlertCircle size={14} />
|
||||||
{t("import.preview.errorCount", { count: errorCount })}
|
{t("import.preview.errorCount", { count: errorCount })}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -67,7 +67,7 @@ export default function FilePreviewTable({
|
||||||
key={row.rowIndex}
|
key={row.rowIndex}
|
||||||
className={
|
className={
|
||||||
row.error
|
row.error
|
||||||
? "bg-red-50 dark:bg-red-950/20"
|
? "bg-[color-mix(in_srgb,var(--negative)_10%,var(--card))]"
|
||||||
: "hover:bg-[var(--muted)]"
|
: "hover:bg-[var(--muted)]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -76,7 +76,7 @@ export default function FilePreviewTable({
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2">
|
<td className="px-3 py-2">
|
||||||
{row.parsed?.date || (
|
{row.parsed?.date || (
|
||||||
<span className="text-red-500 text-xs">
|
<span className="text-[var(--negative)] text-xs">
|
||||||
{row.error || "—"}
|
{row.error || "—"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,19 @@ export default function ImportReportPanel({
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
label: t("import.report.imported"),
|
label: t("import.report.imported"),
|
||||||
value: report.importedCount,
|
value: report.importedCount,
|
||||||
color: "text-emerald-500",
|
color: "text-[var(--positive)]",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
label: t("import.report.skippedDuplicates"),
|
label: t("import.report.skippedDuplicates"),
|
||||||
value: report.skippedDuplicates,
|
value: report.skippedDuplicates,
|
||||||
color: "text-amber-500",
|
color: "text-[var(--accent)]",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
label: t("import.report.errors"),
|
label: t("import.report.errors"),
|
||||||
value: report.errorCount,
|
value: report.errorCount,
|
||||||
color: "text-red-500",
|
color: "text-[var(--negative)]",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Tag,
|
icon: Tag,
|
||||||
|
|
@ -85,7 +85,7 @@ export default function ImportReportPanel({
|
||||||
{/* Errors list */}
|
{/* Errors list */}
|
||||||
{report.errors.length > 0 && (
|
{report.errors.length > 0 && (
|
||||||
<div>
|
<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")}
|
{t("import.report.errorDetails")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="max-h-48 overflow-y-auto rounded-xl border border-[var(--border)]">
|
<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) => (
|
{report.errors.map((err, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td className="px-3 py-2">{err.rowIndex + 1}</td>
|
<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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ export default function ImportPage() {
|
||||||
|
|
||||||
{/* Error banner */}
|
{/* Error banner */}
|
||||||
{state.error && (
|
{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">
|
<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-red-500 shrink-0" />
|
<AlertCircle size={16} className="text-[var(--negative)] shrink-0" />
|
||||||
<p className="text-sm text-red-700 dark:text-red-300">
|
<p className="text-sm text-[var(--foreground)]">
|
||||||
{state.error}
|
{state.error}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,19 @@ export async function createImportedFile(file: {
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}): Promise<number> {
|
}): Promise<number> {
|
||||||
const db = await getDb();
|
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(
|
const result = await db.execute(
|
||||||
`INSERT INTO imported_files (source_id, filename, file_hash, row_count, status, notes)
|
`INSERT INTO imported_files (source_id, filename, file_hash, row_count, status, notes)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue