import { refreshSession, getStoredSession } from './auth'; import { COLUMNS } from '../types'; const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co'; const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv'; export async function safeFetch(url: string, options: RequestInit = {}): Promise { const session = getStoredSession(); const token = session?.access_token || SUPABASE_ANON_KEY; const headers = { ...options.headers, 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${token}`, }; let response = await fetch(url, { ...options, headers }); if (response.status === 401 && session?.refresh_token) { try { const newSession = await refreshSession(session.refresh_token); const newHeaders = { ...options.headers, 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${newSession.access_token}`, }; response = await fetch(url, { ...options, headers: newHeaders }); } catch (refreshError) { console.error('Session refresh failed:', refreshError); } } return response; } export interface ExcelRow extends Array {} export interface SyncedRow { data: ExcelRow; status?: 'pending' | 'edited' | 'synced' | 'excel'; updated_at?: string; } export async function getAllSyncedRows(): Promise> { const PAGE_SIZE = 1000; const result: Record = {}; try { let offset = 0; // Paginate until Supabase returns fewer rows than PAGE_SIZE (no more pages). // Supabase REST caps a single response at 1000 rows, so we must page. // Safety cap at 20 pages (20k products) to avoid infinite loops. for (let page = 0; page < 20; page++) { const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products?select=product_id,data,status,updated_at&order=product_id.asc&limit=${PAGE_SIZE}&offset=${offset}`, { cache: 'no-store' } ); if (!response.ok) { const errText = await response.text(); console.error('getAllSyncedRows failed:', response.status, errText); return result; } const rows = await response.json(); for (const row of rows) { const current = result[row.product_id]; const currentUpdatedAt = current?.updated_at ? Date.parse(current.updated_at) : -1; const nextUpdatedAt = row.updated_at ? Date.parse(row.updated_at) : -1; if (!current || nextUpdatedAt >= currentUpdatedAt) { result[row.product_id] = { data: row.data, status: row.status, updated_at: row.updated_at }; } } if (rows.length < PAGE_SIZE) break; offset += PAGE_SIZE; } return result; } catch (error) { console.error('Error fetching synced rows from Supabase:', error); return result; } } export async function saveRowToSupabase(articleNo: string, rowData: ExcelRow, status: 'pending' | 'edited' | 'synced' = 'pending'): Promise<{ success: boolean; error?: string }> { try { const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products?on_conflict=product_id`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Prefer': 'resolution=merge-duplicates,return=minimal', }, body: JSON.stringify({ product_id: articleNo, data: rowData, status, updated_at: new Date().toISOString() }) } ); if (!response.ok) { const err = await response.json().catch(() => ({})); return { success: false, error: `${response.status} ${response.statusText}: ${err.message || err.error_description || 'Unknown error'}` }; } return { success: true }; } catch (error: any) { console.error('Error saving to Supabase:', error); return { success: false, error: error.message || 'Network error' }; } } export interface HistoryEntry { id?: string; product_id: string; article_name: string; old_data: ExcelRow; new_data: ExcelRow; changed_by: string; changed_at: string; } export interface DashboardDescriptionsSnapshot { total: number; ok: number; longDeMissing: number; longEnMissing: number; shortDeMissing: number; shortEnMissing: number; } export interface DashboardArticleDetailsSnapshot { total: number; ok: number; detailsDeMissing: number; detailsEnMissing: number; } export interface DashboardPricingSnapshot { total: number; ok: number; itemToLogisticMissing: number; uvpMissing: number; srpIntMissing: number; srpUkMissing: number; unitsOuterMissing: number; outerWMissing: number; outerLMissing: number; outerHMissing: number; units40fMissing: number; moqMissing: number; weightIssues: number; } export interface DashboardCosmeticSnapshot { total: number; ok: number; cpnpMissing: number; } export interface DashboardSnapshotTabs { descriptions?: DashboardDescriptionsSnapshot; articleDetails?: DashboardArticleDetailsSnapshot; pricing?: DashboardPricingSnapshot; cosmeticItems?: DashboardCosmeticSnapshot; } const CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID = '__control_dashboard__'; function normalizeHistoryValue(value: any): any { if (value === undefined || value === null || value === '') return null; return value; } function valuesEqual(a: any, b: any): boolean { return normalizeHistoryValue(a) === normalizeHistoryValue(b); } function getChangedIndices(oldData: ExcelRow = [], newData: ExcelRow = []): number[] { const maxLen = Math.max(oldData.length, newData.length); const changed: number[] = []; for (let i = 0; i < maxLen; i++) { if (!valuesEqual(oldData[i], newData[i])) { changed.push(i); } } return changed; } export async function saveHistoryEntry( productId: string, articleName: string, oldData: ExcelRow, newData: ExcelRow, changedBy: string ): Promise<{ success: boolean; error?: string }> { try { const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Prefer': 'return=minimal', }, body: JSON.stringify({ product_id: productId, article_name: articleName, old_data: oldData, new_data: newData, changed_by: changedBy, changed_at: new Date().toISOString() }) } ); if (!response.ok) { const err = await response.json().catch(() => ({})); return { success: false, error: `History ${response.status}: ${err.message || 'Unknown error'}` }; } return { success: true }; } catch (error: any) { console.error('Error saving history to Supabase:', error); return { success: false, error: error.message || 'Network error' }; } } export async function getHistory(): Promise { try { const PAGE_SIZE = 1000; const allRows: HistoryEntry[] = []; for (let page = 0; page < 20; page++) { const offset = page * PAGE_SIZE; const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history?select=*&order=changed_at.asc&limit=${PAGE_SIZE}&offset=${offset}`, { cache: 'no-store' } ); if (!response.ok) { const errText = await response.text(); console.error('[getHistory] Error:', response.status, errText.substring(0, 200)); return [{ id: 'ERROR', product_id: 'ERROR', article_name: `Failed: ${response.status} ${errText.substring(0, 200)}`, old_data: [], new_data: [], changed_at: new Date().toISOString(), changed_by: 'system' }]; } const batch: HistoryEntry[] = await response.json(); allRows.push(...batch.filter(entry => entry.product_id !== CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID)); if (batch.length < PAGE_SIZE) break; } // Return oldest-first so index+1 = natural chronological number return allRows; } catch (error: any) { console.error('[getHistory] Exception:', error.message); return [{ id: 'EXCEPTION', product_id: 'EXCEPTION', article_name: `Message: ${error.message}`, old_data: [], new_data: [], changed_at: new Date().toISOString(), changed_by: 'system' }]; } } export async function getHistoryDataForMerge(): Promise> { try { const PAGE_SIZE = 1000; const entries: HistoryEntry[] = []; for (let page = 0; page < 20; page++) { const offset = page * PAGE_SIZE; const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history?select=product_id,old_data,new_data,changed_at,id&order=changed_at.asc&limit=${PAGE_SIZE}&offset=${offset}`, { cache: 'no-store' } ); if (!response.ok) { console.error('[getHistoryDataForMerge] Error:', response.status); return {}; } const batch: HistoryEntry[] = await response.json(); entries.push( ...batch.filter((entry: HistoryEntry) => entry.product_id !== CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID) ); if (batch.length < PAGE_SIZE) break; } entries.sort((a, b) => { const timeDelta = new Date(a.changed_at).getTime() - new Date(b.changed_at).getTime(); if (timeDelta !== 0) return timeDelta; return String(a.id || '').localeCompare(String(b.id || '')); }); const result: Record = {}; for (const entry of entries) { if (!entry.product_id) continue; if (!result[entry.product_id]) { result[entry.product_id] = []; } const target = result[entry.product_id]; const changedIndices = getChangedIndices(entry.old_data || [], entry.new_data || []); for (const idx of changedIndices) { target[idx] = entry.new_data?.[idx]; } // CPNP values were saved through multiple code paths over time. // Preserve the latest non-empty value even when a given history row // does not surface it as a changed index. const latestCpnp = entry.new_data?.[COLUMNS.CPNP_NO] ?? entry.old_data?.[COLUMNS.CPNP_NO]; if (latestCpnp !== undefined && latestCpnp !== null && latestCpnp !== '') { target[COLUMNS.CPNP_NO] = latestCpnp; } } return result; } catch (error: any) { console.error('[getHistoryDataForMerge] Exception:', error.message); return {}; } } export async function getDashboardSnapshotStore(): Promise> { try { const PAGE_SIZE = 1000; const rows: Array<{ changed_at: string; new_data: any }> = []; for (let page = 0; page < 20; page++) { const offset = page * PAGE_SIZE; const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history?select=changed_at,new_data,product_id&product_id=eq.${encodeURIComponent(CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID)}&order=changed_at.asc&limit=${PAGE_SIZE}&offset=${offset}`, { cache: 'no-store' } ); if (!response.ok) { const errText = await response.text(); console.error('[getDashboardSnapshotStore] Error:', response.status, errText.substring(0, 200)); return {}; } const batch: Array<{ changed_at: string; new_data: any }> = await response.json(); rows.push(...batch); if (batch.length < PAGE_SIZE) break; } const store: Record = {}; rows.forEach(row => { const snapshotDate = normalizeSnapshotDate(row.new_data?.snapshot_date || row.changed_at); const tabs = row.new_data?.tabs; if (!snapshotDate || !tabs || typeof tabs !== 'object') return; store[snapshotDate] = tabs as DashboardSnapshotTabs; }); return store; } catch (error) { console.error('[getDashboardSnapshotStore] Exception:', error); return {}; } } export async function ensureDashboardSnapshot(dateKey: string, tabs: DashboardSnapshotTabs): Promise<{ success: boolean; error?: string; created?: boolean }> { try { const existingRes = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history?select=id&product_id=eq.${encodeURIComponent(CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID)}&changed_at=gte.${encodeURIComponent(`${dateKey}T00:00:00.000Z`)}&changed_at=lt.${encodeURIComponent(nextUtcDateKey(dateKey))}&limit=1`, { cache: 'no-store' } ); if (!existingRes.ok) { const errText = await existingRes.text(); return { success: false, error: `Snapshot lookup failed: ${existingRes.status} ${errText.substring(0, 200)}` }; } const existing = await existingRes.json(); if (Array.isArray(existing) && existing.length > 0) { const existingId = existing[0]?.id; const currentTabs = existing[0]?.new_data?.tabs ?? {}; const mergedTabs = { ...currentTabs, ...tabs, }; if (JSON.stringify(currentTabs) === JSON.stringify(mergedTabs)) { return { success: true, created: false }; } if (!existingId) { return { success: true, created: false }; } const updateRes = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history?id=eq.${encodeURIComponent(existingId)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Prefer': 'return=minimal', }, body: JSON.stringify({ new_data: { snapshot_date: dateKey, tabs: mergedTabs, }, }), } ); if (!updateRes.ok) { const errText = await updateRes.text(); return { success: false, error: `Snapshot update failed: ${updateRes.status} ${errText.substring(0, 200)}` }; } return { success: true, created: false }; } const payload = { product_id: CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID, article_name: 'Control Dashboard Snapshot', old_data: [], new_data: { snapshot_date: dateKey, tabs, }, changed_by: 'system-control-dashboard', changed_at: `${dateKey}T00:00:00.000Z`, }; const insertRes = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Prefer': 'return=minimal', }, body: JSON.stringify(payload), } ); if (!insertRes.ok) { const errText = await insertRes.text(); return { success: false, error: `Snapshot save failed: ${insertRes.status} ${errText.substring(0, 200)}` }; } return { success: true, created: true }; } catch (error: any) { console.error('[ensureDashboardSnapshot] Exception:', error); return { success: false, error: error?.message || 'Network error' }; } } function normalizeSnapshotDate(value: string): string | null { const date = new Date(value); if (Number.isNaN(date.getTime())) return null; return date.toISOString().slice(0, 10); } function nextUtcDateKey(dateKey: string): string { const date = new Date(`${dateKey}T00:00:00.000Z`); date.setUTCDate(date.getUTCDate() + 1); return date.toISOString(); } export async function deleteHistoryEntry(id: string): Promise { try { const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products_history?id=eq.${encodeURIComponent(String(id))}`, { method: 'DELETE' } ); return response.ok; } catch (error) { console.error('Error deleting history from Supabase:', error); return false; } } export async function resetAllPendingRows(): Promise { try { const response = await safeFetch( `${SUPABASE_URL}/rest/v1/products?status=eq.pending`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Prefer': 'return=minimal', }, body: JSON.stringify({ status: 'edited' }) } ); return response.ok; } catch (error) { console.error('Error resetting pending rows in Supabase:', error); return false; } }