'use client';
import { useState, useEffect } from 'react';
import { AlertTriangle, Check, X, Edit, Search } from 'lucide-react';
interface Conflict {
id: number;
account_id: number;
account_name: string;
import_type: 'tradebook' | 'ledger';
conflict_type: string;
existing_data: any;
new_data: any;
conflict_field: string | null;
status: string;
created_at: string;
}
export default function ConflictsPage() {
const [conflicts, setConflicts] = useState<Conflict[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [selectedConflict, setSelectedConflict] = useState<Conflict | null>(null);
const [processing, setProcessing] = useState(false);
const [scanning, setScanning] = useState(false);
useEffect(() => {
fetchConflicts();
}, []);
const fetchConflicts = async () => {
try {
setLoading(true);
const response = await fetch('/api/conflicts');
const data = await response.json();
if (data.success) {
setConflicts(data.conflicts);
} else {
setError(data.error);
}
} catch (err: any) {
setError('Failed to fetch conflicts');
} finally {
setLoading(false);
}
};
const handleResolve = async (conflictId: number, action: string) => {
setProcessing(true);
setError(null);
try {
const response = await fetch(`/api/conflicts/${conflictId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
});
const data = await response.json();
if (data.success) {
await fetchConflicts();
setSelectedConflict(null);
} else {
setError(data.error);
}
} catch (err: any) {
setError('Failed to resolve conflict');
} finally {
setProcessing(false);
}
};
const handleDelete = async (conflictId: number) => {
if (!confirm('Are you sure you want to delete this conflict?')) {
return;
}
setProcessing(true);
try {
const response = await fetch(`/api/conflicts/${conflictId}`, {
method: 'DELETE',
});
const data = await response.json();
if (data.success) {
await fetchConflicts();
} else {
setError(data.error);
}
} catch (err: any) {
setError('Failed to delete conflict');
} finally {
setProcessing(false);
}
};
const handleScanDuplicates = async (accountId: string, importType: string) => {
setScanning(true);
setError(null);
setSuccess(null);
try {
const response = await fetch('/api/conflicts/scan-existing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountId, importType }),
});
const data = await response.json();
if (data.success) {
setSuccess(data.message);
await fetchConflicts();
} else {
setError(data.error);
}
} catch (err: any) {
setError('Failed to scan for duplicates');
} finally {
setScanning(false);
}
};
const renderDataComparison = (conflict: Conflict) => {
const existing = conflict.existing_data;
const newData = conflict.new_data;
const isExactDuplicate = conflict.conflict_type === 'exact_duplicate' || conflict.conflict_type === 'exact_duplicate_existing';
const isExistingDuplicate = conflict.conflict_type.includes('_existing');
if (conflict.import_type === 'tradebook') {
return (
<>
{isExactDuplicate && (
<div className="mb-4 p-3 bg-blue-100 border border-blue-300 rounded text-sm">
<p className="text-blue-900 font-medium">⚠️ Exact Duplicate Entry</p>
<p className="text-blue-800 mt-1">
{isExistingDuplicate
? 'This trade exists multiple times in your database. This duplicate was found during a database scan.'
: 'This trade already exists in the database with identical data. You may have reimported the same file.'}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="bg-yellow-50 p-4 rounded">
<h4 className="font-semibold text-sm mb-2">
{isExistingDuplicate ? 'First Entry (Keep)' : 'Existing Data'}
</h4>
<div className="text-sm space-y-1">
<p><span className="font-medium">Symbol:</span> {existing.symbol}</p>
<p><span className="font-medium">Quantity:</span> {existing.quantity}</p>
<p><span className="font-medium">Price:</span> ₹{existing.price}</p>
<p><span className="font-medium">Trade ID:</span> {existing.trade_id}</p>
<p><span className="font-medium">Date:</span> {new Date(existing.trade_date).toLocaleDateString()}</p>
{isExistingDuplicate && <p className="text-xs text-gray-600 mt-2">Database ID: {existing.id}</p>}
</div>
</div>
<div className={`${isExistingDuplicate ? 'bg-red-50' : 'bg-blue-50'} p-4 rounded`}>
<h4 className="font-semibold text-sm mb-2">
{isExistingDuplicate ? 'Duplicate Entry (Delete)' : 'New Data (CSV)'}
</h4>
<div className="text-sm space-y-1">
<p><span className="font-medium">Symbol:</span> {newData.symbol}</p>
<p><span className="font-medium">Quantity:</span> {newData.quantity}</p>
<p><span className="font-medium">Price:</span> ₹{newData.price}</p>
<p><span className="font-medium">Trade ID:</span> {newData.trade_id}</p>
<p><span className="font-medium">Date:</span> {newData.trade_date}</p>
{isExistingDuplicate && <p className="text-xs text-gray-600 mt-2">Database ID: {newData.id}</p>}
</div>
</div>
</div>
</>
);
} else {
return (
<>
{isExactDuplicate && (
<div className="mb-4 p-3 bg-blue-100 border border-blue-300 rounded text-sm">
<p className="text-blue-900 font-medium">⚠️ Exact Duplicate Entry</p>
<p className="text-blue-800 mt-1">
{isExistingDuplicate
? 'This ledger entry exists multiple times in your database. This duplicate was found during a database scan.'
: 'This ledger entry already exists in the database with identical data. You may have reimported the same file.'}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="bg-yellow-50 p-4 rounded">
<h4 className="font-semibold text-sm mb-2">
{isExistingDuplicate ? 'First Entry (Keep)' : 'Existing Data'}
</h4>
<div className="text-sm space-y-1">
<p><span className="font-medium">Date:</span> {new Date(existing.posting_date).toLocaleDateString()}</p>
<p><span className="font-medium">Particular:</span> {existing.particular || 'N/A'}</p>
<p><span className="font-medium">Debit:</span> ₹{existing.debit}</p>
<p><span className="font-medium">Credit:</span> ₹{existing.credit}</p>
<p><span className="font-medium">Balance:</span> ₹{existing.net_balance}</p>
{isExistingDuplicate && <p className="text-xs text-gray-600 mt-2">Database ID: {existing.id}</p>}
</div>
</div>
<div className={`${isExistingDuplicate ? 'bg-red-50' : 'bg-blue-50'} p-4 rounded`}>
<h4 className="font-semibold text-sm mb-2">
{isExistingDuplicate ? 'Duplicate Entry (Delete)' : 'New Data (CSV)'}
</h4>
<div className="text-sm space-y-1">
<p><span className="font-medium">Date:</span> {newData.posting_date}</p>
<p><span className="font-medium">Particular:</span> {newData.particular || 'N/A'}</p>
<p><span className="font-medium">Debit:</span> ₹{newData.debit || 0}</p>
<p><span className="font-medium">Credit:</span> ₹{newData.credit || 0}</p>
<p><span className="font-medium">Balance:</span> ₹{newData.net_balance}</p>
{isExistingDuplicate && <p className="text-xs text-gray-600 mt-2">Database ID: {newData.id}</p>}
</div>
</div>
</div>
</>
);
}
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-6xl mx-auto">
<p className="text-gray-700">Loading conflicts...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Import Conflicts</h1>
<p className="text-gray-700">
Review and resolve data conflicts detected during CSV imports
</p>
</div>
{/* Scan for Existing Duplicates Section */}
<div className="mb-6 bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-3">Scan Database for Duplicates</h2>
<p className="text-sm text-gray-700 mb-4">
Scan your database to find existing duplicate entries that may have been imported before conflict detection was enabled.
</p>
<div className="flex gap-4">
<ScanDuplicatesForm onScan={handleScanDuplicates} scanning={scanning} />
</div>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">
{error}
</div>
)}
{success && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700">
{success}
</div>
)}
{conflicts.length === 0 ? (
<div className="bg-white rounded-lg shadow p-12 text-center">
<Check className="h-16 w-16 text-green-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">
No Conflicts Found
</h2>
<p className="text-gray-700">
All your imports are clean! No conflicts to resolve.
</p>
</div>
) : (
<div className="space-y-4">
{conflicts.map((conflict) => (
<div key={conflict.id} className="bg-white rounded-lg shadow overflow-hidden">
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<AlertTriangle className={`h-6 w-6 ${conflict.conflict_type === 'exact_duplicate' ? 'text-blue-500' : 'text-yellow-500'}`} />
<div>
<h3 className="font-semibold text-lg">
{conflict.import_type === 'tradebook' ? 'Tradebook' : 'Ledger'} Conflict
</h3>
<p className="text-sm text-gray-700">
Account: {conflict.account_name} • Type: {
conflict.conflict_type === 'exact_duplicate' ? 'Exact Duplicate (Reimport)' :
conflict.conflict_type === 'exact_duplicate_existing' ? 'Exact Duplicate (Already in DB)' :
conflict.conflict_type === 'duplicate_trade_id' ? 'Duplicate Trade ID (Different Data)' :
conflict.conflict_type === 'duplicate_trade_id_existing' ? 'Duplicate Trade ID (Already in DB)' :
conflict.conflict_type === 'duplicate_entry_different_amount' ? 'Duplicate Entry (Different Amount)' :
conflict.conflict_type
}
</p>
<p className="text-xs text-gray-700 mt-1">
Detected: {new Date(conflict.created_at).toLocaleString()}
</p>
</div>
</div>
</div>
{renderDataComparison(conflict)}
{(conflict.conflict_type === 'exact_duplicate' || conflict.conflict_type === 'exact_duplicate_existing') && (
<div className="mt-4 p-3 bg-gray-50 border border-gray-200 rounded text-sm text-gray-700">
<p><strong>Recommendation:</strong> Since this is an exact duplicate, you can safely click "{conflict.conflict_type.includes('_existing') ? 'Delete Duplicate' : 'Keep Existing'}" to clean up the duplicate entry.</p>
</div>
)}
<div className="mt-6 flex gap-2">
{conflict.conflict_type.includes('_existing') ? (
<>
<button
onClick={() => handleResolve(conflict.id, 'keep_existing')}
disabled={processing}
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 disabled:bg-gray-300 flex items-center gap-2"
>
<Check className="h-4 w-4" />
Keep First Entry
</button>
<button
onClick={() => handleResolve(conflict.id, 'use_new')}
disabled={processing}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:bg-gray-300 flex items-center gap-2"
>
<X className="h-4 w-4" />
Delete Duplicate
</button>
</>
) : (
<>
<button
onClick={() => handleResolve(conflict.id, 'keep_existing')}
disabled={processing}
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 disabled:bg-gray-300 flex items-center gap-2"
>
<X className="h-4 w-4" />
Keep Existing
</button>
<button
onClick={() => handleResolve(conflict.id, 'use_new')}
disabled={processing}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 flex items-center gap-2"
>
<Check className="h-4 w-4" />
Use New (CSV)
</button>
</>
)}
<button
onClick={() => handleResolve(conflict.id, 'ignore')}
disabled={processing}
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 disabled:bg-gray-300"
>
Ignore
</button>
<button
onClick={() => handleDelete(conflict.id)}
disabled={processing}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:bg-gray-300 ml-auto"
>
Delete Conflict
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
// Scan Duplicates Form Component
function ScanDuplicatesForm({ onScan, scanning }: { onScan: (accountId: string, importType: string) => void, scanning: boolean }) {
const [accounts, setAccounts] = useState<any[]>([]);
const [selectedAccount, setSelectedAccount] = useState('');
const [selectedType, setSelectedType] = useState('tradebook');
const [loadingAccounts, setLoadingAccounts] = useState(true);
useEffect(() => {
fetchAccounts();
}, []);
const fetchAccounts = async () => {
try {
const response = await fetch('/api/accounts');
const data = await response.json();
if (data.success) {
setAccounts(data.accounts);
if (data.accounts.length > 0) {
setSelectedAccount(data.accounts[0].id.toString());
}
}
} catch (err) {
console.error('Failed to fetch accounts:', err);
} finally {
setLoadingAccounts(false);
}
};
const handleScan = () => {
if (selectedAccount) {
onScan(selectedAccount, selectedType);
}
};
if (loadingAccounts) {
return <p className="text-sm text-gray-600">Loading accounts...</p>;
}
if (accounts.length === 0) {
return <p className="text-sm text-gray-600">No accounts found. Please add an account first.</p>;
}
return (
<div className="flex items-end gap-4 w-full">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Account
</label>
<select
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={scanning}
>
{accounts.map((account) => (
<option key={account.id} value={account.id}>
{account.account_name} ({account.broker})
</option>
))}
</select>
</div>
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">
Type
</label>
<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={scanning}
>
<option value="tradebook">Tradebook</option>
<option value="ledger">Ledger</option>
</select>
</div>
<button
onClick={handleScan}
disabled={scanning || !selectedAccount}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-2"
>
<Search className="h-4 w-4" />
{scanning ? 'Scanning...' : 'Scan for Duplicates'}
</button>
</div>
);
}