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 */}
|
||||
{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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue