Files
Craze-Data-check/src/lib/supabase.ts
T

303 lines
8.8 KiB
TypeScript
Raw Normal View History

import { refreshSession, getStoredSession } from './auth';
2026-03-27 12:23:35 +01:00
const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co';
const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
async function safeFetch(url: string, options: RequestInit = {}): Promise<Response> {
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;
}
2026-03-27 12:23:35 +01:00
2026-04-10 09:47:35 +02:00
export interface ExcelRow extends Array<any> {}
export interface SyncedRow {
data: ExcelRow;
status?: 'pending' | 'edited' | 'synced' | 'excel';
2026-04-10 09:47:35 +02:00
}
export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
const PAGE_SIZE = 1000;
const result: Record<string, SyncedRow> = {};
2026-04-10 09:47:35 +02:00
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&order=product_id.asc&limit=${PAGE_SIZE}&offset=${offset}`,
{ cache: 'no-store' }
);
2026-04-10 09:47:35 +02:00
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) {
result[row.product_id] = { data: row.data, status: row.status };
}
if (rows.length < PAGE_SIZE) break;
offset += PAGE_SIZE;
}
2026-04-10 09:47:35 +02:00
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 }> {
2026-03-27 12:23:35 +01:00
try {
const response = await safeFetch(
`${SUPABASE_URL}/rest/v1/products`,
2026-04-10 09:47:35 +02:00
{
method: 'POST',
2026-04-10 09:47:35 +02:00
headers: {
'Content-Type': 'application/json',
'Prefer': 'resolution=merge-duplicates',
2026-04-10 09:47:35 +02:00
},
body: JSON.stringify({
product_id: articleNo,
data: rowData,
status,
2026-04-10 09:47:35 +02:00
updated_at: new Date().toISOString()
})
}
);
2026-03-27 12:23:35 +01:00
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) {
2026-03-27 12:23:35 +01:00
console.error('Error saving to Supabase:', error);
return { success: false, error: error.message || 'Network error' };
2026-03-27 12:23:35 +01:00
}
}
export interface HistoryEntry {
id?: string;
product_id: string;
article_name: string;
old_data: ExcelRow;
new_data: ExcelRow;
changed_by: string;
2026-04-10 09:47:35 +02:00
changed_at: string;
}
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(
2026-04-10 09:47:35 +02:00
productId: string,
articleName: string,
oldData: ExcelRow,
newData: ExcelRow,
changedBy: string
): Promise<{ success: boolean; error?: string }> {
try {
const response = await safeFetch(
2026-04-10 09:47:35 +02:00
`${SUPABASE_URL}/rest/v1/products_history`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Prefer': 'return=minimal',
2026-04-10 09:47:35 +02:00
},
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<HistoryEntry[]> {
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);
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'
}];
}
}
2026-04-10 09:47:35 +02:00
export async function getHistoryDataForMerge(): Promise<Record<string, ExcelRow>> {
try {
const response = await safeFetch(
`${SUPABASE_URL}/rest/v1/products_history?select=product_id,old_data,new_data,changed_at,id`,
{ cache: 'no-store' }
);
if (!response.ok) {
console.error('[getHistoryDataForMerge] Error:', response.status);
return {};
}
const entries: HistoryEntry[] = await response.json();
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<string, ExcelRow> = {};
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];
}
}
return result;
} catch (error: any) {
console.error('[getHistoryDataForMerge] Exception:', error.message);
return {};
}
}
export async function deleteHistoryEntry(id: string): Promise<boolean> {
2026-04-10 09:47:35 +02:00
try {
const response = await safeFetch(
`${SUPABASE_URL}/rest/v1/products_history?id=eq.${encodeURIComponent(String(id))}`,
{ method: 'DELETE' }
2026-04-10 09:47:35 +02:00
);
return response.ok;
} catch (error) {
console.error('Error deleting history from Supabase:', error);
return false;
}
}
export async function resetAllPendingRows(): Promise<boolean> {
2026-04-10 09:47:35 +02:00
try {
const response = await safeFetch(
2026-04-10 09:47:35 +02:00
`${SUPABASE_URL}/rest/v1/products?status=eq.pending`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Prefer': 'return=minimal',
2026-04-10 09:47:35 +02:00
},
body: JSON.stringify({ status: 'edited' })
2026-04-10 09:47:35 +02:00
}
);
return response.ok;
} catch (error) {
console.error('Error resetting pending rows in Supabase:', error);
return false;
}
}