mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 16:55:24 +02:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea4592b695 | ||
|
|
b42dd32cda | ||
|
|
0c152a4742 |
@@ -0,0 +1,54 @@
|
||||
import { applyCors, isAllowedOrigin } from './_cors.js';
|
||||
import { getBcConfig, getBCToken, getItemsUrl } from '../bc-runtime.js';
|
||||
|
||||
function setCors(req, res) {
|
||||
applyCors(req, res, 'GET, OPTIONS');
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
setCors(req, res);
|
||||
|
||||
if (req.method === 'OPTIONS') return res.status(204).end();
|
||||
|
||||
const origin = req.headers.origin;
|
||||
if (origin && !isAllowedOrigin(origin)) {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
try {
|
||||
const config = getBcConfig();
|
||||
const token = await getBCToken(config);
|
||||
const itemsUrl = getItemsUrl(config);
|
||||
|
||||
// Note: $select suppresses @odata.nextLink on this BC API, so page manually with $skip.
|
||||
const TOP = 1000;
|
||||
const values = {};
|
||||
for (let skip = 0; ; skip += TOP) {
|
||||
const response = await fetch(`${itemsUrl}?$select=no,empCOCompliant&$top=${TOP}&$skip=${skip}`, {
|
||||
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const txt = await response.text();
|
||||
throw new Error(`BC GET items failed (${response.status}): ${txt}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
const page = json.value || [];
|
||||
for (const item of page) {
|
||||
if (item.no !== undefined && item.no !== null) {
|
||||
values[String(item.no)] = Boolean(item.empCOCompliant);
|
||||
}
|
||||
}
|
||||
if (page.length < TOP) break;
|
||||
}
|
||||
|
||||
res.setHeader('Cache-Control', 's-maxage=300, stale-while-revalidate=600');
|
||||
return res.json({ success: true, count: Object.keys(values).length, values });
|
||||
} catch (err) {
|
||||
console.error('[bc-empco] error:', err.message);
|
||||
return res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,24 @@ function findCol(headers: string[], ...keywords: string[]): number {
|
||||
);
|
||||
}
|
||||
|
||||
// EMCO Compliant is maintained manually in Business Central, so it is fetched
|
||||
// live from BC (via /api/bc-empco) instead of the Dropbox matrix file.
|
||||
// Module-level cache: one fetch per page load, shared across tab switches.
|
||||
let empcoFetchPromise: Promise<Record<string, boolean>> | null = null;
|
||||
function fetchEmpcoMap(): Promise<Record<string, boolean>> {
|
||||
if (!empcoFetchPromise) {
|
||||
empcoFetchPromise = fetch('/api/bc-empco')
|
||||
.then(res => (res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))))
|
||||
.then(json => (json && json.values && typeof json.values === 'object' ? json.values : {}))
|
||||
.catch(err => {
|
||||
console.warn('[bc-empco] fetch failed:', err);
|
||||
empcoFetchPromise = null; // allow retry on next mount
|
||||
return {};
|
||||
});
|
||||
}
|
||||
return empcoFetchPromise;
|
||||
}
|
||||
|
||||
export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit, rowStatuses, dashboardDrilldown, onDashboardDrilldownApplied }: PricingViewProps) {
|
||||
const COLUMNS = useColumns();
|
||||
const [filterMode, setFilterMode] = usePersistentState<FilterMode>('pricing-filterMode', 'all_errors');
|
||||
@@ -69,7 +87,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
articleName: 220,
|
||||
line: 100,
|
||||
classification: 130,
|
||||
productType: 140,
|
||||
productType: 200,
|
||||
empco: 150,
|
||||
itemToLogistic: 160,
|
||||
unitsOuter: 100,
|
||||
outerW: 80,
|
||||
@@ -244,6 +263,14 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
}, [resizingColumn, resizeStartX, resizeStartWidth]);
|
||||
|
||||
// ── Dynamic column detection ──────────────────────────────────────────────
|
||||
// EMCO Compliant values live from Business Central (null = still loading)
|
||||
const [empcoMap, setEmpcoMap] = useState<Record<string, boolean> | null>(null);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
fetchEmpcoMap().then(map => { if (alive) setEmpcoMap(map); });
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
|
||||
const { uvpIdx, srpCols, containerCols, nwIdx, gwIdx, unitsOuterIdx, units40fIdx } = useMemo(() => {
|
||||
const uvpIdx = findCol(headers, 'uvp');
|
||||
|
||||
@@ -604,6 +631,18 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
if (sortConfig.key === null || sortConfig.direction === null) return filteredRows;
|
||||
|
||||
const { key, direction } = sortConfig;
|
||||
|
||||
// EMCO Compliant sorts by the live BC map (not a row column)
|
||||
if (key === 'empco') {
|
||||
const rank = (r: ExcelRow) => {
|
||||
const v = empcoMap?.[String(r[COLUMNS.ARTICLE_NO] ?? '')];
|
||||
return v === true ? 2 : v === false ? 1 : 0;
|
||||
};
|
||||
return [...filteredRows].sort((a, b) =>
|
||||
direction === 'asc' ? rank(a.row) - rank(b.row) : rank(b.row) - rank(a.row)
|
||||
);
|
||||
}
|
||||
|
||||
let colIndex: number;
|
||||
|
||||
if (typeof key === 'number') {
|
||||
@@ -655,7 +694,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}, [filteredRows, sortConfig]);
|
||||
}, [filteredRows, sortConfig, empcoMap, COLUMNS]);
|
||||
|
||||
// ── Pagination ─────────────────────────────────────────────────────────
|
||||
const paginatedRows = useMemo(() => {
|
||||
@@ -761,6 +800,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
{ key: 'line', label: 'Line', index: COLUMNS.LINE },
|
||||
{ key: 'classification', label: 'Classification', index: COLUMNS.CLASSIFICATION },
|
||||
{ key: 'productType', label: 'CategorizationCode', index: COLUMNS.CATEGORIZATION_CODE },
|
||||
{ key: 'empco', label: 'EMCO Compliant', index: -1 },
|
||||
{ key: 'itemToLogistic', label: 'Item to Logistic', index: COLUMNS.ITEM_TO_LOGISTIC },
|
||||
{ key: 'unitsOuter', label: 'Units/Outer', index: unitsOuterIdx },
|
||||
{ key: 'outerW', label: 'Outer W', index: COLUMNS.OUTER_W },
|
||||
@@ -791,7 +831,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
|
||||
// Calculate sticky left position for each pinned column
|
||||
const getStickyLeft = useMemo(() => {
|
||||
const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq'];
|
||||
const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'empco', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq'];
|
||||
const dynamicOrder: string[] = [];
|
||||
pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`));
|
||||
containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`));
|
||||
@@ -816,7 +856,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
|
||||
// Calculate z-index for sticky columns (so they stack correctly)
|
||||
const getStickyRank = useMemo(() => {
|
||||
const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq'];
|
||||
const baseOrder = ['articleNo', 'articleName', 'line', 'classification', 'productType', 'empco', 'itemToLogistic', 'unitsOuter', 'outerW', 'outerL', 'outerH', 'moq'];
|
||||
const dynamicOrder: string[] = [];
|
||||
pricingEditableCols.forEach(col => dynamicOrder.push(`prc_${col.index}`));
|
||||
containerCols.forEach(col => dynamicOrder.push(`con_${col.index}`));
|
||||
@@ -1545,6 +1585,17 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'productType', columnWidths.productType); }} />
|
||||
</th>
|
||||
|
||||
<th style={{ width: columnWidths.empco, ...(isPinned('empco') ? { left: getStickyLeft('empco') ?? 0, zIndex: getHeaderStickyRank('empco') ?? 0 } : {}) }} className={cn(
|
||||
"text-left px-3 py-3 text-xs font-semibold text-teal-400 uppercase tracking-wider whitespace-nowrap group cursor-pointer hover:bg-slate-800/50 transition-colors relative",
|
||||
isPinned('empco') && "sticky bg-slate-900"
|
||||
)} onClick={() => handleSort('empco')}>
|
||||
<span className="flex items-center gap-1">
|
||||
{isPinned('empco') && <Pin className="w-3 h-3 text-blue-400 shrink-0" />}
|
||||
EMCO Compliant <SortIcon current={sortConfig.key === 'empco' ? sortConfig.direction : null} />
|
||||
</span>
|
||||
<ResizeHandle onMouseDown={e => { e.stopPropagation(); handleResizeStart(e, 'empco', columnWidths.empco); }} />
|
||||
</th>
|
||||
|
||||
<th style={{
|
||||
width: columnWidths.itemToLogistic,
|
||||
...(isPinned('itemToLogistic')
|
||||
@@ -2111,6 +2162,23 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* EMCO Compliant cell (read-only, live from Business Central) */}
|
||||
<td className={cn("px-3 py-2 relative", isPinned('empco') && "sticky bg-slate-800")} style={isPinned('empco') ? { left: getStickyLeft('empco') ?? 0, zIndex: getStickyRank('empco') ?? 0 } : {}}>
|
||||
{(() => {
|
||||
if (empcoMap === null) {
|
||||
return <span className="text-xs text-slate-600 animate-pulse">…</span>;
|
||||
}
|
||||
const value = empcoMap[String(row[COLUMNS.ARTICLE_NO] ?? '')];
|
||||
if (value === true) {
|
||||
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-teal-500/10 text-teal-300 border border-teal-500/30">Yes</span>;
|
||||
}
|
||||
if (value === false) {
|
||||
return <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-500/10 text-red-300 border border-red-500/30">No</span>;
|
||||
}
|
||||
return <span className="text-xs text-slate-500">-</span>;
|
||||
})()}
|
||||
</td>
|
||||
|
||||
{/* ITEM TO LOGISTIC cell */}
|
||||
<td className={cn("px-3 py-2 overflow-visible relative", isPinned('itemToLogistic') && "sticky bg-slate-800")} style={isPinned('itemToLogistic') ? { left: getStickyLeft('itemToLogistic') ?? 0, zIndex: getStickyRank('itemToLogistic') ?? 0 } : {}}>
|
||||
{editingLogistic?.rowIndex === dataIndex ? (
|
||||
|
||||
Reference in New Issue
Block a user