feat: replace native confirm() with shadcn/ui AlertDialog (v0.23.3)
- TrackerPage: confirm('Mark as paid?') → AlertDialog with dynamic bill name
- DataPage: window.confirm('Import SQLite?') → AlertDialog for import confirmation
- Both dialogs use proper shadcn/ui components (AlertDialogAction/Cancel)
- Theme-aware, accessible, consistent with app design system
- STRUCTURE.md: corrected tech stack (Vite+React, not Next.js)
- Version bumped to 0.23.3
This commit is contained in:
parent
7c3cfd1715
commit
5eed5932b4
24
STRUCTURE.md
24
STRUCTURE.md
|
|
@ -173,9 +173,10 @@ Responsibilities:
|
||||||
|
|
||||||
Technology Focus:
|
Technology Focus:
|
||||||
|
|
||||||
* React and Next.js App Router are primary.
|
* **React with Vite** is the frontend framework (NOT Next.js — never suggest Next.js patterns).
|
||||||
* Tailwind CSS must be used predictably to maintain consistency.
|
* **Tailwind CSS** must be used predictably to maintain consistency.
|
||||||
* shadcn/ui must be the foundational component library.
|
* **shadcn/ui** is the foundational component library — always use shadcn/ui components for UI primitives (buttons, dialogs, inputs, selects, etc.). Do not build custom components when shadcn/ui provides one.
|
||||||
|
* **Sonner** is used for toast notifications.
|
||||||
|
|
||||||
Authority:
|
Authority:
|
||||||
|
|
||||||
|
|
@ -250,13 +251,18 @@ Authority:
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
Base stack:
|
Bill Tracker actual stack:
|
||||||
|
|
||||||
* Next.js App Router
|
* **Vite** (build tool, NOT Next.js)
|
||||||
* React
|
* **React** (SPA, client-side routing via React Router)
|
||||||
* Tailwind CSS
|
* **Tailwind CSS** (utility-first styling)
|
||||||
* shadcn/ui
|
* **shadcn/ui** (component primitives — buttons, dialogs, inputs, etc.)
|
||||||
* SQLite
|
* **Sonner** (toast notifications)
|
||||||
|
* **TanStack Query** (server state management)
|
||||||
|
* **better-sqlite3** (database)
|
||||||
|
* **Express** (backend)
|
||||||
|
|
||||||
|
⚠️ **This project does NOT use Next.js.** Do not suggest Next.js patterns (App Router, server components, etc.).
|
||||||
|
|
||||||
Development target:
|
Development target:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
export const APP_VERSION = '0.23.2';
|
export const APP_VERSION = '0.23.3';
|
||||||
export const APP_NAME = 'BillTracker';
|
export const APP_NAME = 'BillTracker';
|
||||||
|
|
||||||
export const RELEASE_NOTES = {
|
export const RELEASE_NOTES = {
|
||||||
version: '0.23.2',
|
version: '0.23.3',
|
||||||
date: '2026-05-10',
|
date: '2026-05-10',
|
||||||
highlights: [
|
highlights: [
|
||||||
{ icon: '🔒', title: 'Critical: Notification Privacy Leak Fix', desc: 'In per-user notification mode, bills were sent to all opted-in recipients regardless of ownership. Now each recipient only receives their own bills.' },
|
{ icon: '✅', title: 'AlertDialog Integration', desc: 'Replaced native confirm() calls with shadcn/ui AlertDialog for consistent UI across tracker and data pages.' },
|
||||||
{ icon: '🛡️', title: 'Orphaned Bill Guard', desc: 'Defensive check added: bills with no user_id are now skipped with a warning instead of being broadcast to all recipients.' },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -12,6 +12,7 @@ import { Input } from '@/components/ui/input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
|
|
@ -329,6 +330,7 @@ export function ImportMyDataSection({ onHistoryRefresh }) {
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
const [preview, setPreview] = useState({ status: 'idle', data: null, error: null });
|
const [preview, setPreview] = useState({ status: 'idle', data: null, error: null });
|
||||||
const [applyState, setApplyState] = useState({ status: 'idle', result: null, error: null });
|
const [applyState, setApplyState] = useState({ status: 'idle', result: null, error: null });
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setFile(null);
|
setFile(null);
|
||||||
|
|
@ -354,10 +356,13 @@ export function ImportMyDataSection({ onHistoryRefresh }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApply = async () => {
|
const handleApply = () => {
|
||||||
if (!preview.data?.import_session_id) return;
|
if (!preview.data?.import_session_id) return;
|
||||||
const ok = window.confirm('Import this SQLite data export into your account? Existing records will be skipped by default.');
|
setConfirmOpen(true);
|
||||||
if (!ok) return;
|
};
|
||||||
|
|
||||||
|
const handleConfirmImport = async () => {
|
||||||
|
setConfirmOpen(false);
|
||||||
setApplyState({ status: 'loading', result: null, error: null });
|
setApplyState({ status: 'loading', result: null, error: null });
|
||||||
try {
|
try {
|
||||||
const result = await api.applyUserDbImport({
|
const result = await api.applyUserDbImport({
|
||||||
|
|
@ -377,8 +382,9 @@ export function ImportMyDataSection({ onHistoryRefresh }) {
|
||||||
const summary = preview.data?.summary || {};
|
const summary = preview.data?.summary || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionCard title="Import My Data Export"
|
<>
|
||||||
subtitle="Restore data from a SQLite export created by this app for your account.">
|
<SectionCard title="Import My Data Export"
|
||||||
|
subtitle="Restore data from a SQLite export created by this app for your account.">
|
||||||
<div className="px-6 py-5">
|
<div className="px-6 py-5">
|
||||||
<div className="rounded-lg border border-border/60 bg-muted/25 p-4">
|
<div className="rounded-lg border border-border/60 bg-muted/25 p-4">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
|
|
@ -504,6 +510,24 @@ export function ImportMyDataSection({ onHistoryRefresh }) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
{/* Import confirmation dialog */}
|
||||||
|
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Import SQLite data export?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Import this SQLite data export into your account? Existing records will be skipped by default.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleConfirmImport}>
|
||||||
|
Confirm Import
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,16 @@ import {
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
import {
|
import {
|
||||||
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
|
@ -785,6 +795,7 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
||||||
const [editPayment, setEditPayment] = useState(null);
|
const [editPayment, setEditPayment] = useState(null);
|
||||||
const [showMbs, setShowMbs] = useState(false);
|
const [showMbs, setShowMbs] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
|
||||||
// Effective amount threshold for this bill this month:
|
// Effective amount threshold for this bill this month:
|
||||||
// actual_amount (if set by monthly override) takes priority over the template expected_amount.
|
// actual_amount (if set by monthly override) takes priority over the template expected_amount.
|
||||||
|
|
@ -819,6 +830,23 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTogglePaid() {
|
||||||
|
setLoading?.(true);
|
||||||
|
try {
|
||||||
|
const result = await api.togglePaid(row.bill_id, {
|
||||||
|
amount: isPaid ? undefined : threshold,
|
||||||
|
paid_date: new Date().toISOString().slice(0, 10),
|
||||||
|
});
|
||||||
|
toast.success(isPaid ? 'Payment removed' : 'Payment recorded');
|
||||||
|
refresh?.();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err.message || 'Failed to toggle payment status');
|
||||||
|
} finally {
|
||||||
|
setLoading?.(false);
|
||||||
|
setConfirmOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow
|
<TableRow
|
||||||
|
|
@ -913,30 +941,19 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={effectiveStatus}
|
status={effectiveStatus}
|
||||||
clickable
|
clickable
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
if (effectiveStatus === 'skipped') return;
|
if (effectiveStatus === 'skipped') return;
|
||||||
|
|
||||||
const isPaid = effectiveStatus === 'paid' || effectiveStatus === 'autodraft';
|
const isPaid = effectiveStatus === 'paid' || effectiveStatus === 'autodraft';
|
||||||
|
|
||||||
// Confirm before toggling Unpaid -> Paid
|
// Show confirmation dialog for toggling Unpaid -> Paid
|
||||||
if (!isPaid) {
|
if (!isPaid) {
|
||||||
if (!confirm(`Mark "${row.name}" as paid?`)) return;
|
setConfirmOpen(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading?.(true);
|
// For mark unpaid, proceed directly
|
||||||
try {
|
handleTogglePaid();
|
||||||
const result = await api.togglePaid(row.bill_id, {
|
|
||||||
amount: isPaid ? undefined : threshold,
|
|
||||||
paid_date: new Date().toISOString().slice(0, 10),
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(isPaid ? 'Payment removed' : 'Payment recorded');
|
|
||||||
refresh?.();
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(err.message || 'Failed to toggle payment status');
|
|
||||||
} finally {
|
|
||||||
setLoading?.(false);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1013,6 +1030,24 @@ function Row({ row, year, month, refresh, index, onEditBill }) {
|
||||||
onSaved={refresh}
|
onSaved={refresh}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Payment toggle confirmation dialog */}
|
||||||
|
<AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Mark "{row.name}" as paid?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will record a payment for this bill. You can edit the payment later.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleTogglePaid}>
|
||||||
|
Confirm Payment
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bill-tracker",
|
"name": "bill-tracker",
|
||||||
"version": "0.23.2",
|
"version": "0.23.3",
|
||||||
"description": "Monthly bill tracking system",
|
"description": "Monthly bill tracking system",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue