diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 3160a02..01d3b74 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -19,6 +19,9 @@ model UserPreferences {
dailyGoal Int?
userName String?
userAge Int?
+ religion String?
+ lastNicotineUsageTime String?
+ lastWeedUsageTime String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
diff --git a/src/app/api/preferences/route.ts b/src/app/api/preferences/route.ts
index 88bc976..7d592ee 100644
--- a/src/app/api/preferences/route.ts
+++ b/src/app/api/preferences/route.ts
@@ -33,6 +33,9 @@ export async function GET() {
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
userName: preferences.userName,
userAge: preferences.userAge,
+ religion: preferences.religion,
+ lastNicotineUsageTime: preferences.lastNicotineUsageTime,
+ lastWeedUsageTime: preferences.lastWeedUsageTime,
});
} catch (error) {
console.error('Error fetching preferences:', error);
@@ -48,7 +51,18 @@ export async function POST(request: NextRequest) {
}
const body = await request.json();
- const { substance, trackingStartDate, hasCompletedSetup, dailyGoal, quitPlan, userName, userAge } = body;
+ const {
+ substance,
+ trackingStartDate,
+ hasCompletedSetup,
+ dailyGoal,
+ quitPlan,
+ userName,
+ userAge,
+ religion,
+ lastNicotineUsageTime,
+ lastWeedUsageTime
+ } = body;
const preferences = await prisma.userPreferences.upsert({
where: { userId: session.user.id },
@@ -60,6 +74,9 @@ export async function POST(request: NextRequest) {
quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
userName,
userAge,
+ religion,
+ lastNicotineUsageTime,
+ lastWeedUsageTime,
},
create: {
userId: session.user.id,
@@ -70,6 +87,9 @@ export async function POST(request: NextRequest) {
quitPlanJson: quitPlan ? JSON.stringify(quitPlan) : null,
userName,
userAge,
+ religion,
+ lastNicotineUsageTime,
+ lastWeedUsageTime,
},
});
@@ -81,6 +101,9 @@ export async function POST(request: NextRequest) {
quitPlan: preferences.quitPlanJson ? JSON.parse(preferences.quitPlanJson) : null,
userName: preferences.userName,
userAge: preferences.userAge,
+ religion: preferences.religion,
+ lastNicotineUsageTime: preferences.lastNicotineUsageTime,
+ lastWeedUsageTime: preferences.lastWeedUsageTime,
});
} catch (error) {
console.error('Error saving preferences:', error);
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx
index 7e23ac6..9353671 100644
--- a/src/components/Dashboard.tsx
+++ b/src/components/Dashboard.tsx
@@ -64,6 +64,7 @@ export function Dashboard({ user }: DashboardProps) {
setUsageData(usage);
setAchievements(achvs);
setSavingsConfig(savings);
+ console.log('[Dashboard] Loaded prefs:', prefs);
setRefreshKey(prev => prev + 1);
return { prefs, usage, achvs };
}, []);
@@ -233,6 +234,11 @@ export function Dashboard({ user }: DashboardProps) {
setPreferences(updatedPrefs);
await savePreferencesAsync(updatedPrefs);
}}
+ preferences={preferences}
+ onPreferencesUpdate={async (updatedPrefs) => {
+ await savePreferencesAsync(updatedPrefs);
+ setPreferences(updatedPrefs);
+ }}
/>
diff --git a/src/components/HealthTimelineCard.tsx b/src/components/HealthTimelineCard.tsx
index d80b9d0..3146ae7 100644
--- a/src/components/HealthTimelineCard.tsx
+++ b/src/components/HealthTimelineCard.tsx
@@ -1,8 +1,8 @@
'use client';
-import { useMemo, useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { HEALTH_MILESTONES, getMinutesSinceQuit, UsageEntry, UserPreferences } from '@/lib/storage';
+import { HEALTH_MILESTONES, UsageEntry, UserPreferences } from '@/lib/storage';
import { useTheme } from '@/lib/theme-context';
import {
Heart,
@@ -14,13 +14,13 @@ import {
Sparkles,
HeartHandshake,
CheckCircle2,
- Clock,
Cigarette,
Leaf
} from 'lucide-react';
interface HealthTimelineCardProps {
usageData: UsageEntry[];
+ preferences?: UserPreferences | null;
}
const iconMap: Record
= {
@@ -34,8 +34,60 @@ const iconMap: Record = {
HeartHandshake,
};
+// Simple, direct calculation of minutes since last usage
+function calculateMinutesFree(
+ substance: 'nicotine' | 'weed',
+ usageData: UsageEntry[],
+ preferences: UserPreferences | null
+): number {
+ const now = new Date();
+
+ // 1. Check for stored timestamp first (most accurate)
+ const lastUsageTime = substance === 'nicotine'
+ ? preferences?.lastNicotineUsageTime
+ : preferences?.lastWeedUsageTime;
+
+ if (lastUsageTime) {
+ const lastTime = new Date(lastUsageTime);
+ const diffMs = now.getTime() - lastTime.getTime();
+ return Math.max(0, diffMs / (1000 * 60));
+ }
+
+ // 2. Find last recorded usage from usage data
+ const substanceData = usageData
+ .filter(e => e.substance === substance && e.count > 0)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ if (substanceData.length > 0) {
+ const lastDateStr = substanceData[0].date;
+ const todayStr = now.toISOString().split('T')[0];
+
+ // If last usage was today but no timestamp, count from now (0 minutes)
+ if (lastDateStr === todayStr) {
+ return 0;
+ }
+
+ // For past days, count from end of that day (23:59:59)
+ const lastDate = new Date(lastDateStr);
+ lastDate.setHours(23, 59, 59, 999);
+ const diffMs = now.getTime() - lastDate.getTime();
+ return Math.max(0, diffMs / (1000 * 60));
+ }
+
+ // 3. No usage ever - count from tracking start date
+ if (preferences?.trackingStartDate) {
+ const startDate = new Date(preferences.trackingStartDate);
+ startDate.setHours(0, 0, 0, 0);
+ const diffMs = now.getTime() - startDate.getTime();
+ return Math.max(0, diffMs / (1000 * 60));
+ }
+
+ return 0;
+}
+
function formatDuration(minutes: number): string {
- if (minutes < 60) return `${minutes} min`;
+ if (minutes < 1) return '< 1 min';
+ if (minutes < 60) return `${Math.floor(minutes)} min`;
if (minutes < 1440) return `${Math.floor(minutes / 60)} hrs`;
if (minutes < 10080) return `${Math.floor(minutes / 1440)} days`;
if (minutes < 43200) return `${Math.floor(minutes / 10080)} weeks`;
@@ -51,55 +103,52 @@ function formatTimeRemaining(currentMinutes: number, targetMinutes: number): str
interface TimelineColumnProps {
substance: 'nicotine' | 'weed';
- minutesSinceQuit: number;
+ minutesFree: number;
theme: 'light' | 'dark';
}
-function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnProps) {
- const currentMilestoneIndex = useMemo(() => {
- for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) {
- if (minutesSinceQuit >= HEALTH_MILESTONES[i].timeMinutes) {
- return i;
- }
+function TimelineColumn({ substance, minutesFree, theme }: TimelineColumnProps) {
+ // Find current milestone
+ let currentMilestoneIndex = -1;
+ for (let i = HEALTH_MILESTONES.length - 1; i >= 0; i--) {
+ if (minutesFree >= HEALTH_MILESTONES[i].timeMinutes) {
+ currentMilestoneIndex = i;
+ break;
}
- return -1;
- }, [minutesSinceQuit]);
+ }
- const nextMilestone = useMemo(() => {
- const nextIndex = currentMilestoneIndex + 1;
- if (nextIndex < HEALTH_MILESTONES.length) {
- return HEALTH_MILESTONES[nextIndex];
- }
- return null;
- }, [currentMilestoneIndex]);
+ // Find next milestone
+ const nextMilestoneIndex = currentMilestoneIndex + 1;
+ const nextMilestone = nextMilestoneIndex < HEALTH_MILESTONES.length
+ ? HEALTH_MILESTONES[nextMilestoneIndex]
+ : null;
- const progressToNext = useMemo(() => {
- if (!nextMilestone) return 100;
- const prevMinutes =
- currentMilestoneIndex >= 0
- ? HEALTH_MILESTONES[currentMilestoneIndex].timeMinutes
- : 0;
+ // Calculate progress to next milestone
+ let progressToNext = 100;
+ if (nextMilestone) {
+ const prevMinutes = currentMilestoneIndex >= 0
+ ? HEALTH_MILESTONES[currentMilestoneIndex].timeMinutes
+ : 0;
const range = nextMilestone.timeMinutes - prevMinutes;
- const progress = minutesSinceQuit - prevMinutes;
- return Math.min(100, Math.max(0, (progress / range) * 100));
- }, [minutesSinceQuit, nextMilestone, currentMilestoneIndex]);
+ const progress = minutesFree - prevMinutes;
+ progressToNext = Math.min(100, Math.max(0, (progress / range) * 100));
+ }
const substanceLabel = substance === 'nicotine' ? 'Nicotine' : 'Marijuana';
const SubstanceIcon = substance === 'nicotine' ? Cigarette : Leaf;
- const accentColor = substance === 'nicotine' ? 'red' : 'green';
const accentColorClass = substance === 'nicotine' ? 'text-red-500' : 'text-green-500';
const bgAccentClass = substance === 'nicotine' ? 'bg-red-500' : 'bg-green-500';
return (
- {/* Header */}
+ {/* Header with live timer */}
{substanceLabel}
-
- {formatDuration(Math.floor(minutesSinceQuit))} free
+
+ {formatDuration(minutesFree)} free
@@ -110,7 +159,7 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
Next Up
- {formatTimeRemaining(Math.floor(minutesSinceQuit), nextMilestone.timeMinutes)}
+ {formatTimeRemaining(minutesFree, nextMilestone.timeMinutes)}
@@ -127,7 +176,7 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
{/* Timeline Items */}
{HEALTH_MILESTONES.map((milestone, index) => {
- const isAchieved = minutesSinceQuit >= milestone.timeMinutes;
+ const isAchieved = minutesFree >= milestone.timeMinutes;
const isCurrent = index === currentMilestoneIndex;
const Icon = iconMap[milestone.icon] || Heart;
@@ -135,15 +184,15 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
{/* Icon */}
{isAchieved ?
:
}
@@ -151,11 +200,9 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
{/* Content */}
-
-
- {milestone.title}
-
-
+
+ {milestone.title}
+
{milestone.description}
@@ -168,20 +215,30 @@ function TimelineColumn({ substance, minutesSinceQuit, theme }: TimelineColumnPr
);
}
-export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps & { preferences?: UserPreferences | null }) {
+export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCardProps) {
const { theme } = useTheme();
- const [now, setNow] = useState(Date.now());
+ // State for live timer values
+ const [nicotineMinutes, setNicotineMinutes] = useState(0);
+ const [weedMinutes, setWeedMinutes] = useState(0);
+
+ // Function to recalculate both timers
+ const updateTimers = useCallback(() => {
+ const prefs = preferences || null;
+ setNicotineMinutes(calculateMinutesFree('nicotine', usageData, prefs));
+ setWeedMinutes(calculateMinutesFree('weed', usageData, prefs));
+ }, [usageData, preferences]);
+
+ // Initial calculation and start interval
useEffect(() => {
- const interval = setInterval(() => {
- setNow(Date.now());
- }, 1000); // Update every second
+ // Calculate immediately
+ updateTimers();
+
+ // Update every second
+ const interval = setInterval(updateTimers, 1000);
return () => clearInterval(interval);
- }, []);
-
- const nicotineMinutes = useMemo(() => getMinutesSinceQuit(usageData, 'nicotine', true, preferences), [usageData, preferences, now]);
- const weedMinutes = useMemo(() => getMinutesSinceQuit(usageData, 'weed', true, preferences), [usageData, preferences, now]);
+ }, [updateTimers]);
const cardBackground =
theme === 'light'
@@ -207,8 +264,8 @@ export function HealthTimelineCard({ usageData, preferences }: HealthTimelineCar
-
-
+
+
diff --git a/src/components/UsageCalendar.tsx b/src/components/UsageCalendar.tsx
index 3eb19bc..7f72eaf 100644
--- a/src/components/UsageCalendar.tsx
+++ b/src/components/UsageCalendar.tsx
@@ -12,7 +12,7 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
-import { UsageEntry, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
+import { UsageEntry, UserPreferences, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
import { useTheme } from '@/lib/theme-context';
import { DailyInspirationCard } from './DailyInspirationCard';
@@ -25,9 +25,11 @@ interface UsageCalendarProps {
userId: string;
religion?: 'christian' | 'muslim' | 'jewish' | 'secular' | null;
onReligionUpdate?: (religion: 'christian' | 'muslim' | 'jewish' | 'secular') => void;
+ preferences?: UserPreferences | null;
+ onPreferencesUpdate?: (prefs: UserPreferences) => Promise
;
}
-export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpdate }: UsageCalendarProps) {
+export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpdate, preferences, onPreferencesUpdate }: UsageCalendarProps) {
const [selectedDate, setSelectedDate] = useState(undefined);
const [editNicotineCount, setEditNicotineCount] = useState('');
const [editWeedCount, setEditWeedCount] = useState('');
@@ -59,6 +61,7 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
const handleSave = async () => {
if (selectedDate) {
const dateStr = selectedDate.toISOString().split('T')[0];
+ const todayStr = new Date().toISOString().split('T')[0];
const newNicotineCount = parseInt(editNicotineCount, 10) || 0;
const newWeedCount = parseInt(editWeedCount, 10) || 0;
@@ -66,6 +69,25 @@ export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpd
setUsageForDateAsync(dateStr, newNicotineCount, 'nicotine'),
setUsageForDateAsync(dateStr, newWeedCount, 'weed'),
]);
+
+ // Update last usage time preferences if editing today's usage and count > 0
+ if (dateStr === todayStr && preferences && onPreferencesUpdate) {
+ const now = new Date().toISOString();
+ const updatedPrefs = { ...preferences };
+
+ if (newNicotineCount > 0) {
+ updatedPrefs.lastNicotineUsageTime = now;
+ }
+ if (newWeedCount > 0) {
+ updatedPrefs.lastWeedUsageTime = now;
+ }
+
+ // Only update if we changed something
+ if (newNicotineCount > 0 || newWeedCount > 0) {
+ await onPreferencesUpdate(updatedPrefs);
+ }
+ }
+
onDataUpdate();
}
setIsEditing(false);
diff --git a/src/lib/storage.ts b/src/lib/storage.ts
index 9dfb58d..8f0ae11 100644
--- a/src/lib/storage.ts
+++ b/src/lib/storage.ts
@@ -395,81 +395,7 @@ export function calculateTotalSaved(
return Math.max(0, expectedSpend - actualSpend);
}
-export function getMinutesSinceQuit(
- usageData: UsageEntry[],
- substance: 'nicotine' | 'weed',
- precise: boolean = false,
- preferences?: UserPreferences | null
-): number {
- // Try to use precise timestamp from preferences first
- if (preferences) {
- const lastUsageTimeStr = substance === 'nicotine'
- ? preferences.lastNicotineUsageTime
- : preferences.lastWeedUsageTime;
- if (lastUsageTimeStr) {
- const now = new Date();
- const lastUsageTime = new Date(lastUsageTimeStr);
- const diffMs = now.getTime() - lastUsageTime.getTime();
- const minutes = Math.max(0, diffMs / (1000 * 60));
-
- // Sanity check: if the timestamp is OLDER than the last recorded date in usageData,
- // it might mean the user manually added a later date in the calendar without a timestamp.
- // In that case, we should fall back to the date-based logic.
-
- const substanceData = usageData
- .filter((e) => e.substance === substance && e.count > 0)
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
-
- if (substanceData.length > 0) {
- const lastDateStr = substanceData[0].date;
- const lastDate = new Date(lastDateStr);
- // Set lastDate to end of day to compare with timestamp
- lastDate.setHours(23, 59, 59, 999);
-
- // If the timestamp is essentially on the same day or later than the last recorded date, rely on the timestamp
- // (We allow the timestamp to be earlier in the same day, that's the whole point)
- const lastDateStart = new Date(lastDateStr);
- lastDateStart.setHours(0, 0, 0, 0);
-
- if (lastUsageTime >= lastDateStart) {
- return precise ? minutes : Math.floor(minutes);
- }
- } else {
- // No usage data but we have a timestamp? Trust the timestamp.
- return precise ? minutes : Math.floor(minutes);
- }
- }
- }
-
- // Find the last usage date for this substance
- const substanceData = usageData
- .filter((e) => e.substance === substance && e.count > 0)
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
-
- if (substanceData.length === 0) {
- // No usage recorded, assume they just started
- return 0;
- }
-
- const now = new Date();
- const todayStr = now.toISOString().split('T')[0];
- const lastUsageDateStr = substanceData[0].date;
-
- // If the last usage was today, reset to 0 (just used, unknown time)
- if (lastUsageDateStr === todayStr) {
- return 0;
- }
-
- // For past days, count from the end of that day
- const lastUsageDate = new Date(lastUsageDateStr);
- lastUsageDate.setHours(23, 59, 59, 999);
-
- const diffMs = now.getTime() - lastUsageDate.getTime();
- const minutes = Math.max(0, diffMs / (1000 * 60));
-
- return precise ? minutes : Math.floor(minutes);
-}
export function checkBadgeEligibility(
badgeId: string,
diff --git a/src/lib/tracker/NicotineTracker.ts b/src/lib/tracker/NicotineTracker.ts
new file mode 100644
index 0000000..e4fd9d2
--- /dev/null
+++ b/src/lib/tracker/NicotineTracker.ts
@@ -0,0 +1,77 @@
+import { RecoveryTracker } from './RecoveryTracker';
+
+export class NicotineTracker extends RecoveryTracker {
+ calculateMinutesFree(precise: boolean = false): number {
+ // 1. Try to use precise timestamp from preferences first
+ if (this.preferences?.lastNicotineUsageTime) {
+ const now = new Date();
+ const lastUsageTime = new Date(this.preferences.lastNicotineUsageTime);
+ console.log('[NicotineTracker] Found timestamp:', this.preferences.lastNicotineUsageTime);
+
+ const diffMs = now.getTime() - lastUsageTime.getTime();
+ const minutes = this.msToMinutes(diffMs, precise);
+
+ // Validation: Ensure the timestamp aligns with the last recorded usage date.
+ // If the user manually edited usage for a LATER date, the timestamp might be stale.
+ const lastRecordedDateStr = this.getLastRecordedDate();
+ console.log('[NicotineTracker] Last recorded date:', lastRecordedDateStr);
+
+ if (lastRecordedDateStr) {
+ const lastRecordedDate = new Date(lastRecordedDateStr);
+ lastRecordedDate.setHours(0, 0, 0, 0);
+
+ // If the timestamp is older than the start of the last recorded day,
+ // it means we have a newer manual entry without a timestamp.
+ // In this case, fall back to date-based logic.
+ if (lastUsageTime < lastRecordedDate) {
+ console.log('[NicotineTracker] Timestamp is stale, falling back to date logic');
+ return this.calculateDateBasedMinutes(lastRecordedDateStr, precise);
+ }
+ }
+
+ return minutes;
+ }
+
+ // 2. Fallback to date-based logic if no timestamp exists
+ const lastDateStr = this.getLastRecordedDate();
+
+ // 3. If no nicotine usage ever recorded, use tracking start date
+ if (!lastDateStr) {
+ if (this.preferences?.trackingStartDate) {
+ const startDate = new Date(this.preferences.trackingStartDate);
+ startDate.setHours(0, 0, 0, 0); // Count from start of tracking day
+ const now = new Date();
+ const diffMs = now.getTime() - startDate.getTime();
+ return this.msToMinutes(diffMs, precise);
+ }
+ return 0; // No usage and no tracking start date
+ }
+
+ return this.calculateDateBasedMinutes(lastDateStr, precise);
+ }
+
+ private getLastRecordedDate(): string | null {
+ const nicotineData = this.usageData
+ .filter((e) => e.substance === 'nicotine' && e.count > 0)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ return nicotineData.length > 0 ? nicotineData[0].date : null;
+ }
+
+ private calculateDateBasedMinutes(dateStr: string, precise: boolean): number {
+ const now = new Date();
+ const todayStr = now.toISOString().split('T')[0];
+
+ // If the last usage was today but we have no timestamp, reset to 0
+ if (dateStr === todayStr) {
+ return 0;
+ }
+
+ // For past days, count from the END of that day (23:59:59)
+ const lastUsageDate = new Date(dateStr);
+ lastUsageDate.setHours(23, 59, 59, 999);
+
+ const diffMs = now.getTime() - lastUsageDate.getTime();
+ return this.msToMinutes(diffMs, precise);
+ }
+}
diff --git a/src/lib/tracker/RecoveryTracker.ts b/src/lib/tracker/RecoveryTracker.ts
new file mode 100644
index 0000000..2be5593
--- /dev/null
+++ b/src/lib/tracker/RecoveryTracker.ts
@@ -0,0 +1,33 @@
+import { UsageEntry, UserPreferences } from '../storage';
+
+export abstract class RecoveryTracker {
+ protected usageData: UsageEntry[];
+ protected preferences: UserPreferences | null;
+
+ constructor(usageData: UsageEntry[], preferences: UserPreferences | null) {
+ this.usageData = usageData;
+ this.preferences = preferences;
+ }
+
+ /**
+ * Calculates the number of minutes elapsed since the last usage.
+ * This is the core logic that subclasses must support, but the implementation
+ * heavily depends on the specific substance's data source (preferences timestamp vs usage logs).
+ */
+ abstract calculateMinutesFree(precise?: boolean): number;
+
+ /**
+ * Helper to convert milliseconds to minutes with optional precision.
+ */
+ protected msToMinutes(ms: number, precise: boolean = false): number {
+ const minutes = Math.max(0, ms / (1000 * 60));
+ return precise ? minutes : Math.floor(minutes);
+ }
+
+ /**
+ * Helper to check if a timestamp is valid and recent enough to rely on.
+ */
+ protected isValidTimestamp(timestamp: string | null | undefined): boolean {
+ return !!timestamp && !isNaN(new Date(timestamp).getTime());
+ }
+}
diff --git a/src/lib/tracker/WeedTracker.ts b/src/lib/tracker/WeedTracker.ts
new file mode 100644
index 0000000..d52c620
--- /dev/null
+++ b/src/lib/tracker/WeedTracker.ts
@@ -0,0 +1,69 @@
+import { RecoveryTracker } from './RecoveryTracker';
+
+export class WeedTracker extends RecoveryTracker {
+ calculateMinutesFree(precise: boolean = false): number {
+ // 1. Try to use precise timestamp from preferences first
+ if (this.preferences?.lastWeedUsageTime) {
+ const now = new Date();
+ const lastUsageTime = new Date(this.preferences.lastWeedUsageTime);
+ const diffMs = now.getTime() - lastUsageTime.getTime();
+ const minutes = this.msToMinutes(diffMs, precise);
+
+ // Validation: Ensure the timestamp aligns with the last recorded usage date.
+ const lastRecordedDateStr = this.getLastRecordedDate();
+
+ if (lastRecordedDateStr) {
+ const lastRecordedDate = new Date(lastRecordedDateStr);
+ lastRecordedDate.setHours(0, 0, 0, 0);
+
+ if (lastUsageTime < lastRecordedDate) {
+ return this.calculateDateBasedMinutes(lastRecordedDateStr, precise);
+ }
+ }
+
+ return minutes;
+ }
+
+ // 2. Fallback to date-based logic if no timestamp exists
+ const lastDateStr = this.getLastRecordedDate();
+
+ // 3. If no weed usage ever recorded, use tracking start date
+ if (!lastDateStr) {
+ if (this.preferences?.trackingStartDate) {
+ const startDate = new Date(this.preferences.trackingStartDate);
+ startDate.setHours(0, 0, 0, 0); // Count from start of tracking day
+ const now = new Date();
+ const diffMs = now.getTime() - startDate.getTime();
+ return this.msToMinutes(diffMs, precise);
+ }
+ return 0; // No usage and no tracking start date
+ }
+
+ return this.calculateDateBasedMinutes(lastDateStr, precise);
+ }
+
+ private getLastRecordedDate(): string | null {
+ const weedData = this.usageData
+ .filter((e) => e.substance === 'weed' && e.count > 0)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ return weedData.length > 0 ? weedData[0].date : null;
+ }
+
+ private calculateDateBasedMinutes(dateStr: string, precise: boolean): number {
+ const now = new Date();
+ const todayStr = now.toISOString().split('T')[0];
+
+ // If the last usage was today but we have no timestamp, reset to 0
+ if (dateStr === todayStr) {
+ return 0;
+ }
+
+ // For past days, count from the END of that day (23:59:59)
+ const lastUsageDate = new Date(dateStr);
+ lastUsageDate.setHours(23, 59, 59, 999);
+
+ const diffMs = now.getTime() - lastUsageDate.getTime();
+ return this.msToMinutes(diffMs, precise);
+ }
+}