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:
null 2026-05-10 14:36:59 -05:00
parent 7c3cfd1715
commit 5eed5932b4
5 changed files with 100 additions and 36 deletions

View File

@ -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:

View File

@ -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.' },
], ],
}; };

View File

@ -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>
</>
); );
} }

View File

@ -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>
</> </>
); );
} }

View File

@ -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": {