mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 18:55: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 { ProductDescriptions } from './components/ProductDescriptions';
|
||||||
import { MatrixView } from './components/MatrixView';
|
import { MatrixView } from './components/MatrixView';
|
||||||
import { EditPanel } from './components/EditPanel';
|
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 { getStoredSession, signOut, type AuthSession } from './lib/auth';
|
||||||
import { LoginPage } from './components/LoginPage';
|
import { LoginPage } from './components/LoginPage';
|
||||||
import { DimensionsView } from './components/DimensionsView';
|
import { DimensionsView } from './components/DimensionsView';
|
||||||
@@ -474,7 +474,7 @@ export default function App() {
|
|||||||
<HistoryView
|
<HistoryView
|
||||||
headers={appState.headers}
|
headers={appState.headers}
|
||||||
data={appState.data}
|
data={appState.data}
|
||||||
onRevert={(articleNo, revertedData) => {
|
onRevert={async (articleNo, revertedData, historyId) => {
|
||||||
// Find the row in appState.data and update it
|
// Find the row in appState.data and update it
|
||||||
const rowIndex = appState.data.findIndex(r => String(r[COLUMNS.ARTICLE_NO]) === articleNo);
|
const rowIndex = appState.data.findIndex(r => String(r[COLUMNS.ARTICLE_NO]) === articleNo);
|
||||||
if (rowIndex !== -1) {
|
if (rowIndex !== -1) {
|
||||||
@@ -494,6 +494,10 @@ export default function App() {
|
|||||||
articleName: String(revertedData[COLUMNS.ARTICLE_NAME] || articleNo),
|
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 React, { useState, useEffect } from 'react';
|
||||||
import { History, RotateCcw, ChevronDown, ChevronRight, User, Calendar, Tag } from 'lucide-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 { ExcelRow, COLUMNS } from '../types';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface HistoryViewProps {
|
interface HistoryViewProps {
|
||||||
headers: string[];
|
headers: string[];
|
||||||
data: ExcelRow[];
|
data: ExcelRow[];
|
||||||
onRevert: (articleNo: string, oldData: ExcelRow) => void;
|
onRevert: (articleNo: string, oldData: ExcelRow, historyId?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
||||||
@@ -27,6 +27,18 @@ export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
|||||||
setLoading(false);
|
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 getChangedFields = (oldData: ExcelRow, newData: ExcelRow) => {
|
||||||
const changes: { header: string; old: any; new: any; index: number }[] = [];
|
const changes: { header: string; old: any; new: any; index: number }[] = [];
|
||||||
const maxLen = Math.max(oldData.length, newData.length);
|
const maxLen = Math.max(oldData.length, newData.length);
|
||||||
@@ -157,17 +169,7 @@ export function HistoryView({ headers, data, onRevert }: HistoryViewProps) {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (window.confirm(`Are you sure you want to revert changes for ${entry.article_name}?`)) {
|
handleRevert(entry);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
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"
|
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_URL = 'https://hwithddwaapyhnfwcesj.supabase.co';
|
||||||
const SUPABASE_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
|
const SUPABASE_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv';
|
||||||
|
|
||||||
async function fetchWithRetry(
|
export interface ExcelRow extends Array<any> {}
|
||||||
url: string,
|
|
||||||
options: RequestInit,
|
export interface SyncedRow {
|
||||||
retries = 2,
|
data: ExcelRow;
|
||||||
delayMs = 1000
|
status?: 'pending' | 'synced';
|
||||||
): Promise<Response> {
|
}
|
||||||
let lastError: Error | null = null;
|
|
||||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
export async function getAllSyncedRows(): Promise<Record<string, SyncedRow>> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, options);
|
const response = await fetch(
|
||||||
if (res.ok || res.status < 500 || attempt === retries) return res;
|
`${SUPABASE_URL}/rest/v1/products?select=product_id,data,status&order=updated_at.desc`,
|
||||||
} catch (err) {
|
{
|
||||||
lastError = err as Error;
|
headers: {
|
||||||
if (attempt === retries) throw lastError;
|
'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> {
|
export async function saveRowToSupabase(articleNo: string, rowData: ExcelRow): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Single upsert: POST with Prefer=resolution=merge-duplicates
|
const response = await fetch(
|
||||||
// This handles both INSERT (new article) and UPDATE (existing) atomically.
|
`${SUPABASE_URL}/rest/v1/products?product_id=eq.${encodeURIComponent(articleNo)}`,
|
||||||
// The old PATCH approach silently failed for new articles because Supabase
|
{
|
||||||
// returns 200 OK with an empty body when no rows match — indistinguishable
|
method: 'PATCH',
|
||||||
// from a successful update.
|
headers: {
|
||||||
const response = await fetchWithRetry(`${SUPABASE_URL}/rest/v1/products_sync`, {
|
'Content-Type': 'application/json',
|
||||||
method: 'POST',
|
'apikey': SUPABASE_KEY,
|
||||||
headers: {
|
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||||
'apikey': SUPABASE_KEY,
|
'Prefer': 'return=minimal'
|
||||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
},
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({
|
||||||
'Prefer': 'resolution=merge-duplicates'
|
product_id: articleNo,
|
||||||
},
|
data: rowData,
|
||||||
body: JSON.stringify({
|
status: 'synced',
|
||||||
id: articleNo,
|
updated_at: new Date().toISOString()
|
||||||
data: rowData,
|
})
|
||||||
status_check: 'pending',
|
}
|
||||||
updated_at: new Date().toISOString()
|
);
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
return response.ok;
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
console.error('Supabase upsert failed:', response.status, errorData);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving to Supabase:', error);
|
console.error('Error saving to Supabase:', error);
|
||||||
return false;
|
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 {
|
export interface HistoryEntry {
|
||||||
id: string;
|
id?: number;
|
||||||
product_id: string;
|
product_id: string;
|
||||||
article_name: string;
|
article_name: string;
|
||||||
old_data: ExcelRow;
|
old_data: ExcelRow;
|
||||||
new_data: ExcelRow;
|
new_data: ExcelRow;
|
||||||
changed_at: string;
|
|
||||||
changed_by: string;
|
changed_by: string;
|
||||||
|
changed_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveHistoryEntry(
|
export async function saveHistoryEntry(
|
||||||
articleNo: string,
|
productId: string,
|
||||||
articleName: string,
|
articleName: string,
|
||||||
oldData: ExcelRow,
|
oldData: ExcelRow,
|
||||||
newData: ExcelRow,
|
newData: ExcelRow,
|
||||||
userEmail: string
|
changedBy: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${SUPABASE_URL}/rest/v1/products_history`, {
|
const response = await fetch(
|
||||||
method: 'POST',
|
`${SUPABASE_URL}/rest/v1/products_history`,
|
||||||
headers: {
|
{
|
||||||
'apikey': SUPABASE_KEY,
|
method: 'POST',
|
||||||
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
},
|
'apikey': SUPABASE_KEY,
|
||||||
body: JSON.stringify({
|
'Authorization': `Bearer ${SUPABASE_KEY}`,
|
||||||
product_id: articleNo,
|
'Prefer': 'return=minimal'
|
||||||
article_name: articleName,
|
},
|
||||||
old_data: oldData,
|
body: JSON.stringify({
|
||||||
new_data: newData,
|
product_id: productId,
|
||||||
changed_at: new Date().toISOString(),
|
article_name: articleName,
|
||||||
changed_by: userEmail
|
old_data: oldData,
|
||||||
})
|
new_data: newData,
|
||||||
});
|
changed_by: changedBy,
|
||||||
|
changed_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return response.ok;
|
return response.ok;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -171,3 +126,46 @@ export async function getHistory(): Promise<HistoryEntry[]> {
|
|||||||
return [];
|
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