mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:45:24 +02:00
Delete history entry after revert
This commit is contained in:
+6
-2
@@ -6,7 +6,7 @@ import { TopBar } from './components/TopBar';
|
||||
import { ProductDescriptions } from './components/ProductDescriptions';
|
||||
import { MatrixView } from './components/MatrixView';
|
||||
import { EditPanel } from './components/EditPanel';
|
||||
import { getAllSyncedRows, saveRowToSupabase, resetAllPendingRows, saveHistoryEntry } from './lib/supabase';
|
||||
import { getAllSyncedRows, saveRowToSupabase, resetAllPendingRows, saveHistoryEntry, deleteHistoryEntry } from './lib/supabase';
|
||||
import { getStoredSession, signOut, type AuthSession } from './lib/auth';
|
||||
import { LoginPage } from './components/LoginPage';
|
||||
import { DimensionsView } from './components/DimensionsView';
|
||||
@@ -474,7 +474,7 @@ export default function App() {
|
||||
<HistoryView
|
||||
headers={appState.headers}
|
||||
data={appState.data}
|
||||
onRevert={(articleNo, revertedData) => {
|
||||
onRevert={async (articleNo, revertedData, historyId) => {
|
||||
// Find the row in appState.data and update it
|
||||
const rowIndex = appState.data.findIndex(r => String(r[COLUMNS.ARTICLE_NO]) === articleNo);
|
||||
if (rowIndex !== -1) {
|
||||
@@ -494,6 +494,10 @@ export default function App() {
|
||||
articleName: String(revertedData[COLUMNS.ARTICLE_NAME] || articleNo),
|
||||
}
|
||||
}));
|
||||
// Delete the history entry after revert
|
||||
if (historyId) {
|
||||
await deleteHistoryEntry(String(historyId));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { History, RotateCcw, ChevronDown, ChevronRight, User, Calendar, Tag } from 'lucide-react';
|
||||
import { getHistory, HistoryEntry } from '../lib/supabase';
|
||||
import { getHistory, deleteHistoryEntry, HistoryEntry } from '../lib/supabase';
|
||||
import { ExcelRow, COLUMNS } from '../types';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
interface HistoryViewProps {
|
||||
headers: string[];
|
||||
data: ExcelRow[];
|
||||
onRevert: (articleNo: string, oldData: ExcelRow) => void;
|
||||
onRevert: (articleNo: string, oldData: ExcelRow, historyId?: number) => void;
|
||||
}
|
||||
|
||||
export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
||||
@@ -27,6 +27,18 @@ export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleRevert = (entry: HistoryEntry) => {
|
||||
if (window.confirm(`Are you sure you want to revert changes for ${entry.article_name}?`)) {
|
||||
const currentRow = data.find(r => String(r[0]) === entry.product_id);
|
||||
if (currentRow && JSON.stringify(currentRow) === JSON.stringify(entry.old_data)) {
|
||||
if (!window.confirm("Reverting will restore original data. No actual changes will be made. Continue?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
onRevert(entry.product_id, entry.old_data, entry.id);
|
||||
}
|
||||
};
|
||||
|
||||
const getChangedFields = (oldData: ExcelRow, newData: ExcelRow) => {
|
||||
const changes: { header: string; old: any; new: any; index: number }[] = [];
|
||||
const maxLen = Math.max(oldData.length, newData.length);
|
||||
@@ -157,17 +169,7 @@ export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm(`Are you sure you want to revert changes for ${entry.article_name}?`)) {
|
||||
// Check if old_data equals current data in appState
|
||||
const currentRow = data.find(r => String(r[0]) === entry.product_id);
|
||||
if (currentRow && JSON.stringify(currentRow) === JSON.stringify(entry.old_data)) {
|
||||
if (window.confirm("Reverting will restore original data. No actual changes will be made. Continue?")) {
|
||||
onRevert(entry.product_id, entry.old_data);
|
||||
}
|
||||
} else {
|
||||
onRevert(entry.product_id, entry.old_data);
|
||||
}
|
||||
}
|
||||
handleRevert(entry);
|
||||
}}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-orange-500/10 text-orange-400 hover:bg-orange-500/20 transition-colors border border-orange-500/20"
|
||||
>
|
||||
|
||||
+114
-116
@@ -1,149 +1,104 @@
|
||||
import { ExcelRow } from '../types';
|
||||
|
||||
const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co';
|
||||
const SUPABASE_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
|
||||
|
||||
async function fetchWithRetry(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
retries = 2,
|
||||
delayMs = 1000
|
||||
): Promise<Response> {
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
if (res.ok || res.status < 500 || attempt === retries) return res;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
if (attempt === retries) throw lastError;
|
||||
export interface ExcelRow extends Array<any> {}
|
||||
|
||||
export interface SyncedRow {
|
||||
data: ExcelRow;
|
||||
status?: 'pending' | 'synced';
|
||||
}
|
||||
|
||||
export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/products?select=product_id,data,status&order=updated_at.desc`,
|
||||
{
|
||||
headers: {
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) return {};
|
||||
const rows = await response.json();
|
||||
const result: Record<string, SyncedRow> = {};
|
||||
for (const row of rows) {
|
||||
result[row.product_id] = { data: row.data, status: row.status };
|
||||
}
|
||||
await new Promise(r => setTimeout(r, delayMs));
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching synced rows from Supabase:', error);
|
||||
return {};
|
||||
}
|
||||
throw lastError ?? new Error('fetch failed');
|
||||
}
|
||||
|
||||
export async function saveRowToSupabase(articleNo: string, rowData: ExcelRow): Promise<boolean> {
|
||||
try {
|
||||
// Single upsert: POST with Prefer=resolution=merge-duplicates
|
||||
// This handles both INSERT (new article) and UPDATE (existing) atomically.
|
||||
// The old PATCH approach silently failed for new articles because Supabase
|
||||
// returns 200 OK with an empty body when no rows match — indistinguishable
|
||||
// from a successful update.
|
||||
const response = await fetchWithRetry(`${SUPABASE_URL}/rest/v1/products_sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Prefer': 'resolution=merge-duplicates'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: articleNo,
|
||||
data: rowData,
|
||||
status_check: 'pending',
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/products?product_id=eq.${encodeURIComponent(articleNo)}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'Prefer': 'return=minimal'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: articleNo,
|
||||
data: rowData,
|
||||
status: 'synced',
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error('Supabase upsert failed:', response.status, errorData);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error saving to Supabase:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllSyncedRows(): Promise<Record<string, { data: ExcelRow, status: string }>> {
|
||||
try {
|
||||
// Explicit limit to avoid Supabase's default 1000-row cap
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/products_sync?select=id,data,status_check&limit=10000`,
|
||||
{
|
||||
headers: {
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'Range': '0-9999'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.json();
|
||||
const result: Record<string, { data: ExcelRow, status: string }> = {};
|
||||
data.forEach((item: any) => {
|
||||
result[item.id] = {
|
||||
data: item.data,
|
||||
status: item.status_check || 'original'
|
||||
};
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error fetching from Supabase:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetAllPendingRows(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${SUPABASE_URL}/rest/v1/products_sync?status_check=eq.pending`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status_check: 'original',
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error resetting statuses in Supabase:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
id: string;
|
||||
id?: number;
|
||||
product_id: string;
|
||||
article_name: string;
|
||||
old_data: ExcelRow;
|
||||
new_data: ExcelRow;
|
||||
changed_at: string;
|
||||
changed_by: string;
|
||||
changed_at: string;
|
||||
}
|
||||
|
||||
export async function saveHistoryEntry(
|
||||
articleNo: string,
|
||||
productId: string,
|
||||
articleName: string,
|
||||
oldData: ExcelRow,
|
||||
newData: ExcelRow,
|
||||
userEmail: string
|
||||
changedBy: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${SUPABASE_URL}/rest/v1/products_history`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_id: articleNo,
|
||||
article_name: articleName,
|
||||
old_data: oldData,
|
||||
new_data: newData,
|
||||
changed_at: new Date().toISOString(),
|
||||
changed_by: userEmail
|
||||
})
|
||||
});
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/products_history`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'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()
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
@@ -171,3 +126,46 @@ export async function getHistory(): Promise<HistoryEntry[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteHistoryEntry(id: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/products_history?id=eq.${encodeURIComponent(id)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error deleting history from Supabase:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetAllPendingRows(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${SUPABASE_URL}/rest/v1/products?status=eq.pending`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'apikey': SUPABASE_KEY,
|
||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||
'Prefer': 'return=minimal'
|
||||
},
|
||||
body: JSON.stringify({ status: 'synced' })
|
||||
}
|
||||
);
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error resetting pending rows in Supabase:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user