v0.27.04
This commit is contained in:
parent
153ed7ab79
commit
74603ff2d5
|
|
@ -1356,7 +1356,7 @@ export function ImportSpreadsheetSection({ onHistoryRefresh }) {
|
||||||
// Collect created_at dates from duplicate detail entries so we can show
|
// Collect created_at dates from duplicate detail entries so we can show
|
||||||
// when the existing payments were originally recorded.
|
// when the existing payments were originally recorded.
|
||||||
const dupDates = (result.details ?? [])
|
const dupDates = (result.details ?? [])
|
||||||
.filter(d => d.result === 'skipped_duplicate' && d.existing_created_at)
|
.filter(d => (d.result === 'skipped_duplicate' || d.payment === 'skipped_duplicate') && d.existing_created_at)
|
||||||
.map(d => new Date(d.existing_created_at))
|
.map(d => new Date(d.existing_created_at))
|
||||||
.filter(d => !isNaN(d.getTime()))
|
.filter(d => !isNaN(d.getTime()))
|
||||||
.sort((a, b) => a - b);
|
.sort((a, b) => a - b);
|
||||||
|
|
@ -1522,6 +1522,9 @@ export function ImportSpreadsheetSection({ onHistoryRefresh }) {
|
||||||
setDecisions({});
|
setDecisions({});
|
||||||
setSelectedRows(new Set());
|
setSelectedRows(new Set());
|
||||||
setApplyState({ status: 'idle', result: null, error: null });
|
setApplyState({ status: 'idle', result: null, error: null });
|
||||||
|
setViewMode('rows');
|
||||||
|
setImportingBillId(null);
|
||||||
|
setBillImportResults(new Map());
|
||||||
if (fileRef.current) fileRef.current.value = '';
|
if (fileRef.current) fileRef.current.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -283,16 +283,19 @@ function useSortable(items, setItems, setDirty) {
|
||||||
|
|
||||||
const onPointerDown = useCallback((e, index) => {
|
const onPointerDown = useCallback((e, index) => {
|
||||||
// Only trigger on the grip handle (data-grip attr)
|
// Only trigger on the grip handle (data-grip attr)
|
||||||
if (!e.currentTarget.hasAttribute('data-grip')) return;
|
if (!e.target.closest('[data-grip]')) return;
|
||||||
// Ignore right-click
|
// Ignore right-click
|
||||||
if (e.button !== undefined && e.button !== 0) return;
|
if (e.button !== undefined && e.button !== 0) return;
|
||||||
|
|
||||||
e.currentTarget.setPointerCapture(e.pointerId);
|
const card = e.target.closest('[data-card]');
|
||||||
|
|
||||||
const card = e.currentTarget.closest('[data-card]');
|
|
||||||
const list = card?.parentElement;
|
const list = card?.parentElement;
|
||||||
const rect = card?.getBoundingClientRect();
|
const rect = card?.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Capture on the container so pointermove/pointerup are dispatched
|
||||||
|
// directly to the element that owns those React handlers — avoids
|
||||||
|
// relying on bubbling from the grip through React's delegation chain.
|
||||||
|
list?.setPointerCapture(e.pointerId);
|
||||||
|
|
||||||
state.current = {
|
state.current = {
|
||||||
fromIdx: index,
|
fromIdx: index,
|
||||||
currentIdx: index,
|
currentIdx: index,
|
||||||
|
|
@ -593,12 +596,14 @@ export default function SnowballPage() {
|
||||||
data-card
|
data-card
|
||||||
data-card-index={index}
|
data-card-index={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'surface-elevated rounded-xl border transition-all duration-150 select-none touch-none',
|
'surface-elevated rounded-xl border select-none touch-none',
|
||||||
|
// Only animate when not in a drag gesture — instant feedback on grab
|
||||||
|
!isDragging && 'transition-all duration-150',
|
||||||
isAttack ? 'border-emerald-500/30 bg-emerald-950/5' : 'border-border/40',
|
isAttack ? 'border-emerald-500/30 bg-emerald-950/5' : 'border-border/40',
|
||||||
// Card being actively dragged — lifted look
|
// Card being actively dragged — lifted look
|
||||||
isDragSource && 'scale-[1.03] shadow-2xl ring-2 ring-primary/40 opacity-80 relative z-10',
|
isDragSource && 'scale-105 shadow-2xl ring-2 ring-primary/60 opacity-75 relative z-10',
|
||||||
// Where the card will land — slot highlight
|
// Where the card will land — slot highlight
|
||||||
isLandTarget && 'ring-2 ring-primary/60 scale-[0.98] opacity-60',
|
isLandTarget && 'ring-2 ring-primary/40 scale-[0.97] opacity-50',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-stretch">
|
<div className="flex items-stretch">
|
||||||
|
|
|
||||||
|
|
@ -1401,18 +1401,18 @@ function createPaymentFromImport(db, billId, amount, paidDate, notes, allowOverw
|
||||||
if (!paidDate || amount == null || amount <= 0) return null;
|
if (!paidDate || amount == null || amount <= 0) return null;
|
||||||
|
|
||||||
const dup = db.prepare(`
|
const dup = db.prepare(`
|
||||||
SELECT id FROM payments
|
SELECT id, created_at FROM payments
|
||||||
WHERE bill_id = ? AND paid_date = ? AND amount = ? AND deleted_at IS NULL
|
WHERE bill_id = ? AND paid_date = ? AND amount = ? AND deleted_at IS NULL
|
||||||
`).get(billId, paidDate, amount);
|
`).get(billId, paidDate, amount);
|
||||||
|
|
||||||
if (dup && !allowOverwrite) return 'skipped_duplicate';
|
if (dup && !allowOverwrite) return { result: 'skipped_duplicate', existing_created_at: dup.created_at ?? null };
|
||||||
|
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO payments (bill_id, amount, paid_date, method, notes)
|
INSERT INTO payments (bill_id, amount, paid_date, method, notes)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
`).run(billId, amount, paidDate, null, notes);
|
`).run(billId, amount, paidDate, null, notes);
|
||||||
|
|
||||||
return 'created';
|
return { result: 'created', existing_created_at: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePaymentInfo(decision, previewRow, amount) {
|
function resolvePaymentInfo(decision, previewRow, amount) {
|
||||||
|
|
@ -1448,7 +1448,7 @@ function importRelatedPaidMonthsForNewBill(db, newBillId, billName, sourceRowId,
|
||||||
|
|
||||||
const paymentResult = createPaymentFromImport(db, newBillId, paymentAmount, paymentDate, notes, allowOverwrite);
|
const paymentResult = createPaymentFromImport(db, newBillId, paymentAmount, paymentDate, notes, allowOverwrite);
|
||||||
if (paymentResult) {
|
if (paymentResult) {
|
||||||
detail.payment = paymentResult;
|
detail.payment = paymentResult.result;
|
||||||
detail.paid_date = paymentDate;
|
detail.paid_date = paymentDate;
|
||||||
detail.payment_amount = paymentAmount;
|
detail.payment_amount = paymentAmount;
|
||||||
}
|
}
|
||||||
|
|
@ -1531,7 +1531,7 @@ function applyOneDecision(db, userId, decision, previewRow, sessionData, allowOv
|
||||||
allowOverwrite,
|
allowOverwrite,
|
||||||
);
|
);
|
||||||
if (paymentResult) {
|
if (paymentResult) {
|
||||||
detail.payment = paymentResult;
|
detail.payment = paymentResult.result;
|
||||||
detail.paid_date = paymentDate;
|
detail.paid_date = paymentDate;
|
||||||
detail.payment_amount = paymentAmount;
|
detail.payment_amount = paymentAmount;
|
||||||
}
|
}
|
||||||
|
|
@ -1588,9 +1588,13 @@ function applyOneDecision(db, userId, decision, previewRow, sessionData, allowOv
|
||||||
allowOverwrite,
|
allowOverwrite,
|
||||||
);
|
);
|
||||||
if (paymentResult) {
|
if (paymentResult) {
|
||||||
detail.payment = paymentResult;
|
detail.payment = paymentResult.result;
|
||||||
detail.paid_date = paymentDate;
|
detail.paid_date = paymentDate;
|
||||||
detail.payment_amount = paymentAmount;
|
detail.payment_amount = paymentAmount;
|
||||||
|
if (paymentResult.result === 'skipped_duplicate') {
|
||||||
|
summary.duplicates++;
|
||||||
|
detail.existing_created_at = paymentResult.existing_created_at;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
summary.details.push(detail);
|
summary.details.push(detail);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue