// ==================== Baby Tracker Database ==================== window.BabyDB = (() => { 'use strict'; let _db = null; let _initPromise = null; /** * Initialize Supabase client after env is ready. * Safe to call multiple times — returns the same promise. */ function init() { if (_initPromise) return _initPromise; _initPromise = (async () => { // Wait for remote config (or fallback defaults) if (window.envReady) { try { await window.envReady; } catch { /* use defaults */ } } const url = window.env?.SUPABASE_URL || ''; const key = window.env?.SUPABASE_KEY || ''; if (!url || !key || url.includes('YOUR_') || key.includes('YOUR_')) { console.warn('BabyDB: Supabase credentials not configured'); return false; } if (typeof supabase === 'undefined') { console.warn('BabyDB: supabase-js SDK not loaded'); return false; } _db = supabase.createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } }); return true; })(); return _initPromise; } function db() { if (!_db) { throw new Error('BabyDB 未初始化,请检查 Supabase 配置'); } return _db; } async function ensureReady() { const ok = await init(); if (!ok) throw new Error('数据库未就绪,请检查网络或配置'); } async function fetchRecords(start, end) { await ensureReady(); const { data, error } = await db() .from('baby_records') .select('*') .gte('recorded_at', start) .lte('recorded_at', end) .order('recorded_at', { ascending: false }); return { data: data || [], error }; } async function fetchRecordsByType(start, end, types) { await ensureReady(); const { data, error } = await db() .from('baby_records') .select('*') .gte('recorded_at', start) .lte('recorded_at', end) .in('type', types) .order('recorded_at', { ascending: false }); return { data: data || [], error }; } async function insertRecord(payload) { await ensureReady(); const { data, error } = await db() .from('baby_records') .insert([payload]) .select() .single(); return { data, error }; } async function updateRecord(id, payload) { await ensureReady(); const { data, error } = await db() .from('baby_records') .update(payload) .eq('id', id) .select() .single(); return { data, error }; } async function deleteRecord(id) { await ensureReady(); const { error } = await db() .from('baby_records') .delete() .eq('id', id); return { error }; } /** Latest milk record (any day) — for default amount / last-feed timer */ async function fetchLatestMilk() { await ensureReady(); const { data, error } = await db() .from('baby_records') .select('id, recorded_at, amount, sub_type') .eq('type', 'milk') .order('recorded_at', { ascending: false }) .limit(1) .maybeSingle(); return { data: data || null, error }; } /** * Check VitD status for yesterday and today. * Uses Asia/Shanghai date strings for comparison. */ async function checkVitDStatus(yesterdayStr, todayStr) { try { await ensureReady(); const start = `${yesterdayStr}T00:00:00+08:00`; const end = `${todayStr}T23:59:59+08:00`; const { data, error } = await db() .from('baby_records') .select('recorded_at, type, sub_type') .eq('type', 'meds') .gte('recorded_at', start) .lte('recorded_at', end); if (error || !data) return { today: false, yesterday: false }; const isVitD = (row) => (row.sub_type || '').includes('维D') || (row.sub_type || '').includes('Vitamin') || (row.sub_type || '').includes('VD'); // Convert recorded_at to CN date for comparison const toCNDate = (iso) => { const d = new Date(new Date(iso).getTime() + 8 * 60 * 60 * 1000); return d.toISOString().slice(0, 10); }; const today = data.some(row => isVitD(row) && toCNDate(row.recorded_at) === todayStr); const yesterday = data.some(row => isVitD(row) && toCNDate(row.recorded_at) === yesterdayStr); return { today, yesterday }; } catch (err) { console.error('检查维D记录失败:', err); return { today: false, yesterday: false }; } } // Kick off init early (non-blocking) init(); return { init, fetchRecords, fetchRecordsByType, insertRecord, updateRecord, deleteRecord, fetchLatestMilk, checkVitDStatus }; })();