mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 13:35:25 +02:00
feat: implement manual user validation and user deletion flow
This commit is contained in:
+199
-7
@@ -3,7 +3,7 @@ import { refreshSession, getStoredSession } from './auth';
|
||||
const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co';
|
||||
const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
|
||||
|
||||
async function safeFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
export async function safeFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const session = getStoredSession();
|
||||
const token = session?.access_token || SUPABASE_ANON_KEY;
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface ExcelRow extends Array<any> {}
|
||||
export interface SyncedRow {
|
||||
data: ExcelRow;
|
||||
status?: 'pending' | 'edited' | 'synced' | 'excel';
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||
@@ -49,7 +50,7 @@ export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||
// 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}`,
|
||||
`${SUPABASE_URL}/rest/v1/products?select=product_id,data,status,updated_at&order=product_id.asc&limit=${PAGE_SIZE}&offset=${offset}`,
|
||||
{ cache: 'no-store' }
|
||||
);
|
||||
|
||||
@@ -60,7 +61,12 @@ export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||
}
|
||||
const rows = await response.json();
|
||||
for (const row of rows) {
|
||||
result[row.product_id] = { data: row.data, status: row.status };
|
||||
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;
|
||||
@@ -75,12 +81,12 @@ export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||
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`,
|
||||
`${SUPABASE_URL}/rest/v1/products?on_conflict=product_id`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Prefer': 'resolution=merge-duplicates',
|
||||
'Prefer': 'resolution=merge-duplicates,return=minimal',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: articleNo,
|
||||
@@ -116,6 +122,53 @@ export interface HistoryEntry {
|
||||
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;
|
||||
@@ -205,7 +258,7 @@ export async function getHistory(): Promise<HistoryEntry[]> {
|
||||
}];
|
||||
}
|
||||
const batch: HistoryEntry[] = await response.json();
|
||||
allRows.push(...batch);
|
||||
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
|
||||
@@ -235,7 +288,7 @@ export async function getHistoryDataForMerge(): Promise<Record<string, ExcelRow>
|
||||
console.error('[getHistoryDataForMerge] Error:', response.status);
|
||||
return {};
|
||||
}
|
||||
const entries: HistoryEntry[] = await response.json();
|
||||
const entries: HistoryEntry[] = (await response.json()).filter((entry: HistoryEntry) => entry.product_id !== CONTROL_DASHBOARD_SNAPSHOT_PRODUCT_ID);
|
||||
entries.sort((a, b) => {
|
||||
const timeDelta = new Date(a.changed_at).getTime() - new Date(b.changed_at).getTime();
|
||||
if (timeDelta !== 0) return timeDelta;
|
||||
@@ -266,6 +319,145 @@ export async function getHistoryDataForMerge(): Promise<Record<string, ExcelRow>
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDashboardSnapshotStore(): Promise<Record<string, DashboardSnapshotTabs>> {
|
||||
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<string, DashboardSnapshotTabs> = {};
|
||||
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<boolean> {
|
||||
try {
|
||||
const response = await safeFetch(
|
||||
|
||||
Reference in New Issue
Block a user