mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 15:35:23 +02:00
feat: revert individual pending changes from TopBar dropdown
- pendingRows now stores originalData + newData per article so changes can be individually reverted to their pre-edit state - TopBar Save button becomes a split button: left saves all, right opens a dropdown listing every pending change with article no + name - Each row in the dropdown has a Revert button that restores the original data locally and removes it from the pending queue - Dropdown closes automatically when all changes are saved/reverted or when clicking outside Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
acc1d06269
commit
a7d8dad36e
+84
-17
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Download, LogOut, Undo2, CloudUpload, Loader2 } from 'lucide-react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Download, LogOut, Undo2, CloudUpload, Loader2, ChevronDown, RotateCcw } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface TopBarProps {
|
||||
@@ -14,17 +14,38 @@ interface TopBarProps {
|
||||
undoMessage?: string;
|
||||
undoSteps: number;
|
||||
pendingCount: number;
|
||||
pendingChanges: Record<string, { articleName: string }>;
|
||||
onSaveAll: () => Promise<void>;
|
||||
onRevertRow: (articleNo: string) => void;
|
||||
isSavingAll: boolean;
|
||||
}
|
||||
|
||||
export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, onSaveAll, isSavingAll }: TopBarProps) {
|
||||
export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, pendingChanges, onSaveAll, onRevertRow, isSavingAll }: TopBarProps) {
|
||||
const [showPending, setShowPending] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPending) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setShowPending(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [showPending]);
|
||||
|
||||
// Close dropdown when all changes are saved/reverted
|
||||
useEffect(() => {
|
||||
if (pendingCount === 0) setShowPending(false);
|
||||
}, [pendingCount]);
|
||||
|
||||
return (
|
||||
<header className="bg-[#020812] border-b border-slate-700/30 h-32 flex items-center justify-between px-10 shrink-0 z-10 shadow-2xl">
|
||||
<div className="flex items-center -ml-4">
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Craze Scan"
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Craze Scan"
|
||||
className="h-24 w-auto object-contain filter brightness-110 contrast-125 hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
</div>
|
||||
@@ -60,15 +81,61 @@ export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail,
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{pendingCount > 0 && (
|
||||
<button
|
||||
onClick={onSaveAll}
|
||||
disabled={isSavingAll}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-md text-sm font-bold transition-all bg-green-600 hover:bg-green-500 disabled:opacity-60 text-white shadow-lg shadow-green-900/30"
|
||||
>
|
||||
{isSavingAll ? <Loader2 className="w-4 h-4 animate-spin" /> : <CloudUpload className="w-4 h-4" />}
|
||||
{isSavingAll ? 'Saving...' : `Save ${pendingCount} change${pendingCount > 1 ? 's' : ''}`}
|
||||
</button>
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{/* Split button: Save All + dropdown toggle */}
|
||||
<div className="flex items-center rounded-md overflow-hidden shadow-lg shadow-green-900/30">
|
||||
<button
|
||||
onClick={onSaveAll}
|
||||
disabled={isSavingAll}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-bold transition-all bg-green-600 hover:bg-green-500 disabled:opacity-60 text-white"
|
||||
>
|
||||
{isSavingAll ? <Loader2 className="w-4 h-4 animate-spin" /> : <CloudUpload className="w-4 h-4" />}
|
||||
{isSavingAll ? 'Saving...' : `Save ${pendingCount} change${pendingCount > 1 ? 's' : ''}`}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowPending(v => !v)}
|
||||
disabled={isSavingAll}
|
||||
className="flex items-center px-2 py-2 bg-green-700 hover:bg-green-600 disabled:opacity-60 text-white border-l border-green-500/40 transition-all"
|
||||
title="View pending changes"
|
||||
>
|
||||
<ChevronDown className={cn("w-4 h-4 transition-transform", showPending && "rotate-180")} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Dropdown: list of pending changes */}
|
||||
{showPending && (
|
||||
<div className="absolute right-0 top-full mt-2 w-80 bg-slate-800 border border-slate-700 rounded-lg shadow-2xl z-50 overflow-hidden">
|
||||
<div className="px-3 py-2 border-b border-slate-700 flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider">Pending changes</span>
|
||||
<span className="text-xs text-slate-500">{pendingCount} unsaved</span>
|
||||
</div>
|
||||
<div className="max-h-64 overflow-y-auto">
|
||||
{Object.entries(pendingChanges).map(([articleNo, { articleName }]) => (
|
||||
<div
|
||||
key={articleNo}
|
||||
className="flex items-center gap-2 px-3 py-2.5 hover:bg-slate-700/50 border-b border-slate-700/50 last:border-0"
|
||||
>
|
||||
<div className="w-2 h-2 rounded-full bg-yellow-400 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-mono text-slate-400">{articleNo}</p>
|
||||
<p className="text-sm text-white truncate">{articleName}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onRevertRow(articleNo)}
|
||||
title="Discard this change"
|
||||
className="shrink-0 flex items-center gap-1 px-2 py-1 text-xs text-red-400 hover:text-white hover:bg-red-600 rounded transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
Revert
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasData && (
|
||||
<button
|
||||
onClick={onExport}
|
||||
@@ -83,15 +150,15 @@ export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail,
|
||||
{hasUnsavedChanges && <span className="w-2 h-2 rounded-full bg-red-500 ml-1 animate-pulse" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
<button
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
title={canUndo ? `Undo: ${undoMessage}` : 'No changes to undo'}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-md text-sm font-bold transition-all shadow-lg relative group",
|
||||
canUndo
|
||||
? "bg-amber-600 hover:bg-amber-700 text-white shadow-amber-900/40 cursor-pointer"
|
||||
canUndo
|
||||
? "bg-amber-600 hover:bg-amber-700 text-white shadow-amber-900/40 cursor-pointer"
|
||||
: "bg-slate-800 text-slate-600 shadow-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user