mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:05:25 +02:00
feat: add Business Central CPNP sync and expand history access
- Add bc-proxy and bc-export serverless API handlers - Add businessCentral service with updateCpnpNoInBC and downloadBusinessCentralItemsExcel - Sync cpnpNo to BC on save (CosmeticItemsView and handleSaveAll) - Parallelize handleSaveAll with Promise.all - Fix array index key, a11y click/keyboard handlers, autoFocus - Grant Change History and Pending Validation access to jingying.shi@craze-group.com Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
co-authored by
claude-flow
parent
2961d3f86a
commit
9b5932920e
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { ExcelRow } from '../types';
|
||||
import { useColumns } from '../contexts/ColumnsContext';
|
||||
import { Search, ChevronDown, ChevronUp, X, Save, Check, Loader2 } from 'lucide-react';
|
||||
@@ -6,6 +6,7 @@ import { cn } from '../lib/utils';
|
||||
import { ColumnFilterPopover } from './ColumnFilterPopover';
|
||||
import { usePersistentState } from '../contexts/FilterContext';
|
||||
import { saveRowToSupabase } from '../lib/supabase';
|
||||
import { updateCpnpNoInBC } from '../services/businessCentral';
|
||||
|
||||
const COSMETIC_LINES = ['INKEE', 'BATH FUN', 'TOP FASHION', 'SENSES', 'BODYNESS'];
|
||||
|
||||
@@ -27,6 +28,11 @@ export function CosmeticItemsView({ data, headers, onSaveRow, onCaptureState, ro
|
||||
const [openFilter, setOpenFilter] = useState<number | null>(null);
|
||||
const [editingCpnp, setEditingCpnp] = useState<{ rowIndex: number; value: string } | null>(null);
|
||||
const [savingCpnp, setSavingCpnp] = useState<number | null>(null);
|
||||
const cpnpInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (editingCpnp !== null) cpnpInputRef.current?.focus();
|
||||
}, [editingCpnp]);
|
||||
|
||||
const pageSize = 100;
|
||||
|
||||
@@ -129,16 +135,26 @@ export function CosmeticItemsView({ data, headers, onSaveRow, onCaptureState, ro
|
||||
if (!editingCpnp || editingCpnp.rowIndex !== rowIndex) return;
|
||||
const row = data[rowIndex];
|
||||
const articleNo = String(row[COLUMNS.ARTICLE_NO]);
|
||||
const cpnpValue = editingCpnp.value.trim();
|
||||
const newRow = [...row];
|
||||
newRow[COLUMNS.CPNP_NO] = editingCpnp.value.trim();
|
||||
newRow[COLUMNS.CPNP_NO] = cpnpValue;
|
||||
setSavingCpnp(rowIndex);
|
||||
setEditingCpnp(null);
|
||||
onCaptureState(`Updated CPNP No. for ${articleNo}`);
|
||||
onSaveRow(rowIndex, newRow);
|
||||
const result = await saveRowToSupabase(articleNo, newRow, 'edited');
|
||||
|
||||
const [supabaseResult, bcResult] = await Promise.all([
|
||||
saveRowToSupabase(articleNo, newRow, 'edited'),
|
||||
updateCpnpNoInBC(articleNo, cpnpValue),
|
||||
]);
|
||||
|
||||
setSavingCpnp(null);
|
||||
if (!result.success) {
|
||||
alert(`Error saving CPNP No. for ${articleNo}: ${result.error}`);
|
||||
|
||||
const errors: string[] = [];
|
||||
if (!supabaseResult.success) errors.push(`Supabase: ${supabaseResult.error}`);
|
||||
if (!bcResult.success) errors.push(`Business Central: ${bcResult.error}`);
|
||||
if (errors.length > 0) {
|
||||
alert(`Error saving CPNP No. for ${articleNo}:\n${errors.join('\n')}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -198,7 +214,10 @@ export function CosmeticItemsView({ data, headers, onSaveRow, onCaptureState, ro
|
||||
>
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer select-none hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleSort(col)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handleSort(col); }}
|
||||
>
|
||||
{label}
|
||||
{sortCol === col && (
|
||||
@@ -251,7 +270,7 @@ export function CosmeticItemsView({ data, headers, onSaveRow, onCaptureState, ro
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
key={articleNo || index}
|
||||
className={cn(
|
||||
"hover:bg-slate-700/20 transition-colors",
|
||||
status === 'pending' && "bg-amber-500/5"
|
||||
@@ -283,9 +302,9 @@ export function CosmeticItemsView({ data, headers, onSaveRow, onCaptureState, ro
|
||||
) : isEditing ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
ref={cpnpInputRef}
|
||||
type="text"
|
||||
value={editingCpnp.value}
|
||||
autoFocus
|
||||
onChange={e => setEditingCpnp(prev => prev ? { ...prev, value: e.target.value } : prev)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') saveCpnp(index);
|
||||
|
||||
@@ -9,7 +9,11 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export function Sidebar({ activeModule, setActiveModule, userEmail }: SidebarProps) {
|
||||
const isMasterUser = userEmail?.toLowerCase() === 'christian.vidal@craze-group.com';
|
||||
const MASTER_USERS = new Set([
|
||||
'christian.vidal@craze-group.com',
|
||||
'jingying.shi@craze-group.com',
|
||||
]);
|
||||
const isMasterUser = MASTER_USERS.has(userEmail?.toLowerCase());
|
||||
|
||||
type ModuleId = 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history' | 'missing_data' | 'cosmetic_items';
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Download, LogOut, Undo2, CloudUpload, Loader2, ChevronDown, RotateCcw, Maximize2, X } from 'lucide-react';
|
||||
import { Download, LogOut, Undo2, CloudUpload, Loader2, ChevronDown, RotateCcw, Maximize2, X, CloudDownload } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface TopBarProps {
|
||||
stats: any;
|
||||
activeModule?: string;
|
||||
onExport: () => void;
|
||||
onDownloadBCExcel: () => void;
|
||||
onRefresh?: () => void;
|
||||
hasData: boolean;
|
||||
hasUnsavedChanges: boolean;
|
||||
@@ -20,11 +21,12 @@ interface TopBarProps {
|
||||
onSaveAll: () => Promise<void>;
|
||||
onRevertRow: (articleNo: string) => void;
|
||||
isSavingAll: boolean;
|
||||
isDownloadingBCExcel: boolean;
|
||||
isMaximized: boolean;
|
||||
onToggleMaximize: () => void;
|
||||
}
|
||||
|
||||
export function TopBar({ stats, activeModule, onExport, onRefresh, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, pendingChanges, onSaveAll, onRevertRow, isSavingAll, isMaximized, onToggleMaximize }: TopBarProps) {
|
||||
export function TopBar({ stats, activeModule, onExport, onDownloadBCExcel, onRefresh, hasData, hasUnsavedChanges, userEmail, onSignOut, canUndo, onUndo, undoMessage, undoSteps, pendingCount, pendingChanges, onSaveAll, onRevertRow, isSavingAll, isDownloadingBCExcel, isMaximized, onToggleMaximize }: TopBarProps) {
|
||||
const [showPending, setShowPending] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -170,6 +172,21 @@ export function TopBar({ stats, activeModule, onExport, onRefresh, hasData, hasU
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onDownloadBCExcel}
|
||||
disabled={isDownloadingBCExcel}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors',
|
||||
isDownloadingBCExcel
|
||||
? 'bg-slate-700 text-slate-400 cursor-wait'
|
||||
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'
|
||||
)}
|
||||
title="Download the latest table directly from Business Central"
|
||||
>
|
||||
{isDownloadingBCExcel ? <Loader2 className="w-4 h-4 animate-spin" /> : <CloudDownload className="w-4 h-4" />}
|
||||
Download BC Excel
|
||||
</button>
|
||||
|
||||
{onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
|
||||
Reference in New Issue
Block a user