mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 11:55:23 +02:00
feat(history): add Discard selected button to cancel pending BC syncs
This commit is contained in:
@@ -15,13 +15,14 @@ interface HistoryViewProps {
|
|||||||
onEdit?: (rowIndex: number) => void;
|
onEdit?: (rowIndex: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HistorySyncStatus = 'bc_pending' | 'previewed' | 'syncing' | 'synced' | 'failed';
|
type HistorySyncStatus = 'bc_pending' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed';
|
||||||
|
|
||||||
interface HistorySyncRecord {
|
interface HistorySyncRecord {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
status: HistorySyncStatus;
|
status: HistorySyncStatus;
|
||||||
previewToken?: string;
|
previewToken?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
warning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HistorySyncMap = Record<string, HistorySyncRecord>;
|
type HistorySyncMap = Record<string, HistorySyncRecord>;
|
||||||
@@ -45,7 +46,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
const [search, setSearch] = usePersistentState('history-search', '');
|
const [search, setSearch] = usePersistentState('history-search', '');
|
||||||
const [userFilter, setUserFilter] = usePersistentState('history-userFilter', '');
|
const [userFilter, setUserFilter] = usePersistentState('history-userFilter', '');
|
||||||
const [statusFilter, setStatusFilter] = usePersistentState<'all' | 'bc_pending' | 'previewed' | 'synced' | 'failed'>('history-statusFilter', 'all');
|
const [statusFilter, setStatusFilter] = usePersistentState<'all' | 'bc_pending' | 'previewed' | 'preview_only' | 'synced' | 'failed'>('history-statusFilter', 'all');
|
||||||
const [bcHistorySync, setBcHistorySync] = useState<HistorySyncMap>(() => readStoredHistorySyncMap());
|
const [bcHistorySync, setBcHistorySync] = useState<HistorySyncMap>(() => readStoredHistorySyncMap());
|
||||||
const [bcSyncBusy, setBcSyncBusy] = useState(false);
|
const [bcSyncBusy, setBcSyncBusy] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
@@ -163,7 +164,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
);
|
);
|
||||||
|
|
||||||
const statusCounts = useMemo(() => {
|
const statusCounts = useMemo(() => {
|
||||||
const counts = { bc_pending: 0, previewed: 0, synced: 0, failed: 0 };
|
const counts = { bc_pending: 0, previewed: 0, preview_only: 0, synced: 0, failed: 0 };
|
||||||
history.forEach(entry => {
|
history.forEach(entry => {
|
||||||
const status = getEntryStatus(entry);
|
const status = getEntryStatus(entry);
|
||||||
if (status in counts) counts[status as keyof typeof counts] += 1;
|
if (status in counts) counts[status as keyof typeof counts] += 1;
|
||||||
@@ -196,12 +197,26 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
status: next[key]?.status ?? 'bc_pending',
|
status: next[key]?.status ?? 'bc_pending',
|
||||||
previewToken: next[key]?.previewToken,
|
previewToken: next[key]?.previewToken,
|
||||||
error: next[key]?.error,
|
error: next[key]?.error,
|
||||||
|
warning: next[key]?.warning,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const discardSelected = () => {
|
||||||
|
if (selectedEntries.length === 0) return;
|
||||||
|
if (!window.confirm(`Discard ${selectedEntries.length} pending sync(s)? They will be reset to "Pending BC" and will not be synced to Business Central.`)) return;
|
||||||
|
setBcHistorySync(prev => {
|
||||||
|
const next = { ...prev };
|
||||||
|
selectedEntries.forEach(entry => {
|
||||||
|
const key = getEntryKey(entry);
|
||||||
|
next[key] = { selected: false, status: 'bc_pending' };
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const previewSelected = async () => {
|
const previewSelected = async () => {
|
||||||
if (selectedEntries.length === 0) return;
|
if (selectedEntries.length === 0) return;
|
||||||
setBcSyncBusy(true);
|
setBcSyncBusy(true);
|
||||||
@@ -213,13 +228,14 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
})) {
|
})) {
|
||||||
const preview = await previewBusinessCentralSync(headers, entry.new_data);
|
const preview = await previewBusinessCentralSync(headers, entry.new_data);
|
||||||
if (!preview.success) {
|
if (!preview.success) {
|
||||||
updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', selected: true });
|
updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', warning: undefined, selected: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
updateEntry(entry, {
|
updateEntry(entry, {
|
||||||
status: 'previewed',
|
status: 'previewed',
|
||||||
previewToken: preview.previewToken,
|
previewToken: preview.previewToken,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
warning: undefined,
|
||||||
selected: true,
|
selected: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -247,11 +263,11 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
if (!previewToken) {
|
if (!previewToken) {
|
||||||
const preview = await previewBusinessCentralSync(headers, entry.new_data);
|
const preview = await previewBusinessCentralSync(headers, entry.new_data);
|
||||||
if (!preview.success) {
|
if (!preview.success) {
|
||||||
updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', selected: true });
|
updateEntry(entry, { status: 'failed', error: preview.error || 'Preview failed', warning: undefined, selected: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
previewToken = preview.previewToken;
|
previewToken = preview.previewToken;
|
||||||
updateEntry(entry, { status: 'previewed', previewToken, error: undefined, selected: true });
|
updateEntry(entry, { status: 'previewed', previewToken, error: undefined, warning: undefined, selected: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const apply = await applyBusinessCentralSync(headers, entry.new_data, previewToken);
|
const apply = await applyBusinessCentralSync(headers, entry.new_data, previewToken);
|
||||||
@@ -263,6 +279,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
status: 'previewed',
|
status: 'previewed',
|
||||||
previewToken: refreshedPreview.previewToken,
|
previewToken: refreshedPreview.previewToken,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
warning: undefined,
|
||||||
selected: true,
|
selected: true,
|
||||||
});
|
});
|
||||||
const retryApply = await applyBusinessCentralSync(headers, entry.new_data, refreshedPreview.previewToken);
|
const retryApply = await applyBusinessCentralSync(headers, entry.new_data, refreshedPreview.previewToken);
|
||||||
@@ -270,22 +287,24 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
updateEntry(entry, {
|
updateEntry(entry, {
|
||||||
status: 'synced',
|
status: 'synced',
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
warning: retryApply.warning,
|
||||||
selected: false,
|
selected: false,
|
||||||
previewToken: retryApply.previewToken || refreshedPreview.previewToken,
|
previewToken: retryApply.previewToken || refreshedPreview.previewToken,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
updateEntry(entry, { status: 'failed', error: retryApply.error || 'BC sync failed', selected: true });
|
updateEntry(entry, { status: 'failed', error: retryApply.error || 'BC sync failed', warning: retryApply.warning, selected: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateEntry(entry, { status: 'failed', error: apply.error || 'BC sync failed', selected: true });
|
updateEntry(entry, { status: 'failed', error: apply.error || 'BC sync failed', warning: apply.warning, selected: true });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEntry(entry, {
|
updateEntry(entry, {
|
||||||
status: 'synced',
|
status: 'synced',
|
||||||
error: undefined,
|
error: undefined,
|
||||||
|
warning: apply.warning,
|
||||||
selected: false,
|
selected: false,
|
||||||
previewToken: apply.previewToken || previewToken,
|
previewToken: apply.previewToken || previewToken,
|
||||||
});
|
});
|
||||||
@@ -392,6 +411,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
<span className="text-slate-600">·</span>
|
<span className="text-slate-600">·</span>
|
||||||
<span>{statusCounts.bc_pending} pending</span>
|
<span>{statusCounts.bc_pending} pending</span>
|
||||||
<span>{statusCounts.previewed} previewed</span>
|
<span>{statusCounts.previewed} previewed</span>
|
||||||
|
<span>{statusCounts.preview_only} preview only</span>
|
||||||
<span>{statusCounts.synced} synced</span>
|
<span>{statusCounts.synced} synced</span>
|
||||||
<span>{statusCounts.failed} failed</span>
|
<span>{statusCounts.failed} failed</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,6 +424,7 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
<option value="all">All statuses</option>
|
<option value="all">All statuses</option>
|
||||||
<option value="bc_pending">Pending BC</option>
|
<option value="bc_pending">Pending BC</option>
|
||||||
<option value="previewed">Previewed</option>
|
<option value="previewed">Previewed</option>
|
||||||
|
<option value="preview_only">Preview only</option>
|
||||||
<option value="synced">Synced BC</option>
|
<option value="synced">Synced BC</option>
|
||||||
<option value="failed">Failed</option>
|
<option value="failed">Failed</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -419,6 +440,18 @@ export function HistoryView({ headers, data, onRevert, onEdit }: HistoryViewProp
|
|||||||
>
|
>
|
||||||
Clear selection
|
Clear selection
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={discardSelected}
|
||||||
|
disabled={selectedEntries.length === 0}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-2 rounded-md text-xs border transition-colors",
|
||||||
|
selectedEntries.length === 0
|
||||||
|
? "bg-slate-700 text-slate-500 border-slate-600 cursor-not-allowed"
|
||||||
|
: "bg-slate-800 hover:bg-red-900/40 text-red-400 border-red-500/30 hover:border-red-500/60"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Discard selected
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={previewSelected}
|
onClick={previewSelected}
|
||||||
disabled={bcSyncBusy || selectedEntries.length === 0}
|
disabled={bcSyncBusy || selectedEntries.length === 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user