mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 12:25:23 +02:00
Implement Undo system for AI and bulk actions
This commit is contained in:
@@ -6,3 +6,5 @@ coverage/
|
|||||||
*.log
|
*.log
|
||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
.vercel
|
||||||
|
.env*.local
|
||||||
|
|||||||
+27
@@ -10,6 +10,7 @@ import { getAllSyncedRows, saveRowToSupabase } from './lib/supabase';
|
|||||||
import { getStoredSession, signOut, type AuthSession } from './lib/auth';
|
import { getStoredSession, signOut, type AuthSession } from './lib/auth';
|
||||||
import { LoginPage } from './components/LoginPage';
|
import { LoginPage } from './components/LoginPage';
|
||||||
import { DimensionsView } from './components/DimensionsView';
|
import { DimensionsView } from './components/DimensionsView';
|
||||||
|
import { UndoToast } from './components/UndoToast';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
|
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
|
||||||
@@ -31,6 +32,7 @@ export default function App() {
|
|||||||
hasUnsavedChanges: false
|
hasUnsavedChanges: false
|
||||||
});
|
});
|
||||||
const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix' | 'dimensions'>('descriptions');
|
const [activeModule, setActiveModule] = useState<'descriptions' | 'matrix' | 'dimensions'>('descriptions');
|
||||||
|
const [undoState, setUndoState] = useState<{ data: ExcelRow[], message: string } | null>(null);
|
||||||
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
|
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
|
||||||
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
|
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
|
||||||
const [defaultLoadError, setDefaultLoadError] = useState<string | null>(null);
|
const [defaultLoadError, setDefaultLoadError] = useState<string | null>(null);
|
||||||
@@ -245,6 +247,23 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const captureState = (message: string) => {
|
||||||
|
setUndoState({
|
||||||
|
data: JSON.parse(JSON.stringify(appState.data)), // Deep copy
|
||||||
|
message
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUndo = () => {
|
||||||
|
if (!undoState) return;
|
||||||
|
setAppState(prev => ({
|
||||||
|
...prev,
|
||||||
|
data: undoState.data,
|
||||||
|
hasUnsavedChanges: true
|
||||||
|
}));
|
||||||
|
setUndoState(null);
|
||||||
|
};
|
||||||
|
|
||||||
const stats = useMemo(() => {
|
const stats = useMemo(() => {
|
||||||
if (appState.data.length === 0) return null;
|
if (appState.data.length === 0) return null;
|
||||||
let missingDeLong = 0;
|
let missingDeLong = 0;
|
||||||
@@ -329,6 +348,7 @@ export default function App() {
|
|||||||
headers={appState.headers}
|
headers={appState.headers}
|
||||||
onEdit={(index) => setEditingRowIndex(index)}
|
onEdit={(index) => setEditingRowIndex(index)}
|
||||||
onSaveRow={handleSaveRow}
|
onSaveRow={handleSaveRow}
|
||||||
|
onCaptureState={captureState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -336,12 +356,19 @@ export default function App() {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UndoToast
|
||||||
|
undoState={undoState}
|
||||||
|
onUndo={handleUndo}
|
||||||
|
onClose={() => setUndoState(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
{editingRowIndex !== null && (
|
{editingRowIndex !== null && (
|
||||||
<EditPanel
|
<EditPanel
|
||||||
row={appState.data[editingRowIndex]}
|
row={appState.data[editingRowIndex]}
|
||||||
rowIndex={editingRowIndex}
|
rowIndex={editingRowIndex}
|
||||||
onSave={handleSaveRow}
|
onSave={handleSaveRow}
|
||||||
onClose={() => setEditingRowIndex(null)}
|
onClose={() => setEditingRowIndex(null)}
|
||||||
|
onCaptureState={captureState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface DimensionsViewProps {
|
|||||||
headers: string[];
|
headers: string[];
|
||||||
onEdit: (index: number) => void;
|
onEdit: (index: number) => void;
|
||||||
onSaveRow: (index: number, updatedRow: ExcelRow) => void;
|
onSaveRow: (index: number, updatedRow: ExcelRow) => void;
|
||||||
|
onCaptureState: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DimensionGroup {
|
interface DimensionGroup {
|
||||||
@@ -22,7 +23,7 @@ interface DimensionGroup {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DimensionsView({ data, headers, onEdit, onSaveRow }: DimensionsViewProps) {
|
export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureState }: DimensionsViewProps) {
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||||
const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true);
|
const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true);
|
||||||
const [syncing, setSyncing] = useState<string | null>(null);
|
const [syncing, setSyncing] = useState<string | null>(null);
|
||||||
@@ -100,6 +101,7 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow }: DimensionsV
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCaptureState(`Synced ${group.rows.length} products in ${group.innerDims} group`);
|
||||||
setSyncing(group.key);
|
setSyncing(group.key);
|
||||||
try {
|
try {
|
||||||
const first = group.rows[0].row;
|
const first = group.rows[0].row;
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ interface EditPanelProps {
|
|||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
onSave: (rowIndex: number, updatedRow: ExcelRow) => void;
|
onSave: (rowIndex: number, updatedRow: ExcelRow) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onCaptureState: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) {
|
export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: EditPanelProps) {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
longDe: row[COLUMNS.LONG_DE] || '',
|
longDe: row[COLUMNS.LONG_DE] || '',
|
||||||
longEn: row[COLUMNS.LONG_EN] || '',
|
longEn: row[COLUMNS.LONG_EN] || '',
|
||||||
@@ -100,6 +101,10 @@ export function EditPanel({ row, rowIndex, onSave, onClose }: EditPanelProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
|
const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData));
|
||||||
|
if (hasModifications) {
|
||||||
|
onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`);
|
||||||
|
}
|
||||||
const newRow = [...row];
|
const newRow = [...row];
|
||||||
newRow[COLUMNS.LONG_DE] = formData.longDe;
|
newRow[COLUMNS.LONG_DE] = formData.longDe;
|
||||||
newRow[COLUMNS.LONG_EN] = formData.longEn;
|
newRow[COLUMNS.LONG_EN] = formData.longEn;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Undo2, X, AlertCircle } from 'lucide-react';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
import { ExcelRow } from '../types';
|
||||||
|
|
||||||
|
interface UndoToastProps {
|
||||||
|
undoState: { data: ExcelRow[], message: string } | null;
|
||||||
|
onUndo: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UndoToast({ undoState, onUndo, onClose }: UndoToastProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (undoState) {
|
||||||
|
setIsVisible(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 300); // Wait for transition
|
||||||
|
}, 8000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
}, [undoState, onClose]);
|
||||||
|
|
||||||
|
if (!undoState && !isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
"fixed bottom-8 left-1/2 -translate-x-1/2 z-[100] transition-all duration-300 transform",
|
||||||
|
isVisible ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0 pointer-events-none"
|
||||||
|
)}>
|
||||||
|
<div className="bg-slate-900 border border-slate-700 rounded-lg shadow-2xl p-1 pr-2 flex items-center gap-4 min-w-[320px]">
|
||||||
|
<div className="bg-blue-600/10 p-3 rounded-l-md border-r border-slate-700/50">
|
||||||
|
<AlertCircle className="w-5 h-5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 py-2">
|
||||||
|
<div className="text-xs text-slate-500 uppercase font-bold tracking-widest">Action Completed</div>
|
||||||
|
<div className="text-sm text-white font-medium">{undoState?.message}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 pr-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onUndo();
|
||||||
|
setIsVisible(false);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold rounded transition-colors"
|
||||||
|
>
|
||||||
|
<Undo2 className="w-3.5 h-3.5" />
|
||||||
|
UNDO
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsVisible(false)}
|
||||||
|
className="p-1.5 text-slate-500 hover:text-white rounded transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user