mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:05:25 +02:00
feat: add Pending Validation tab showing all pending changes
This commit is contained in:
+44
-3
@@ -6,13 +6,15 @@ 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 } from './lib/supabase';
|
import { getAllSyncedRows, saveRowToSupabase, resetAllPendingRows, saveHistoryEntry } 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';
|
||||||
import { PricingView } from './components/PricingView';
|
import { PricingView } from './components/PricingView';
|
||||||
import { ArticleDetails } from './components/ArticleDetails';
|
import { ArticleDetails } from './components/ArticleDetails';
|
||||||
|
import { HistoryView } from './components/HistoryView';
|
||||||
import { UndoToast } from './components/UndoToast';
|
import { UndoToast } from './components/UndoToast';
|
||||||
|
import { PendingValidationView } from './components/PendingValidationView';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
|
const [session, setSession] = useState<AuthSession | null>(() => getStoredSession());
|
||||||
@@ -33,7 +35,7 @@ export default function App() {
|
|||||||
fileDate: null,
|
fileDate: null,
|
||||||
hasUnsavedChanges: false
|
hasUnsavedChanges: false
|
||||||
});
|
});
|
||||||
const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing'>('descriptions');
|
const [activeModule, setActiveModule] = useState<'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history'>('descriptions');
|
||||||
const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]);
|
const [undoHistory, setUndoHistory] = useState<{ data: ExcelRow[], message: string }[]>([]);
|
||||||
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
|
const [editingRowIndex, setEditingRowIndex] = useState<number | null>(null);
|
||||||
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
|
const [isLoadingDefault, setIsLoadingDefault] = useState(true);
|
||||||
@@ -280,9 +282,12 @@ export default function App() {
|
|||||||
if (entries.length === 0) return;
|
if (entries.length === 0) return;
|
||||||
setIsSavingAll(true);
|
setIsSavingAll(true);
|
||||||
let allSuccess = true;
|
let allSuccess = true;
|
||||||
for (const [articleNo, { newData }] of entries) {
|
for (const [articleNo, { newData, originalData, articleName }] of entries) {
|
||||||
const success = await saveRowToSupabase(articleNo, newData);
|
const success = await saveRowToSupabase(articleNo, newData);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
// Also save to history
|
||||||
|
await saveHistoryEntry(articleNo, articleName, originalData, newData, session?.user?.email || 'unknown');
|
||||||
|
|
||||||
setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' }));
|
setRowStatuses(prev => ({ ...prev, [articleNo]: 'saved' }));
|
||||||
setPendingRows(prev => { const n = { ...prev }; delete n[articleNo]; return n; });
|
setPendingRows(prev => { const n = { ...prev }; delete n[articleNo]; return n; });
|
||||||
} else {
|
} else {
|
||||||
@@ -435,6 +440,42 @@ export default function App() {
|
|||||||
rowStatuses={rowStatuses}
|
rowStatuses={rowStatuses}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeModule === 'pending_validation' && (
|
||||||
|
<PendingValidationView
|
||||||
|
data={appState.data}
|
||||||
|
pendingRows={pendingRows}
|
||||||
|
rowStatuses={rowStatuses}
|
||||||
|
onRevertRow={handleRevertRow}
|
||||||
|
onEdit={(index) => setEditingRowIndex(index)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeModule === 'history' && (
|
||||||
|
<HistoryView
|
||||||
|
headers={appState.headers}
|
||||||
|
onRevert={(articleNo, revertedData) => {
|
||||||
|
// Find the row in appState.data and update it
|
||||||
|
const rowIndex = appState.data.findIndex(r => String(r[COLUMNS.ARTICLE_NO]) === articleNo);
|
||||||
|
if (rowIndex !== -1) {
|
||||||
|
setAppState(prev => {
|
||||||
|
const newData = [...prev.data];
|
||||||
|
newData[rowIndex] = revertedData;
|
||||||
|
return { ...prev, data: newData, hasUnsavedChanges: true };
|
||||||
|
});
|
||||||
|
// Mark as pending for sync
|
||||||
|
setRowStatuses(prev => ({ ...prev, [articleNo]: 'pending' }));
|
||||||
|
setPendingRows(prev => ({
|
||||||
|
...prev,
|
||||||
|
[articleNo]: {
|
||||||
|
rowIndex,
|
||||||
|
originalData: appState.data[rowIndex],
|
||||||
|
newData: revertedData,
|
||||||
|
articleName: String(revertedData[COLUMNS.ARTICLE_NAME] || articleNo),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { History, RotateCcw, ChevronDown, ChevronRight, User, Calendar, Tag } from 'lucide-react';
|
||||||
|
import { getHistory, HistoryEntry } from '../lib/supabase';
|
||||||
|
import { ExcelRow, COLUMNS } from '../types';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
|
interface HistoryViewProps {
|
||||||
|
headers: string[];
|
||||||
|
onRevert: (articleNo: string, oldData: ExcelRow) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryView({ headers, onRevert }: HistoryViewProps) {
|
||||||
|
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadHistory();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadHistory = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await getHistory();
|
||||||
|
setHistory(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChangedFields = (oldData: ExcelRow, newData: ExcelRow) => {
|
||||||
|
const changes: { header: string; old: any; new: any; index: number }[] = [];
|
||||||
|
const maxLen = Math.max(oldData.length, newData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
if (oldData[i] !== newData[i]) {
|
||||||
|
changes.push({
|
||||||
|
header: headers[i] || `Col ${i}`,
|
||||||
|
old: oldData[i],
|
||||||
|
new: newData[i],
|
||||||
|
index: i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return new Intl.DateTimeFormat('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}).format(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredHistory = history.filter(entry =>
|
||||||
|
entry.article_name?.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
entry.product_id.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
entry.changed_by?.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-slate-400">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
|
||||||
|
<p className="text-lg">Fetching history...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
||||||
|
<History className="w-8 h-8 text-blue-500" />
|
||||||
|
Change History
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-400 mt-1">Review and revert any changes made to products.</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Tag className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search history..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-md text-sm text-slate-200 focus:outline-none focus:border-blue-500 transition-colors w-64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={loadHistory}
|
||||||
|
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md transition-colors flex items-center gap-2 border border-slate-700"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-[#0a1628] border border-slate-800 rounded-xl overflow-hidden shadow-2xl">
|
||||||
|
{filteredHistory.length === 0 ? (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<History className="w-12 h-12 text-slate-700 mx-auto mb-4" />
|
||||||
|
<p className="text-slate-500 text-lg">No history records found.</p>
|
||||||
|
<p className="text-slate-600 text-sm mt-1">
|
||||||
|
{search ? "Try adjusting your search filters." : "Changes are recorded when you 'Save All' pending modifications."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-slate-800">
|
||||||
|
{filteredHistory.map((entry) => {
|
||||||
|
const isExpanded = expandedId === entry.id;
|
||||||
|
const changes = getChangedFields(entry.old_data, entry.new_data);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={entry.id} className={cn(
|
||||||
|
"transition-colors",
|
||||||
|
isExpanded ? "bg-blue-600/5" : "hover:bg-slate-800/30"
|
||||||
|
)}>
|
||||||
|
{/* Summary Row */}
|
||||||
|
<div
|
||||||
|
className="p-4 flex items-center gap-4 cursor-pointer"
|
||||||
|
onClick={() => setExpandedId(isExpanded ? null : entry.id)}
|
||||||
|
>
|
||||||
|
{isExpanded ? <ChevronDown className="w-5 h-5 text-slate-500" /> : <ChevronRight className="w-5 h-5 text-slate-500" />}
|
||||||
|
|
||||||
|
<div className="flex-1 grid grid-cols-4 gap-4 items-center">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center text-blue-500 font-bold shrink-0">
|
||||||
|
{entry.product_id.slice(0, 2).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="font-semibold text-slate-200 truncate">{entry.article_name}</div>
|
||||||
|
<div className="text-xs text-slate-500 font-mono">ID: {entry.product_id}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-slate-400">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
<span className="text-sm truncate">{entry.changed_by}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-slate-400">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span className="text-sm">{formatDate(entry.changed_at)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3 text-sm">
|
||||||
|
<span className="px-2.5 py-1 rounded-full bg-blue-500/10 text-blue-400 font-medium">
|
||||||
|
{changes.length} {changes.length === 1 ? 'change' : 'changes'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (window.confirm(`Are you sure you want to revert changes for ${entry.article_name}?`)) {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
|
Revert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details View */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="px-14 pb-6 pt-2 animate-in slide-in-from-top-2 duration-300">
|
||||||
|
<div className="bg-[#040d1a] border border-slate-700/50 rounded-lg overflow-hidden">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-800/50 text-slate-400 text-left">
|
||||||
|
<th className="px-4 py-2 font-medium">Field</th>
|
||||||
|
<th className="px-4 py-2 font-medium">Original Value</th>
|
||||||
|
<th className="px-4 py-2 font-medium">New Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-800">
|
||||||
|
{changes.map((change, idx) => (
|
||||||
|
<tr key={idx} className="hover:bg-slate-700/20">
|
||||||
|
<td className="px-4 py-2 text-slate-300 font-medium whitespace-nowrap">
|
||||||
|
{change.header}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className="text-red-400/80 line-through decoration-red-500/50">
|
||||||
|
{String(change.old ?? '-')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
<span className="text-emerald-400 font-medium">
|
||||||
|
{String(change.new ?? '-')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-center justify-between px-2">
|
||||||
|
<div className="text-xs text-slate-500 flex items-center gap-1">
|
||||||
|
<Tag className="w-3 h-3" />
|
||||||
|
Row Index Reference: {entry.product_id}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 italic">
|
||||||
|
Reverting will move this record to 'Pending Validation' for final approval before re-syncing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ExcelRow, COLUMNS } from '../types';
|
||||||
|
import { Clock, Undo2, Package, Box, DollarSign, FileText, Search, Filter } from 'lucide-react';
|
||||||
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
|
interface PendingValidationViewProps {
|
||||||
|
data: ExcelRow[];
|
||||||
|
pendingRows: Record<string, { rowIndex: number; originalData: ExcelRow; newData: ExcelRow; articleName: string }>;
|
||||||
|
rowStatuses: Record<string, string>;
|
||||||
|
onRevertRow: (articleNo: string) => void;
|
||||||
|
onEdit: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PendingValidationView({ data, pendingRows, rowStatuses, onRevertRow, onEdit }: PendingValidationViewProps) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const pendingEntries = Object.entries(pendingRows);
|
||||||
|
|
||||||
|
const filteredEntries = search
|
||||||
|
? pendingEntries.filter(([articleNo, { articleName }]) =>
|
||||||
|
articleNo.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
articleName.toLowerCase().includes(search.toLowerCase())
|
||||||
|
)
|
||||||
|
: pendingEntries;
|
||||||
|
|
||||||
|
const getFieldDiff = (original: ExcelRow, updated: ExcelRow, colIndex: number) => {
|
||||||
|
const orig = original[colIndex];
|
||||||
|
const upd = updated[colIndex];
|
||||||
|
if (orig !== upd) {
|
||||||
|
return { from: String(orig ?? ''), to: String(upd ?? '') };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChangedFields = (original: ExcelRow, updated: ExcelRow) => {
|
||||||
|
const changes: { field: string; from: string; to: string }[] = [];
|
||||||
|
|
||||||
|
const fieldConfigs = [
|
||||||
|
{ col: COLUMNS.DETAILS_EN, label: 'Details EN' },
|
||||||
|
{ col: COLUMNS.DETAILS_DE, label: 'Details DE' },
|
||||||
|
{ col: COLUMNS.INNER_L, label: 'Inner L' },
|
||||||
|
{ col: COLUMNS.INNER_W, label: 'Inner W' },
|
||||||
|
{ col: COLUMNS.INNER_H, label: 'Inner H' },
|
||||||
|
{ col: COLUMNS.OUTER_L, label: 'Outer L' },
|
||||||
|
{ col: COLUMNS.OUTER_W, label: 'Outer W' },
|
||||||
|
{ col: COLUMNS.OUTER_H, label: 'Outer H' },
|
||||||
|
{ col: COLUMNS.UNITS_INNER, label: 'Units Inner' },
|
||||||
|
{ col: COLUMNS.UNITS_OUTER, label: 'Units Outer' },
|
||||||
|
{ col: COLUMNS.MOQ, label: 'MOQ' },
|
||||||
|
{ col: COLUMNS.BARCODE, label: 'Barcode' },
|
||||||
|
{ col: COLUMNS.TARIFF_CODE, label: 'Tariff Code' },
|
||||||
|
{ col: COLUMNS.COUNTRY_ORIGIN, label: 'Country' },
|
||||||
|
{ col: COLUMNS.RECOMMENDED_AGE, label: 'Recommended Age' },
|
||||||
|
];
|
||||||
|
|
||||||
|
fieldConfigs.forEach(({ col, label }) => {
|
||||||
|
const diff = getFieldDiff(original, updated, col);
|
||||||
|
if (diff) {
|
||||||
|
changes.push({ field: label, ...diff });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-slate-700">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Clock className="w-5 h-5 text-yellow-500" />
|
||||||
|
<h2 className="text-lg font-semibold text-white">Pending Validation</h2>
|
||||||
|
<span className="text-sm text-slate-500">
|
||||||
|
{pendingEntries.length} change{pendingEntries.length !== 1 ? 's' : ''} pending
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-4 h-4 text-slate-500 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search by article no or name..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="bg-slate-800 border border-slate-700 rounded-md pl-9 pr-3 py-2 text-sm text-white placeholder:text-slate-500 focus:outline-none focus:border-blue-500 w-64"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredEntries.length === 0 ? (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center text-slate-500">
|
||||||
|
<Clock className="w-16 h-16 mb-4 opacity-30" />
|
||||||
|
{search ? (
|
||||||
|
<p>No pending changes match your search</p>
|
||||||
|
) : (
|
||||||
|
<p>No pending validation changes</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||||
|
{filteredEntries.map(([articleNo, { rowIndex, originalData, newData, articleName }]) => {
|
||||||
|
const changes = getChangedFields(originalData, newData);
|
||||||
|
const status = rowStatuses[articleNo];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={articleNo}
|
||||||
|
className="border border-yellow-500/30 bg-yellow-500/5 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4 bg-yellow-500/10 border-b border-yellow-500/20">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="font-mono text-sm text-blue-400 bg-blue-400/10 px-2 py-1 rounded">
|
||||||
|
{articleNo}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-slate-300">{articleName}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onEdit(rowIndex)}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3" />
|
||||||
|
View/Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onRevertRow(articleNo)}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 bg-red-900/30 hover:bg-red-900/50 text-red-400 rounded text-xs font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<Undo2 className="w-3 h-3" />
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wider mb-3">
|
||||||
|
Changes ({changes.length})
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{changes.map((change, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-3 text-sm">
|
||||||
|
<span className="text-slate-400 w-28 shrink-0">{change.field}:</span>
|
||||||
|
<span className="text-red-400 line-through opacity-70">{change.from}</span>
|
||||||
|
<span className="text-slate-500">→</span>
|
||||||
|
<span className="text-green-400 font-medium">{change.to}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FileText, Table, Box, DollarSign, Package } from 'lucide-react';
|
import { FileText, Table, Box, DollarSign, Package, Clock, History } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
activeModule: string;
|
activeModule: string;
|
||||||
setActiveModule: (m: 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing') => void;
|
setActiveModule: (m: 'descriptions' | 'article_details' | 'matrix' | 'dimensions' | 'pricing' | 'pending_validation' | 'history') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ activeModule, setActiveModule }: SidebarProps) {
|
export function Sidebar({ activeModule, setActiveModule }: SidebarProps) {
|
||||||
@@ -14,6 +14,8 @@ export function Sidebar({ activeModule, setActiveModule }: SidebarProps) {
|
|||||||
{ id: 'article_details', label: 'Article Details', icon: Package },
|
{ id: 'article_details', label: 'Article Details', icon: Package },
|
||||||
{ id: 'dimensions', label: 'Dimensions', icon: Box },
|
{ id: 'dimensions', label: 'Dimensions', icon: Box },
|
||||||
{ id: 'pricing', label: 'Pricing & Units', icon: DollarSign },
|
{ id: 'pricing', label: 'Pricing & Units', icon: DollarSign },
|
||||||
|
{ id: 'pending_validation', label: 'Pending Validation', icon: Clock },
|
||||||
|
{ id: 'history', label: 'Change History', icon: History },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -109,3 +109,65 @@ export async function resetAllPendingRows(): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HistoryEntry {
|
||||||
|
id: string;
|
||||||
|
product_id: string;
|
||||||
|
article_name: string;
|
||||||
|
old_data: ExcelRow;
|
||||||
|
new_data: ExcelRow;
|
||||||
|
changed_at: string;
|
||||||
|
changed_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveHistoryEntry(
|
||||||
|
articleNo: string,
|
||||||
|
articleName: string,
|
||||||
|
oldData: ExcelRow,
|
||||||
|
newData: ExcelRow,
|
||||||
|
userEmail: 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
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving history to Supabase:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHistory(): Promise<HistoryEntry[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${SUPABASE_URL}/rest/v1/products_history?select=*&order=changed_at.desc&limit=100`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_KEY,
|
||||||
|
'Authorization': `Bearer ${SUPABASE_KEY}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) return [];
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching history from Supabase:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user