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:
Christian Vidal Wolf
2026-05-14 17:17:10 +02:00
co-authored by claude-flow
parent 2961d3f86a
commit 9b5932920e
11 changed files with 585 additions and 19 deletions
+26 -7
View File
@@ -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);