From 99825af08cf20cce4b99be8d42ededa7283e8cab Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Sun, 29 Mar 2026 18:26:20 +0200 Subject: [PATCH] Implement premium Confirmation Modal for all data-modifying actions --- src/components/ConfirmModal.tsx | 100 ++++++++++++++++++++++++++++++ src/components/DimensionsView.tsx | 68 ++++++++++---------- src/components/EditPanel.tsx | 15 ++++- 3 files changed, 148 insertions(+), 35 deletions(-) create mode 100644 src/components/ConfirmModal.tsx diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx new file mode 100644 index 0000000..71f4eda --- /dev/null +++ b/src/components/ConfirmModal.tsx @@ -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 ( +
+
+
+
+
+ +
+

{title}

+
+ +
+ +
+

+ {message} +

+
+ +
+ + +
+
+
+ ); +} diff --git a/src/components/DimensionsView.tsx b/src/components/DimensionsView.tsx index 1f3446b..4e30724 100644 --- a/src/components/DimensionsView.tsx +++ b/src/components/DimensionsView.tsx @@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react'; import { ExcelRow, COLUMNS } from '../types'; import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Edit2, Package, Boxes, Scale, Loader2, RefreshCw, Layers } from 'lucide-react'; import { cn } from '../lib/utils'; +import { ConfirmModal } from './ConfirmModal'; interface DimensionsViewProps { data: ExcelRow[]; @@ -27,6 +28,11 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [showOnlyInconsistent, setShowOnlyInconsistent] = useState(true); 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 groupMap = new Map(); @@ -98,13 +104,21 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat }; const handleSyncField = async (group: DimensionGroup, sourceRow: ExcelRow, fieldType: 'outer' | 'units' | 'moq') => { - const fieldLabel = fieldType === 'outer' ? 'Outer Box Dimensions' : fieldType === 'units' ? 'Units per Outer' : 'MOQ'; - if (!window.confirm(`Sync ALL products in this group to match "${fieldLabel}" from product ${sourceRow[COLUMNS.ARTICLE_NO]}?`)) { - return; - } + setPendingAction({ group, sourceRow, fieldType }); + }; + + 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}`); setSyncing({ key: group.key, field: fieldType }); + setPendingAction(null); try { const outerL = sourceRow[COLUMNS.OUTER_L]; @@ -125,6 +139,12 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat updatedRow[COLUMNS.UNITS_OUTER] = unitsOuter; } else if (fieldType === '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); @@ -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 (
@@ -363,6 +353,16 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
)}
+ + 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" + /> ); } diff --git a/src/components/EditPanel.tsx b/src/components/EditPanel.tsx index 290a42e..08bfe55 100644 --- a/src/components/EditPanel.tsx +++ b/src/components/EditPanel.tsx @@ -3,6 +3,7 @@ import { ExcelRow, COLUMNS } from '../types'; import { X, Sparkles, Save, Loader2, Languages, Package } from 'lucide-react'; import { generateGemini } from '../services/gemini'; import { cn } from '../lib/utils'; +import { ConfirmModal } from './ConfirmModal'; interface EditPanelProps { row: ExcelRow; @@ -30,6 +31,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed const [loadingField, setLoadingField] = useState(null); const [error, setError] = useState(null); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); const isModified = (field: keyof typeof formData) => { const colMap: Record = { @@ -101,6 +103,7 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed }; const handleSave = () => { + setIsConfirmOpen(false); const hasModifications = Object.keys(formData).some(k => isModified(k as keyof typeof formData)); if (hasModifications) { onCaptureState(`Updated product ${row[COLUMNS.ARTICLE_NO]}`); @@ -251,13 +254,23 @@ export function EditPanel({ row, rowIndex, onSave, onClose, onCaptureState }: Ed Cancel + + 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" + /> );