fix: starting amounts paid_from_other calculation + pay badge alignment on tracker
This commit is contained in:
parent
890427c75a
commit
b29d3a0b02
|
|
@ -73,6 +73,7 @@ export const api = {
|
||||||
runAdminCleanup: () => post('/admin/cleanup/run'),
|
runAdminCleanup: () => post('/admin/cleanup/run'),
|
||||||
seedDemoData: () => post('/user/seed-demo-data'),
|
seedDemoData: () => post('/user/seed-demo-data'),
|
||||||
clearDemoData: () => post('/user/clear-demo-data'),
|
clearDemoData: () => post('/user/clear-demo-data'),
|
||||||
|
seededStatus: () => get('/user/seeded-status'),
|
||||||
downloadAdminBackup: async (id) => {
|
downloadAdminBackup: async (id) => {
|
||||||
const res = await fetch(`/api/admin/backups/${encodeURIComponent(id)}/download`, {
|
const res = await fetch(`/api/admin/backups/${encodeURIComponent(id)}/download`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|
|
||||||
|
|
@ -1453,6 +1453,25 @@ function SeedDemoDataSection({ onSeeded }) {
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const [clearing, setClearing] = useState(false);
|
const [clearing, setClearing] = useState(false);
|
||||||
const [showClearConfirm, setShowClearConfirm] = 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 () => {
|
const handleSeed = async () => {
|
||||||
setLoading(true);
|
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 (
|
return (
|
||||||
<SectionCard title="Demo Data" subtitle="Seed your database with demo data for testing" icon={Sparkles}>
|
<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">
|
<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="mt-4 space-y-4">
|
||||||
<div className="border-t border-border pt-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'}
|
{loading ? <><Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />Seeding…</> : 'Seed Demo Data'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,17 @@ function calculatePaidDeductions(db, userId, year, month) {
|
||||||
AND b.due_day BETWEEN 15 AND 31
|
AND b.due_day BETWEEN 15 AND 31
|
||||||
`).get(userId, start, end);
|
`).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(`
|
const totalPaid = db.prepare(`
|
||||||
SELECT COALESCE(SUM(p.amount), 0) AS paid
|
SELECT COALESCE(SUM(p.amount), 0) AS paid
|
||||||
FROM payments p
|
FROM payments p
|
||||||
|
|
@ -74,6 +85,7 @@ function calculatePaidDeductions(db, userId, year, month) {
|
||||||
return {
|
return {
|
||||||
paid_from_first: money(firstPaid.paid),
|
paid_from_first: money(firstPaid.paid),
|
||||||
paid_from_fifteenth: money(fifteenthPaid.paid),
|
paid_from_fifteenth: money(fifteenthPaid.paid),
|
||||||
|
paid_from_other: money(otherPaid.paid),
|
||||||
paid_total: money(totalPaid.paid),
|
paid_total: money(totalPaid.paid),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -94,10 +106,11 @@ function buildStartingAmountsResponse(db, userId, year, month) {
|
||||||
combined_amount,
|
combined_amount,
|
||||||
paid_from_first: paid.paid_from_first,
|
paid_from_first: paid.paid_from_first,
|
||||||
paid_from_fifteenth: paid.paid_from_fifteenth,
|
paid_from_fifteenth: paid.paid_from_fifteenth,
|
||||||
|
paid_from_other: paid.paid_from_other,
|
||||||
paid_total,
|
paid_total,
|
||||||
first_remaining: amounts.first_amount - paid.paid_from_first,
|
first_remaining: amounts.first_amount - paid.paid_from_first,
|
||||||
fifteenth_remaining: amounts.fifteenth_amount - paid.paid_from_fifteenth,
|
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,
|
combined_remaining: combined_amount - paid_total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,17 @@ function calculatePaidDeductions(db, userId, year, month) {
|
||||||
AND b.due_day BETWEEN 15 AND 31
|
AND b.due_day BETWEEN 15 AND 31
|
||||||
`).get(userId, start, end);
|
`).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(`
|
const totalPaid = db.prepare(`
|
||||||
SELECT COALESCE(SUM(p.amount), 0) AS paid
|
SELECT COALESCE(SUM(p.amount), 0) AS paid
|
||||||
FROM payments p
|
FROM payments p
|
||||||
|
|
@ -76,6 +87,7 @@ function calculatePaidDeductions(db, userId, year, month) {
|
||||||
return {
|
return {
|
||||||
paid_from_first: money(firstPaid.paid),
|
paid_from_first: money(firstPaid.paid),
|
||||||
paid_from_fifteenth: money(fifteenthPaid.paid),
|
paid_from_fifteenth: money(fifteenthPaid.paid),
|
||||||
|
paid_from_other: money(otherPaid.paid),
|
||||||
paid_total: money(totalPaid.paid),
|
paid_total: money(totalPaid.paid),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -96,10 +108,11 @@ function buildStartingAmountsSummary(db, userId, year, month) {
|
||||||
combined_amount,
|
combined_amount,
|
||||||
paid_from_first: paid.paid_from_first,
|
paid_from_first: paid.paid_from_first,
|
||||||
paid_from_fifteenth: paid.paid_from_fifteenth,
|
paid_from_fifteenth: paid.paid_from_fifteenth,
|
||||||
|
paid_from_other: paid.paid_from_other,
|
||||||
paid_total,
|
paid_total,
|
||||||
first_remaining: amounts.first_amount - paid.paid_from_first,
|
first_remaining: amounts.first_amount - paid.paid_from_first,
|
||||||
fifteenth_remaining: amounts.fifteenth_amount - paid.paid_from_fifteenth,
|
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,
|
combined_remaining: combined_amount - paid_total,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,33 @@ const { getDb } = require('../db/database');
|
||||||
const { seedDemoData } = require('../scripts/seedDemoData');
|
const { seedDemoData } = require('../scripts/seedDemoData');
|
||||||
const { demoDataLimiter } = require('../middleware/rateLimiter');
|
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
|
// POST /api/user/clear-demo-data — removes all seeded bills and categories for the requesting user
|
||||||
router.post('/clear-demo-data', demoDataLimiter, (req, res) => {
|
router.post('/clear-demo-data', demoDataLimiter, (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue