Implement premium Confirmation Modal for all data-modifying actions

This commit is contained in:
Christian Vidal Wolf
2026-03-29 18:26:20 +02:00
parent 7a86a8e64b
commit 99825af08c
3 changed files with 148 additions and 35 deletions
+100
View File
@@ -0,0 +1,100 @@
import React, { useEffect, useState } from 'react';
import { X, AlertTriangle, CheckCircle2, Info, Loader2 } from 'lucide-react';
import { cn } from '../lib/utils';
interface ConfirmModalProps {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
type?: 'info' | 'warning' | 'danger';
isLoading?: boolean;
}
export function ConfirmModal({
isOpen,
onConfirm,
onCancel,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
type = 'info',
isLoading = false
}: ConfirmModalProps) {
const [shouldRender, setShouldRender] = useState(isOpen);
useEffect(() => {
if (isOpen) setShouldRender(true);
}, [isOpen]);
const onAnimationEnd = () => {
if (!isOpen) setShouldRender(false);
};
if (!shouldRender) return null;
const Icon = type === 'danger' || type === 'warning' ? AlertTriangle : Info;
const iconColor = type === 'danger' ? 'text-red-500 bg-red-500/10' : type === 'warning' ? 'text-amber-500 bg-amber-500/10' : 'text-blue-500 bg-blue-500/10';
const confirmBtnClass = type === 'danger' ? 'bg-red-600 hover:bg-red-700' : type === 'warning' ? 'bg-amber-600 hover:bg-amber-700' : 'bg-blue-600 hover:bg-blue-700';
return (
<div className={cn(
"fixed inset-0 z-[110] flex items-center justify-center p-4 transition-all duration-300",
isOpen ? "bg-black/80 backdrop-blur-sm opacity-100" : "bg-transparent backdrop-blur-0 opacity-0 pointer-events-none"
)}>
<div
onAnimationEnd={onAnimationEnd}
className={cn(
"bg-slate-900 border border-slate-700 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden transition-all duration-300 transform",
isOpen ? "scale-100 translate-y-0" : "scale-95 translate-y-4"
)}
>
<div className="flex items-center justify-between p-4 border-b border-slate-700/50 bg-slate-800/20">
<div className="flex items-center gap-3">
<div className={cn("p-2 rounded-lg", iconColor)}>
<Icon className="w-5 h-5" />
</div>
<h3 className="text-lg font-semibold text-white uppercase tracking-tight text-xs">{title}</h3>
</div>
<button
onClick={onCancel}
className="p-1.5 text-slate-500 hover:text-white hover:bg-slate-700 rounded-md transition-all"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6">
<p className="text-slate-300 leading-relaxed font-medium">
{message}
</p>
</div>
<div className="flex items-center justify-end gap-3 p-4 bg-slate-800/40 border-t border-slate-700/50">
<button
onClick={onCancel}
disabled={isLoading}
className="px-5 py-2.5 text-slate-400 hover:text-white font-bold text-xs uppercase tracking-widest transition-all disabled:opacity-50"
>
{cancelText}
</button>
<button
onClick={onConfirm}
disabled={isLoading}
className={cn(
"px-6 py-2.5 text-white font-black text-xs uppercase tracking-widest rounded-lg shadow-lg transition-all active:scale-95 flex items-center gap-2 disabled:opacity-50",
confirmBtnClass
)}
>
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <CheckCircle2 className="w-4 h-4" />}
{confirmText}
</button>
</div>
</div>
</div>
);
}
+34 -34
View File
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
import { ExcelRow, COLUMNS } from '../types'; import { ExcelRow, COLUMNS } from '../types';
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers } from 'lucide-react'; import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers } from 'lucide-react';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal';
interface DimensionsViewProps { interface DimensionsViewProps {
data: ExcelRow[]; data: ExcelRow[];
@@ -27,6 +28,11 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
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<{ key: string, field: string } | null>(null); const [syncing, setSyncing] = useState<{ key: string, field: string } | null>(null);
const [pendingAction, setPendingAction] = useState<{
group: DimensionGroup,
sourceRow: ExcelRow,
fieldType: 'outer' | 'units' | 'moq' | 'all'
} | null>(null);
const groups = useMemo(() => { const groups = useMemo(() => {
const groupMap = new Map<string, { row: ExcelRow; index: number }[]>(); const groupMap = new Map<string, { row: ExcelRow; index: number }[]>();
@@ -98,13 +104,21 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
}; };
const handleSyncField = async (group: DimensionGroup, sourceRow: ExcelRow, fieldType: 'outer' | 'units' | 'moq') => { const handleSyncField = async (group: DimensionGroup, sourceRow: ExcelRow, fieldType: 'outer' | 'units' | 'moq') => {
const fieldLabel = fieldType === 'outer' ? 'Outer Box Dimensions' : fieldType === 'units' ? 'Units per Outer' : 'MOQ'; setPendingAction({ group, sourceRow, fieldType });
if (!window.confirm(`Sync ALL products in this group to match "${fieldLabel}" from product ${sourceRow[COLUMNS.ARTICLE_NO]}?`)) { };
return;
} const handleFullSync = async (group: DimensionGroup, sourceRow: ExcelRow) => {
setPendingAction({ group, sourceRow, fieldType: 'all' });
};
const executeSync = async () => {
if (!pendingAction) return;
const { group, sourceRow, fieldType } = pendingAction;
const fieldLabel = fieldType === 'outer' ? 'Outer Box Dimensions' : fieldType === 'units' ? 'Units per Outer' : fieldType === 'moq' ? 'MOQ' : 'Full Packaging Data';
onCaptureState(`Bulk synced ${fieldLabel} in group ${group.innerDims}`); onCaptureState(`Bulk synced ${fieldLabel} in group ${group.innerDims}`);
setSyncing({ key: group.key, field: fieldType }); setSyncing({ key: group.key, field: fieldType });
setPendingAction(null);
try { try {
const outerL = sourceRow[COLUMNS.OUTER_L]; const outerL = sourceRow[COLUMNS.OUTER_L];
@@ -125,6 +139,12 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
updatedRow[COLUMNS.UNITS_OUTER] = unitsOuter; updatedRow[COLUMNS.UNITS_OUTER] = unitsOuter;
} else if (fieldType === 'moq') { } else if (fieldType === 'moq') {
updatedRow[COLUMNS.MOQ] = moq; updatedRow[COLUMNS.MOQ] = moq;
} else if (fieldType === 'all') {
updatedRow[COLUMNS.OUTER_L] = outerL;
updatedRow[COLUMNS.OUTER_W] = outerW;
updatedRow[COLUMNS.OUTER_H] = outerH;
updatedRow[COLUMNS.UNITS_OUTER] = unitsOuter;
updatedRow[COLUMNS.MOQ] = moq;
} }
await onSaveRow(index, updatedRow); await onSaveRow(index, updatedRow);
@@ -134,36 +154,6 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
} }
}; };
const handleFullSync = async (group: DimensionGroup, sourceRow: ExcelRow) => {
if (!window.confirm(`Sync ALL Packaging & MOQ data in this group to match product ${sourceRow[COLUMNS.ARTICLE_NO]}?`)) {
return;
}
onCaptureState(`Full sync of packaging data in group ${group.innerDims}`);
setSyncing({ key: group.key, field: 'all' });
try {
const outerL = sourceRow[COLUMNS.OUTER_L];
const outerW = sourceRow[COLUMNS.OUTER_W];
const outerH = sourceRow[COLUMNS.OUTER_H];
const unitsOuter = sourceRow[COLUMNS.UNITS_OUTER];
const moq = sourceRow[COLUMNS.MOQ];
for (const { row, index } of group.rows) {
if (row === sourceRow) continue;
const updatedRow = [...row];
updatedRow[COLUMNS.OUTER_L] = outerL;
updatedRow[COLUMNS.OUTER_W] = outerW;
updatedRow[COLUMNS.OUTER_H] = outerH;
updatedRow[COLUMNS.UNITS_OUTER] = unitsOuter;
updatedRow[COLUMNS.MOQ] = moq;
await onSaveRow(index, updatedRow);
}
} finally {
setSyncing(null);
}
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between bg-slate-800/50 p-4 rounded-lg border border-slate-700"> <div className="flex items-center justify-between bg-slate-800/50 p-4 rounded-lg border border-slate-700">
@@ -363,6 +353,16 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
</div> </div>
)} )}
</div> </div>
<ConfirmModal
isOpen={!!pendingAction}
onConfirm={executeSync}
onCancel={() => setPendingAction(null)}
title="Sync Group Data"
message={`Are you sure you want to sync ${pendingAction?.fieldType === 'all' ? 'ALL packaging data' : pendingAction?.fieldType} for the whole group using product ${pendingAction?.sourceRow[COLUMNS.ARTICLE_NO]} as the template?`}
type="warning"
confirmText="Sync Group"
/>
</div> </div>
); );
} }
+14 -1
View File
@@ -3,6 +3,7 @@ import { ExcelRow, COLUMNS } from '../types';
import { X, Sparkles, Save, Loader2, Languages, Package } from 'lucide-react'; import { X, Sparkles, Save, Loader2, Languages, Package } from 'lucide-react';
import { generateGemini } from '../services/gemini'; import { generateGemini } from '../services/gemini';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { ConfirmModal } from './ConfirmModal';
interface EditPanelProps { interface EditPanelProps {
row: ExcelRow; row: ExcelRow;
@@ -30,6 +31,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
const [loadingField, setLoadingField] = useState<string | null>(null); const [loadingField, setLoadingField] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const isModified = (field: keyof typeof formData) => { const isModified = (field: keyof typeof formData) => {
const colMap: Record<string, number> = { const colMap: Record<string, number> = {
@@ -101,6 +103,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
}; };
const handleSave = () => { const handleSave = () => {
setIsConfirmOpen(false);
const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData)); const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData));
if (hasModifications) { if (hasModifications) {
onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`); onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`);
@@ -251,13 +254,23 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed
Cancel Cancel
</button> </button>
<button <button
onClick={handleSave} onClick={() => setIsConfirmOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow-lg shadow-blue-900/20 transition-colors" className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md shadow-lg shadow-blue-900/20 transition-colors"
> >
<Save className="w-4 h-4" /> <Save className="w-4 h-4" />
Save to Memory Save to Memory
</button> </button>
</div> </div>
<ConfirmModal
isOpen={isConfirmOpen}
onConfirm={handleSave}
onCancel={() => setIsConfirmOpen(false)}
title="Confirm Changes"
message={`Are you sure you want to save the modifications for product ${row[COLUMNS.ARTICLE_NO]}?`}
type="info"
confirmText="Save changes"
/>
</div> </div>
</> </>
); );