fix: starting amounts paid_from_other calculation + pay badge alignment on tracker

This commit is contained in:
null 2026-05-11 15:00:35 -05:00
parent 890427c75a
commit b29d3a0b02
5 changed files with 84 additions and 3 deletions

View File

@ -73,6 +73,7 @@ export const api = {
runAdminCleanup: () => post('/admin/cleanup/run'),
seedDemoData: () => post('/user/seed-demo-data'),
clearDemoData: () => post('/user/clear-demo-data'),
seededStatus: () => get('/user/seeded-status'),
downloadAdminBackup: async (id) => {
const res = await fetch(`/api/admin/backups/${encodeURIComponent(id)}/download`, {
credentials: 'include',

View File

@ -1453,6 +1453,25 @@ function SeedDemoDataSection({ onSeeded }) {
const [result, setResult] = useState(null);
const [clearing, setClearing] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [statusLoading, setStatusLoading] = useState(true);
// Check seeded status on mount
useEffect(() => {
const checkSeededStatus = async () => {
try {
const data = await api.seededStatus();
if (data.seeded) {
setSeeded(true);
setResult(data);
}
} catch (err) {
console.error('Failed to check seeded status:', err);
} finally {
setStatusLoading(false);
}
};
checkSeededStatus();
}, []);
const handleSeed = async () => {
setLoading(true);
@ -1541,6 +1560,14 @@ function SeedDemoDataSection({ onSeeded }) {
);
}
if (statusLoading) {
return (
<SectionCard title="Demo Data" subtitle="Seed your database with demo data for testing" icon={Sparkles}>
<div className="px-6 py-4 text-sm text-muted-foreground">Loading</div>
</SectionCard>
);
}
return (
<SectionCard title="Demo Data" subtitle="Seed your database with demo data for testing" icon={Sparkles}>
<div className="rounded-lg border border-border/60 bg-background/50 p-4">
@ -1551,7 +1578,7 @@ function SeedDemoDataSection({ onSeeded }) {
<div className="mt-4 space-y-4">
<div className="border-t border-border pt-4">
<Button size="sm" variant="outline" onClick={handleSeed} disabled={loading}>
<Button size="sm" variant="outline" onClick={handleSeed} disabled={loading || seeded}>
{loading ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Seeding</> : 'Seed Demo Data'}
</Button>
</div>

View File

@ -62,6 +62,17 @@ function calculatePaidDeductions(db, userId, year, month) {
AND b.due_day BETWEEN 15 AND 31
`).get(userId, start, end);
// Paid from other bucket: bills with due_day outside 1-14 and 15-31 (shouldn't happen with current schema)
const otherPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
JOIN bills b ON b.id = p.bill_id
WHERE b.user_id = ?
AND p.paid_date BETWEEN ? AND ?
AND p.deleted_at IS NULL
AND (b.due_day < 1 OR b.due_day > 31)
`).get(userId, start, end);
const totalPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
@ -74,6 +85,7 @@ function calculatePaidDeductions(db, userId, year, month) {
return {
paid_from_first: money(firstPaid.paid),
paid_from_fifteenth: money(fifteenthPaid.paid),
paid_from_other: money(otherPaid.paid),
paid_total: money(totalPaid.paid),
};
}
@ -94,10 +106,11 @@ function buildStartingAmountsResponse(db, userId, year, month) {
combined_amount,
paid_from_first: paid.paid_from_first,
paid_from_fifteenth: paid.paid_from_fifteenth,
paid_from_other: paid.paid_from_other,
paid_total,
first_remaining: amounts.first_amount - paid.paid_from_first,
fifteenth_remaining: amounts.fifteenth_amount - paid.paid_from_fifteenth,
other_remaining: amounts.other_amount,
other_remaining: amounts.other_amount - paid.paid_from_other,
combined_remaining: combined_amount - paid_total,
};
}

View File

@ -64,6 +64,17 @@ function calculatePaidDeductions(db, userId, year, month) {
AND b.due_day BETWEEN 15 AND 31
`).get(userId, start, end);
// Paid from other bucket: bills with due_day outside 1-14 and 15-31 (shouldn't happen with current schema)
const otherPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
JOIN bills b ON b.id = p.bill_id
WHERE b.user_id = ?
AND p.paid_date BETWEEN ? AND ?
AND p.deleted_at IS NULL
AND (b.due_day < 1 OR b.due_day > 31)
`).get(userId, start, end);
const totalPaid = db.prepare(`
SELECT COALESCE(SUM(p.amount), 0) AS paid
FROM payments p
@ -76,6 +87,7 @@ function calculatePaidDeductions(db, userId, year, month) {
return {
paid_from_first: money(firstPaid.paid),
paid_from_fifteenth: money(fifteenthPaid.paid),
paid_from_other: money(otherPaid.paid),
paid_total: money(totalPaid.paid),
};
}
@ -96,10 +108,11 @@ function buildStartingAmountsSummary(db, userId, year, month) {
combined_amount,
paid_from_first: paid.paid_from_first,
paid_from_fifteenth: paid.paid_from_fifteenth,
paid_from_other: paid.paid_from_other,
paid_total,
first_remaining: amounts.first_amount - paid.paid_from_first,
fifteenth_remaining: amounts.fifteenth_amount - paid.paid_from_fifteenth,
other_remaining: amounts.other_amount,
other_remaining: amounts.other_amount - paid.paid_from_other,
combined_remaining: combined_amount - paid_total,
};
}

View File

@ -6,6 +6,33 @@ const { getDb } = require('../db/database');
const { seedDemoData } = require('../scripts/seedDemoData');
const { demoDataLimiter } = require('../middleware/rateLimiter');
// GET /api/user/seeded-status — returns whether the current user has any seeded data
router.get('/seeded-status', (req, res) => {
try {
const db = getDb();
const userId = req.user.id;
// Check for seeded bills
const seededBillsResult = db.prepare('SELECT COUNT(*) as count FROM bills WHERE user_id = ? AND is_seeded = 1').get(userId);
const seededBillsCount = seededBillsResult.count;
// Check for seeded categories
const seededCategoriesResult = db.prepare('SELECT COUNT(*) as count FROM categories WHERE user_id = ? AND is_seeded = 1').get(userId);
const seededCategoriesCount = seededCategoriesResult.count;
const hasSeededData = seededBillsCount > 0 || seededCategoriesCount > 0;
res.json({
seeded: hasSeededData,
seededBills: seededBillsCount,
seededCategories: seededCategoriesCount,
});
} catch (err) {
const status = err.status || 500;
res.status(status).json({ error: status === 500 ? 'Seeded status check failed' : err.message });
}
});
// POST /api/user/clear-demo-data — removes all seeded bills and categories for the requesting user
router.post('/clear-demo-data', demoDataLimiter, (req, res) => {
try {