mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:15:24 +02:00
fix: commit all BC sync files missing from repo
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const BC_PROXY_URL = '/api/bc-proxy';
|
||||
const BC_SYNC_PREVIEW_URL = '/api/bc-sync-preview';
|
||||
const BC_SYNC_APPLY_URL = '/api/bc-sync-apply';
|
||||
const BC_EXPORT_URL = '/api/bc-export';
|
||||
|
||||
export interface BCUpdateResult {
|
||||
@@ -6,12 +7,60 @@ export interface BCUpdateResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function isPreviewTokenMismatchError(error?: string): boolean {
|
||||
return Boolean(error && error.toLowerCase().includes('preview token mismatch'));
|
||||
}
|
||||
|
||||
export interface BCDownloadResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface BCFieldChange {
|
||||
sourceLabel: string;
|
||||
sourceIndex: number | null;
|
||||
targetField: string;
|
||||
before: any;
|
||||
after: any;
|
||||
changed: boolean;
|
||||
}
|
||||
|
||||
export interface BCSyncPreviewSection {
|
||||
type: 'items' | 'itemUnitsOfMeasure';
|
||||
desired: Record<string, any>;
|
||||
current: Record<string, any> | null;
|
||||
changes: BCFieldChange[];
|
||||
changedFields: string[];
|
||||
writeConfigured: boolean;
|
||||
writeMethod: string | null;
|
||||
writeUrlTemplate: string | null;
|
||||
writeBodyTemplate: string | null;
|
||||
canApply: boolean;
|
||||
}
|
||||
|
||||
export interface BCSyncPreviewResult {
|
||||
success: boolean;
|
||||
articleNo: string;
|
||||
items: BCSyncPreviewSection;
|
||||
itemUnitsOfMeasure: BCSyncPreviewSection;
|
||||
hasChanges: boolean;
|
||||
previewToken: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface BCSyncApplyResult {
|
||||
success: boolean;
|
||||
articleNo: string;
|
||||
previewToken?: string;
|
||||
preview?: BCSyncPreviewResult;
|
||||
results?: {
|
||||
items: { applied: boolean; reason?: string; url?: string };
|
||||
itemUnitsOfMeasure: { applied: boolean; reason?: string; url?: string };
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function readResponsePayload(res: Response): Promise<{ data: any; rawText: string }> {
|
||||
const rawText = await res.text();
|
||||
if (!rawText.trim()) {
|
||||
@@ -27,7 +76,7 @@ async function readResponsePayload(res: Response): Promise<{ data: any; rawText:
|
||||
|
||||
export async function updateCpnpNoInBC(articleNo: string, cpnpNo: string): Promise<BCUpdateResult> {
|
||||
try {
|
||||
const res = await fetch(BC_PROXY_URL, {
|
||||
const res = await fetch(BC_SYNC_APPLY_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ articleNo, cpnpNo }),
|
||||
@@ -48,6 +97,52 @@ export async function updateCpnpNoInBC(articleNo: string, cpnpNo: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
export async function previewBusinessCentralSync(headers: string[], row: any[]): Promise<BCSyncPreviewResult> {
|
||||
try {
|
||||
const res = await fetch(BC_SYNC_PREVIEW_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ headers, row }),
|
||||
});
|
||||
|
||||
const { data, rawText } = await readResponsePayload(res);
|
||||
if (!res.ok || !data?.success) {
|
||||
const error =
|
||||
data?.error ||
|
||||
(typeof data === 'string' && data.trim()) ||
|
||||
rawText ||
|
||||
`HTTP ${res.status}`;
|
||||
return { success: false, error } as BCSyncPreviewResult;
|
||||
}
|
||||
return data as BCSyncPreviewResult;
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message } as BCSyncPreviewResult;
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyBusinessCentralSync(headers: string[], row: any[], previewToken?: string): Promise<BCSyncApplyResult> {
|
||||
try {
|
||||
const res = await fetch(BC_SYNC_APPLY_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ headers, row, previewToken }),
|
||||
});
|
||||
|
||||
const { data, rawText } = await readResponsePayload(res);
|
||||
if (!res.ok || !data?.success) {
|
||||
const error =
|
||||
data?.error ||
|
||||
(typeof data === 'string' && data.trim()) ||
|
||||
rawText ||
|
||||
`HTTP ${res.status}`;
|
||||
return { success: false, error } as BCSyncApplyResult;
|
||||
}
|
||||
return data as BCSyncApplyResult;
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err.message } as BCSyncApplyResult;
|
||||
}
|
||||
}
|
||||
|
||||
function getFilenameFromDisposition(contentDisposition: string | null): string | null {
|
||||
if (!contentDisposition) return null;
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { ExcelRow } from '../types';
|
||||
|
||||
export interface MappingFieldPreview {
|
||||
sourceLabel: string;
|
||||
sourceIndex: number | null;
|
||||
targetField: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface BusinessCentralMappingPreview {
|
||||
articleNo: string;
|
||||
itemsPayload: Record<string, any>;
|
||||
itemUnitsOfMeasurePayload: Record<string, any>;
|
||||
itemsFields: MappingFieldPreview[];
|
||||
itemUnitsFields: MappingFieldPreview[];
|
||||
}
|
||||
|
||||
function findHeaderIndex(headers: string[], patterns: string[]): number {
|
||||
const normalized = headers.map(h => String(h || '').toLowerCase());
|
||||
return normalized.findIndex(header =>
|
||||
patterns.every(pattern => header.includes(pattern.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateForBc(value: any): string | null {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
|
||||
if (typeof value === 'number' && value >= 25569 && value <= 60000) {
|
||||
const excelEpoch = new Date(1899, 11, 30);
|
||||
const date = new Date(excelEpoch.getTime() + value * 86400000);
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
const str = String(value).trim();
|
||||
if (!str) return null;
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return str;
|
||||
|
||||
const parsed = new Date(str);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return parsed.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function getValue(row: ExcelRow, index: number | null): any {
|
||||
if (index === null || index < 0) return null;
|
||||
const value = row[index];
|
||||
return value === undefined || value === '' ? null : value;
|
||||
}
|
||||
|
||||
function toBcDecimal(value: any): number | null {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
|
||||
const normalized = String(value).trim().replace(/\s+/g, '').replace(',', '.');
|
||||
if (!normalized) return null;
|
||||
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function makeFieldPreview(
|
||||
sourceLabel: string,
|
||||
sourceIndex: number | null,
|
||||
targetField: string,
|
||||
value: any
|
||||
): MappingFieldPreview {
|
||||
return { sourceLabel, sourceIndex, targetField, value };
|
||||
}
|
||||
|
||||
export function buildBusinessCentralMappingPreview(headers: string[], row: ExcelRow): BusinessCentralMappingPreview {
|
||||
const articleNoIdx = findHeaderIndex(headers, ['article', 'no']);
|
||||
const articleDetailsEnIdx = findHeaderIndex(headers, ['article', 'details', 'english']);
|
||||
const articleDetailsDeIdx = findHeaderIndex(headers, ['article', 'details', 'german']);
|
||||
const launchDateIdx = findHeaderIndex(headers, ['launch']);
|
||||
const readyToOrderDateIdx = findHeaderIndex(headers, ['ready']);
|
||||
const moqIdx = findHeaderIndex(headers, ['moq']);
|
||||
const shortDeIdx = findHeaderIndex(headers, ['short', 'description', 'german']);
|
||||
const shortEnIdx = findHeaderIndex(headers, ['short', 'description', 'english']);
|
||||
const cpnpIdx = findHeaderIndex(headers, ['cpnp']);
|
||||
|
||||
const unitsOuterIdx = findHeaderIndex(headers, ['units', 'outer']);
|
||||
const innerWIdx = findHeaderIndex(headers, ['inner', 'w']);
|
||||
const innerLIdx = findHeaderIndex(headers, ['inner', 'l']);
|
||||
const innerHIdx = findHeaderIndex(headers, ['inner', 'h']);
|
||||
const outerWIdx = findHeaderIndex(headers, ['outer', 'w']);
|
||||
const outerLIdx = findHeaderIndex(headers, ['outer', 'l']);
|
||||
const outerHIdx = findHeaderIndex(headers, ['outer', 'h']);
|
||||
|
||||
const articleNo = String(getValue(row, articleNoIdx) ?? '');
|
||||
|
||||
const itemsPayload = {
|
||||
no: getValue(row, articleNoIdx),
|
||||
articleDetailsEnglish: getValue(row, articleDetailsEnIdx),
|
||||
articleDetailsGerman: getValue(row, articleDetailsDeIdx),
|
||||
launchDate: formatDateForBc(getValue(row, launchDateIdx)),
|
||||
readyToOrderDate: formatDateForBc(getValue(row, readyToOrderDateIdx)),
|
||||
minimumOrderQuantity: toBcDecimal(getValue(row, moqIdx)),
|
||||
shortDescriptionInGerman: getValue(row, shortDeIdx),
|
||||
shortDescriptionInEnglish: getValue(row, shortEnIdx),
|
||||
cpnpNo: getValue(row, cpnpIdx),
|
||||
};
|
||||
|
||||
const itemUnitsOfMeasurePayload = {
|
||||
itemNo: getValue(row, articleNoIdx),
|
||||
qtyPerUnitOfMeasure6: toBcDecimal(getValue(row, unitsOuterIdx)),
|
||||
width4: toBcDecimal(getValue(row, innerWIdx)),
|
||||
length4: toBcDecimal(getValue(row, innerLIdx)),
|
||||
height4: toBcDecimal(getValue(row, innerHIdx)),
|
||||
width6: toBcDecimal(getValue(row, outerWIdx)),
|
||||
length6: toBcDecimal(getValue(row, outerLIdx)),
|
||||
height6: toBcDecimal(getValue(row, outerHIdx)),
|
||||
};
|
||||
|
||||
return {
|
||||
articleNo,
|
||||
itemsPayload,
|
||||
itemUnitsOfMeasurePayload,
|
||||
itemsFields: [
|
||||
makeFieldPreview('Article No.', articleNoIdx, 'no', itemsPayload.no),
|
||||
makeFieldPreview('Article Details - English', articleDetailsEnIdx, 'articleDetailsEnglish', itemsPayload.articleDetailsEnglish),
|
||||
makeFieldPreview('Article Details - German', articleDetailsDeIdx, 'articleDetailsGerman', itemsPayload.articleDetailsGerman),
|
||||
makeFieldPreview('Launch Date', launchDateIdx, 'launchDate', itemsPayload.launchDate),
|
||||
makeFieldPreview('Ready to Order Date', readyToOrderDateIdx, 'readyToOrderDate', itemsPayload.readyToOrderDate),
|
||||
makeFieldPreview('MOQ', moqIdx, 'minimumOrderQuantity', itemsPayload.minimumOrderQuantity),
|
||||
makeFieldPreview('Short Description - German', shortDeIdx, 'shortDescriptionInGerman', itemsPayload.shortDescriptionInGerman),
|
||||
makeFieldPreview('Short Description - English', shortEnIdx, 'shortDescriptionInEnglish', itemsPayload.shortDescriptionInEnglish),
|
||||
makeFieldPreview('CPNP', cpnpIdx, 'cpnpNo', itemsPayload.cpnpNo),
|
||||
],
|
||||
itemUnitsFields: [
|
||||
makeFieldPreview('Article No.', articleNoIdx, 'itemNo', itemUnitsOfMeasurePayload.itemNo),
|
||||
makeFieldPreview('Units Outer', unitsOuterIdx, 'qtyPerUnitOfMeasure6', itemUnitsOfMeasurePayload.qtyPerUnitOfMeasure6),
|
||||
makeFieldPreview('CDU/Inner W (cm)', innerWIdx, 'width4', itemUnitsOfMeasurePayload.width4),
|
||||
makeFieldPreview('CDU/Inner L (cm)', innerLIdx, 'length4', itemUnitsOfMeasurePayload.length4),
|
||||
makeFieldPreview('CDU/Inner H (cm)', innerHIdx, 'height4', itemUnitsOfMeasurePayload.height4),
|
||||
makeFieldPreview('Outer W (cm)', outerWIdx, 'width6', itemUnitsOfMeasurePayload.width6),
|
||||
makeFieldPreview('Outer L (cm)', outerLIdx, 'length6', itemUnitsOfMeasurePayload.length6),
|
||||
makeFieldPreview('Outer H (cm)', outerHIdx, 'height6', itemUnitsOfMeasurePayload.height6),
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user