feat: implement manual user validation and user deletion flow

This commit is contained in:
Christian Vidal Wolf
2026-05-20 13:33:37 +02:00
parent 1d105e19ae
commit 47b9303202
22 changed files with 2613 additions and 93 deletions
+81 -39
View File
@@ -21,7 +21,11 @@ import { UndoToast } from './components/UndoToast';
import { PendingValidationView } from './components/PendingValidationView';
import { MissingDataView } from './components/MissingDataView';
import { CosmeticItemsView } from './components/CosmeticItemsView';
import { ControlDashboardView } from './components/ControlDashboardView';
import { UserManagementView } from './components/UserManagementView';
import { downloadBusinessCentralItemsExcel, previewBusinessCentralSync, applyBusinessCentralSync, isPreviewTokenMismatchError } from './services/businessCentral';
import { ControlTabId, type DashboardDrilldownRequest } from './lib/controlDashboard';
const FORCED_ZERO_STOCK_SKUS = new Set([
'11631VC', '1237VC', '1238VC', '1652VC', '1653VC', '1684VC', '1688VC', '1717VC',
'1718VC', '180VC', '1832VC', '2025VC', '2027VC', '2180VC', '2181VC', '2210VC',
@@ -92,7 +96,7 @@ export default function App() {
hasUnsavedChanges: false,
asinColumnIndex: null
});
const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data' | 'cosmetic_items'>('descriptions');
const [activeModule, setActiveModule] = useState<ControlTabId>('control_dashboard');
const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]);
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
@@ -104,11 +108,59 @@ export default function App() {
const [isSyncingBC, setIsSyncingBC] = useState(false);
const [isDownloadingBCExcel, setIsDownloadingBCExcel] = useState(false);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [dashboardDrilldown, setDashboardDrilldown] = useState<DashboardDrilldownRequest | null>(null);
useEffect(() => {
console.log('[App] session changed:', session ? 'logged in' : 'logged out');
}, [session]);
// Background user validation status check
useEffect(() => {
if (!session) return;
let isMounted = true;
const checkUserStatus = async () => {
try {
const res = await fetch('/api/users-admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
body: JSON.stringify({ action: 'check-status' })
});
if (!isMounted) return;
if (!res.ok) {
console.warn('[App] Validation check failed, logging out.');
handleSignOut();
return;
}
const data = await res.json();
if (!data.validated) {
alert('Tu usuario ya no está autorizado o está pendiente de validación.');
handleSignOut();
}
} catch (err) {
console.error('[App] Background validation check error:', err);
}
};
// Check immediately on mount/session change
checkUserStatus();
// Check periodically every 5 minutes
const interval = setInterval(checkUserStatus, 5 * 60 * 1000);
return () => {
isMounted = false;
clearInterval(interval);
};
}, [session]);
useEffect(() => {
try {
localStorage.setItem(BC_SYNC_QUEUE_STORAGE_KEY, JSON.stringify(bcSyncQueue));
@@ -235,30 +287,7 @@ export default function App() {
}
const resolvedCols = resolveColumnIndices(extendedHeaders);
const editableColumns = new Set([
resolvedCols.CLASSIFICATION,
resolvedCols.LONG_DE, resolvedCols.LONG_EN,
resolvedCols.SHORT_DE, resolvedCols.SHORT_EN,
resolvedCols.DETAILS_DE, resolvedCols.DETAILS_EN,
resolvedCols.INNER_L, resolvedCols.INNER_W, resolvedCols.INNER_H,
resolvedCols.OUTER_L, resolvedCols.OUTER_W, resolvedCols.OUTER_H,
resolvedCols.UNITS_OUTER, resolvedCols.MOQ,
resolvedCols.VERIFIED_DIMS,
resolvedCols.VALIDATED_CHECK,
resolvedCols.VALIDATED_NOTE,
resolvedCols.PRODUCT_TYPE,
resolvedCols.ITEM_TO_LOGISTIC,
resolvedCols.CPNP_NO,
]);
headers.forEach((h: any, i: number) => {
const hl = String(h || '').toLowerCase();
if (hl.includes('srp') || hl.includes('uvp') || hl.includes('40') || hl.includes('price')) {
editableColumns.add(i);
}
});
const articleNoIdx = resolvedCols.ARTICLE_NO;
const articleNoIdx = resolvedCols.ARTICLE_NO;
const processedRows = rows.map(row => {
const articleNo = String(row[articleNoIdx]);
const synced = syncedData[articleNo];
@@ -300,17 +329,6 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
}
});
// 2. For Master Data (indices < 100, like UVP/SRP), only restore if it's an active edit (pending)
// This protects against the "Column Shift" bug where old saved indices might be wrong.
if (synced?.status === 'pending' || synced?.status === 'edited') {
editableColumns.forEach(idx => {
const value = synced?.data?.[idx];
if (idx < 100 && value !== undefined && value !== null) {
finalRow[idx] = value;
}
});
}
// Update row status in UI if it's not the default 'excel'
if (synced?.status && synced.status !== 'excel') {
setRowStatuses(prev => ({ ...prev, [articleNo]: synced.status! }));
@@ -372,7 +390,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
hasUnsavedChanges: false,
asinColumnIndex: asinIdx !== -1 ? asinIdx : null
});
setActiveModule('descriptions');
setActiveModule('control_dashboard');
}
} catch (err) {
console.error('Failed to load from Dropbox:', err);
@@ -465,7 +483,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
hasUnsavedChanges: false,
asinColumnIndex: null
});
setActiveModule('descriptions');
setActiveModule('control_dashboard');
}
};
reader.readAsBinaryString(file);
@@ -694,7 +712,7 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
};
const handleDiscardSelectedBcQueue = () => {
const selectedCount = Object.values(bcSyncQueue).filter(e => e.selected).length;
const selectedCount = (Object.values(bcSyncQueue) as BcSyncQueueEntry[]).filter(e => e.selected).length;
if (selectedCount === 0) return;
if (!window.confirm(`Discard ${selectedCount} pending BC sync(s)? They will be removed from the queue.`)) return;
setBcSyncQueue(prev => {
@@ -1055,6 +1073,21 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
asinColumnIndex={appState.asinColumnIndex}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
dashboardDrilldown={dashboardDrilldown}
onDashboardDrilldownApplied={() => setDashboardDrilldown(null)}
/>
)}
{activeModule === 'control_dashboard' && (
<ControlDashboardView
headers={appState.headers}
data={appState.data}
pendingRows={pendingRows}
rowStatuses={rowStatuses}
activeModule={activeModule}
onDrillDown={(request) => {
setDashboardDrilldown(request);
setActiveModule(request.tabId);
}}
/>
)}
{activeModule === 'matrix' && (
@@ -1081,6 +1114,8 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
onCaptureState={captureState}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
dashboardDrilldown={dashboardDrilldown}
onDashboardDrilldownApplied={() => setDashboardDrilldown(null)}
/>
)}
{activeModule === 'article_details' && (
@@ -1088,6 +1123,8 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
data={appState.data}
onEdit={(index) => setEditingRowIndex(index)}
rowStatuses={rowStatuses}
dashboardDrilldown={dashboardDrilldown}
onDashboardDrilldownApplied={() => setDashboardDrilldown(null)}
/>
)}
{activeModule === 'pending_validation' && (
@@ -1115,6 +1152,8 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
onCaptureState={captureState}
rowStatuses={rowStatuses}
onQueueBcSync={handleQueueBcSync}
dashboardDrilldown={dashboardDrilldown}
onDashboardDrilldownApplied={() => setDashboardDrilldown(null)}
/>
)}
{activeModule === 'history' && (
@@ -1150,6 +1189,9 @@ const articleNoIdx = resolvedCols.ARTICLE_NO;
}}
/>
)}
{activeModule === 'user_management' && session && (
<UserManagementView session={session} />
)}
</>
)}
</main>