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 { success: boolean; 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; current: Record | null; changes: BCFieldChange[]; changedFields: string[]; writeConfigured: boolean; writeMethod: string | null; writeUrlTemplate: string | null; writeBodyTemplate: string | null; canApply: boolean; supported?: boolean; supportReason?: string | null; } 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; warning?: string; } async function readResponsePayload(res: Response): Promise<{ data: any; rawText: string }> { const rawText = await res.text(); if (!rawText.trim()) { return { data: null, rawText }; } try { return { data: JSON.parse(rawText), rawText }; } catch { return { data: rawText, rawText }; } } export async function updateCpnpNoInBC(articleNo: string, cpnpNo: string): Promise { try { const res = await fetch(BC_SYNC_APPLY_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ articleNo, cpnpNo }), }); 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 }; } return { success: true }; } catch (err: any) { return { success: false, error: err.message }; } } export async function previewBusinessCentralSync(headers: string[], row: any[]): Promise { 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 { 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; const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); if (utf8Match?.[1]) { try { return decodeURIComponent(utf8Match[1].trim().replace(/^"|"$/g, '')); } catch { return utf8Match[1].trim().replace(/^"|"$/g, ''); } } const filenameMatch = contentDisposition.match(/filename\s*=\s*([^;]+)/i); if (filenameMatch?.[1]) { return filenameMatch[1].trim().replace(/^"|"$/g, ''); } return null; } export async function downloadBusinessCentralItemsExcel(): Promise { try { const res = await fetch(BC_EXPORT_URL, { method: 'GET', }); if (!res.ok) { let error = `HTTP ${res.status}`; try { const { data, rawText } = await readResponsePayload(res); error = data?.error || (typeof data === 'string' && data.trim()) || rawText || error; } catch { error = `HTTP ${res.status}`; } return { success: false, error }; } const blob = await res.blob(); const filename = getFilenameFromDisposition(res.headers.get('content-disposition')) || `BusinessCentral_Items_${new Date().toISOString().split('T')[0]}.xlsx`; const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.rel = 'noopener'; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); return { success: true, filename }; } catch (err: any) { return { success: false, error: err.message }; } }