diff --git a/client/pages/DataPage.jsx b/client/pages/DataPage.jsx index 2c8ff68..3ac0df1 100644 --- a/client/pages/DataPage.jsx +++ b/client/pages/DataPage.jsx @@ -1356,7 +1356,7 @@ export function ImportSpreadsheetSection({ onHistoryRefresh }) { // Collect created_at dates from duplicate detail entries so we can show // when the existing payments were originally recorded. 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)) .filter(d => !isNaN(d.getTime())) .sort((a, b) => a - b); @@ -1522,6 +1522,9 @@ export function ImportSpreadsheetSection({ onHistoryRefresh }) { setDecisions({}); setSelectedRows(new Set()); setApplyState({ status: 'idle', result: null, error: null }); + setViewMode('rows'); + setImportingBillId(null); + setBillImportResults(new Map()); if (fileRef.current) fileRef.current.value = ''; }; diff --git a/client/pages/SnowballPage.jsx b/client/pages/SnowballPage.jsx index 54ac29e..7ccaddb 100644 --- a/client/pages/SnowballPage.jsx +++ b/client/pages/SnowballPage.jsx @@ -283,16 +283,19 @@ function useSortable(items, setItems, setDirty) { const onPointerDown = useCallback((e, index) => { // 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 if (e.button !== undefined && e.button !== 0) return; - e.currentTarget.setPointerCapture(e.pointerId); - - const card = e.currentTarget.closest('[data-card]'); + const card = e.target.closest('[data-card]'); const list = card?.parentElement; 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 = { fromIdx: index, currentIdx: index, @@ -593,12 +596,14 @@ export default function SnowballPage() { data-card data-card-index={index} 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', // 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 - isLandTarget && 'ring-2 ring-primary/60 scale-[0.98] opacity-60', + isLandTarget && 'ring-2 ring-primary/40 scale-[0.97] opacity-50', )} >
diff --git a/services/spreadsheetImportService.js b/services/spreadsheetImportService.js index 7697c20..4dacabd 100644 --- a/services/spreadsheetImportService.js +++ b/services/spreadsheetImportService.js @@ -1401,18 +1401,18 @@ function createPaymentFromImport(db, billId, amount, paidDate, notes, allowOverw if (!paidDate || amount == null || amount <= 0) return null; 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 `).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(` INSERT INTO payments (bill_id, amount, paid_date, method, notes) VALUES (?, ?, ?, ?, ?) `).run(billId, amount, paidDate, null, notes); - return 'created'; + return { result: 'created', existing_created_at: null }; } function resolvePaymentInfo(decision, previewRow, amount) { @@ -1448,7 +1448,7 @@ function importRelatedPaidMonthsForNewBill(db, newBillId, billName, sourceRowId, const paymentResult = createPaymentFromImport(db, newBillId, paymentAmount, paymentDate, notes, allowOverwrite); if (paymentResult) { - detail.payment = paymentResult; + detail.payment = paymentResult.result; detail.paid_date = paymentDate; detail.payment_amount = paymentAmount; } @@ -1531,7 +1531,7 @@ function applyOneDecision(db, userId, decision, previewRow, sessionData, allowOv allowOverwrite, ); if (paymentResult) { - detail.payment = paymentResult; + detail.payment = paymentResult.result; detail.paid_date = paymentDate; detail.payment_amount = paymentAmount; } @@ -1588,9 +1588,13 @@ function applyOneDecision(db, userId, decision, previewRow, sessionData, allowOv allowOverwrite, ); if (paymentResult) { - detail.payment = paymentResult; + detail.payment = paymentResult.result; detail.paid_date = paymentDate; detail.payment_amount = paymentAmount; + if (paymentResult.result === 'skipped_duplicate') { + summary.duplicates++; + detail.existing_created_at = paymentResult.existing_created_at; + } } } summary.details.push(detail);