Files
Craze-Data-check/src/components/HistoryView.tsx
T

285 lines
12 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { History, RotateCcw, ChevronDown, ChevronRight, User, Calendar, Tag, Search, X, Edit2, Maximize2 } from 'lucide-react';
2026-04-10 09:47:35 +02:00
import { getHistory, deleteHistoryEntry, HistoryEntry } from '../lib/supabase';
import { ExcelRow } from '../types';
import { useColumns } from '../contexts/ColumnsContext';
import { cn } from '../lib/utils';
import { usePersistentState } from '../contexts/FilterContext';
interface HistoryViewProps {
headers: string[];
data: ExcelRow[];
onRevert: (articleNo: string, oldData: ExcelRow, historyId?: string) => void;
onEdit?: (rowIndex: number) => void;
}
export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProps) {
const COLUMNS = useColumns();
const [history, setHistory] = useState<HistoryEntry[]>([]);
const [loading, setLoading] = useState(true);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [search, setSearch] = usePersistentState('global-search', '');
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
loadHistory();
}, []);
useEffect(() => {
if (data.length > 0) {
loadHistory();
}
}, [data]);
const loadHistory = async () => {
setLoading(true);
const historyData = await getHistory();
setHistory(historyData);
setLoading(false);
};
2026-04-10 09:47:35 +02:00
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);
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 => {
const terms = search.toLowerCase().split(/\s+/).filter(Boolean);
if (terms.length === 0) return true;
const searchableText = `${entry.article_name} ${entry.product_id} ${entry.changed_by}`.toLowerCase();
return terms.every(term => searchableText.includes(term));
});
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={isFullscreen ? "fixed inset-0 z-40 bg-[#041021] p-6 overflow-auto" : "space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500"}>
{isFullscreen && (
<button
onClick={() => setIsFullscreen(false)}
className="fixed top-4 right-4 z-50 p-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md transition-colors border border-slate-700"
>
<X className="w-5 h-5" />
</button>
)}
<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-10 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"
/>
{search && (
<button
onClick={() => setSearch('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-white"
>
<X className="w-4 h-4" />
</button>
)}
</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>
<button
onClick={() => setIsFullscreen(!isFullscreen)}
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-md transition-colors border border-slate-700"
title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
>
<Maximize2 className="w-5 h-5" />
</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 (onEdit) {
const idx = data.findIndex(r => String(r[COLUMNS.ARTICLE_NO]) === entry.product_id);
if (idx !== -1) onEdit(idx);
}
}}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-blue-500/10 text-blue-400 hover:bg-blue-500/20 transition-colors border border-blue-500/20"
>
<Edit2 className="w-4 h-4" />
Edit
</button>
<button
onClick={(e) => {
e.stopPropagation();
2026-04-10 09:47:35 +02:00
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"
>
<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>
);
}