diff --git a/.github/assets/screenshots/flutter_01.png b/.github/assets/screenshots/flutter_01.png new file mode 100644 index 0000000..f812c6f Binary files /dev/null and b/.github/assets/screenshots/flutter_01.png differ diff --git a/.github/assets/screenshots/flutter_02.png b/.github/assets/screenshots/flutter_02.png new file mode 100644 index 0000000..2668b2f Binary files /dev/null and b/.github/assets/screenshots/flutter_02.png differ diff --git a/.github/assets/screenshots/flutter_03.png b/.github/assets/screenshots/flutter_03.png new file mode 100644 index 0000000..f7abe83 Binary files /dev/null and b/.github/assets/screenshots/flutter_03.png differ diff --git a/.github/assets/screenshots/flutter_04.png b/.github/assets/screenshots/flutter_04.png new file mode 100644 index 0000000..4f1d9ef Binary files /dev/null and b/.github/assets/screenshots/flutter_04.png differ diff --git a/.github/assets/screenshots/flutter_05.png b/.github/assets/screenshots/flutter_05.png new file mode 100644 index 0000000..7923fd3 Binary files /dev/null and b/.github/assets/screenshots/flutter_05.png differ diff --git a/.github/assets/screenshots/flutter_06.png b/.github/assets/screenshots/flutter_06.png new file mode 100644 index 0000000..2c0130e Binary files /dev/null and b/.github/assets/screenshots/flutter_06.png differ diff --git a/.github/assets/screenshots/flutter_07.png b/.github/assets/screenshots/flutter_07.png new file mode 100644 index 0000000..a537d1a Binary files /dev/null and b/.github/assets/screenshots/flutter_07.png differ diff --git a/.github/assets/screenshots/flutter_08.png b/.github/assets/screenshots/flutter_08.png new file mode 100644 index 0000000..3714e14 Binary files /dev/null and b/.github/assets/screenshots/flutter_08.png differ diff --git a/.github/assets/screenshots/flutter_09.png b/.github/assets/screenshots/flutter_09.png new file mode 100644 index 0000000..d2029f3 Binary files /dev/null and b/.github/assets/screenshots/flutter_09.png differ diff --git a/.github/assets/screenshots/flutter_10.png b/.github/assets/screenshots/flutter_10.png new file mode 100644 index 0000000..d217a9f Binary files /dev/null and b/.github/assets/screenshots/flutter_10.png differ diff --git a/.github/assets/screenshots/flutter_11.png b/.github/assets/screenshots/flutter_11.png new file mode 100644 index 0000000..9ae2f66 Binary files /dev/null and b/.github/assets/screenshots/flutter_11.png differ diff --git a/.github/assets/screenshots/flutter_12.png b/.github/assets/screenshots/flutter_12.png new file mode 100644 index 0000000..cdad275 Binary files /dev/null and b/.github/assets/screenshots/flutter_12.png differ diff --git a/.github/assets/screenshots/flutter_13.png b/.github/assets/screenshots/flutter_13.png new file mode 100644 index 0000000..5583abe Binary files /dev/null and b/.github/assets/screenshots/flutter_13.png differ diff --git a/patient/lib/core/repository/auth/auth_repository.dart b/patient/lib/core/repository/auth/auth_repository.dart index 86986c7..0d98dea 100644 --- a/patient/lib/core/repository/auth/auth_repository.dart +++ b/patient/lib/core/repository/auth/auth_repository.dart @@ -1,7 +1,4 @@ -import 'package:flutter/material.dart'; import 'package:patient/core/core.dart'; -import 'package:patient/core/entities/auth_entities/personal_info_entity.dart'; -import 'package:patient/core/result/result.dart'; abstract interface class AuthRepository { // The abstract repository class will define the methods that the repository must implement. diff --git a/patient/lib/presentation/result/result.dart b/patient/lib/presentation/result/result.dart index a486e98..d856738 100644 --- a/patient/lib/presentation/result/result.dart +++ b/patient/lib/presentation/result/result.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:http/http.dart' as http; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:patient/presentation/result/book_appointment.dart'; import 'package:patient/presentation/result/widgets/buildresult.dart'; // Import the widgets file diff --git a/patient/lib/provider/appointments_provider.dart b/patient/lib/provider/appointments_provider.dart index 8c05f1d..811ca7b 100644 --- a/patient/lib/provider/appointments_provider.dart +++ b/patient/lib/provider/appointments_provider.dart @@ -135,7 +135,7 @@ class AppointmentsProvider extends ChangeNotifier { availableTimeSlots = []; } } catch(e) { - print(e); + debugPrint("Error fetching time slots: $e"); if (token == _fetchToken) { availableTimeSlots = []; } @@ -166,7 +166,7 @@ class AppointmentsProvider extends ChangeNotifier { } } } catch(e) { - print(e); + debugPrint("Error fetching appointments: $e"); } finally { notifyListeners(); } @@ -193,7 +193,7 @@ class AppointmentsProvider extends ChangeNotifier { return false; } } catch(e) { - print(e); + debugPrint("Error fetching appointments: $e"); return false; } finally { fetchAllAppointments(); @@ -211,7 +211,7 @@ class AppointmentsProvider extends ChangeNotifier { return false; } } catch(e) { - print(e); + debugPrint("Error fetching appointments: $e"); return false; } finally { fetchAllAppointments(); diff --git a/patient/lib/provider/therapist_provider.dart b/patient/lib/provider/therapist_provider.dart index 94422a8..bab8851 100644 --- a/patient/lib/provider/therapist_provider.dart +++ b/patient/lib/provider/therapist_provider.dart @@ -27,7 +27,8 @@ class TherapistProvider with ChangeNotifier { notifyListeners(); } catch (e) { _isLoading = false; - print("Error fetching therapists: $e"); + // TODO: Implement proper error logging and user notification + debugPrint("Error fetching therapists: $e"); notifyListeners(); } } diff --git a/patient/lib/repository/supabase_assessments_repository.dart b/patient/lib/repository/supabase_assessments_repository.dart index a3cf467..c8f7dcd 100644 --- a/patient/lib/repository/supabase_assessments_repository.dart +++ b/patient/lib/repository/supabase_assessments_repository.dart @@ -12,14 +12,12 @@ class SupabaseAssessmentsRepository implements AssessmentsRepository { @override Future>> fetchAssessmentById(String id) async { - print('Fetching assessment with id: $id'); final response = await _supabase .from('assessments') .select('*') .eq('id', id) .limit(1) .maybeSingle(); -print('Response: $response'); return response != null ? [response] : []; } diff --git a/patient/lib/repository/supabase_patient_repository.dart b/patient/lib/repository/supabase_patient_repository.dart index 698a065..97fbd43 100644 --- a/patient/lib/repository/supabase_patient_repository.dart +++ b/patient/lib/repository/supabase_patient_repository.dart @@ -123,7 +123,21 @@ class SupabasePatientRepository implements PatientRepository { @override Future deleteAppointment(String id) async { try { - await _supabaseClient.from('session').delete().eq('id', id); + final currentUser = _supabaseClient.auth.currentUser; + if (currentUser == null) { + return ActionResultFailure( + errorMessage: 'User is not authenticated', + statusCode: 401 + ); + } + + // Add ownership check - only allow deletion of user's own sessions + await _supabaseClient + .from('session') + .delete() + .eq('id', id) + .eq('patient_id', currentUser.id); + return ActionResultSuccess( data: 'Appointment deleted successfully', statusCode: 200 diff --git a/supabase/schemas/schema_fixed.sql b/supabase/schemas/schema_fixed.sql new file mode 100644 index 0000000..4617bb4 --- /dev/null +++ b/supabase/schemas/schema_fixed.sql @@ -0,0 +1,175 @@ +-- NeuroTrack Database Schema (fixed ordering & syntax) +-- Run this in Supabase SQL Editor: https://supabase.com/dashboard/project/apqhleefisqnavwxuvqg/sql/new + +-- Create therapist table first (referenced by patient) +CREATE TABLE IF NOT EXISTS therapist ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + phone TEXT NOT NULL, + clinic_id UUID, + license TEXT, + approved BOOLEAN DEFAULT FALSE, + specialisation TEXT, + gender TEXT, + offered_therapies TEXT[], + age INT2, + regulatory_body TEXT, + start_availability_time TEXT, + end_availability_time TEXT, + license_number TEXT +); + +-- Create the patient table +CREATE TABLE IF NOT EXISTS patient ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + patient_name TEXT NOT NULL, + age INT2, + is_adult BOOLEAN NOT NULL, + guardian_name TEXT, + phone TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + guardian_relation TEXT, + autism_level INT2, + onboarded_on TIMESTAMPTZ, + therapist_id UUID REFERENCES therapist(id) ON DELETE SET NULL, + gender TEXT, + country TEXT +); + +-- Create the package table +CREATE TABLE IF NOT EXISTS package ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + name TEXT NOT NULL, + duration INT4 NOT NULL +); + +-- Create the session table +-- TODO: Enable Row-Level Security (RLS) in follow-up PR for defense-in-depth +-- Will add: ALTER TABLE session ENABLE ROW LEVEL SECURITY; +-- CREATE POLICY patient_owns_session ON session FOR DELETE USING (patient_id = auth.uid()); +CREATE TABLE IF NOT EXISTS session ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + timestamp TIMESTAMPTZ NOT NULL, + therapist_id UUID REFERENCES therapist(id), + patient_id UUID REFERENCES patient(id), + is_consultation BOOLEAN DEFAULT FALSE, + mode INT2, + duration INT4, + name TEXT, + status TEXT NOT NULL CHECK (status IN ('accepted', 'declined', 'pending')) DEFAULT 'pending', + declined_reason TEXT +); + +-- Therapy Table +CREATE TABLE IF NOT EXISTS therapy ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + name TEXT NOT NULL UNIQUE, + description TEXT +); + +-- Create the therapy_goal table +CREATE TABLE IF NOT EXISTS therapy_goal ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + performed_on TIMESTAMPTZ, + therapist_id UUID REFERENCES therapist(id), + therapy_mode INT2, + duration INT4, + therapy_type INT2, + therapy_type_id UUID REFERENCES therapy(id), + goals JSONB, + observations JSONB, + regressions JSONB, + activities JSONB, + patient_id UUID REFERENCES patient(id) +); + +-- Create the assessments table +CREATE TABLE IF NOT EXISTS assessments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + name TEXT NOT NULL, + description TEXT, + category TEXT, + cutoff_score INT2, + image_url TEXT, + questions JSONB NOT NULL +); + +-- Create the assessment_results table +CREATE TABLE IF NOT EXISTS assessment_results ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + assessment_id UUID REFERENCES assessments(id), + patient_id UUID REFERENCES patient(id), + submission JSONB, + result JSONB +); + +-- Therapy Goals Master Table +CREATE TABLE IF NOT EXISTS goal_master ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + goal_text TEXT NOT NULL, + applicable_therapies UUID[] NOT NULL +); + +-- Observations Master Table +CREATE TABLE IF NOT EXISTS observation_master ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + observation_text TEXT NOT NULL, + applicable_therapies UUID[] NOT NULL +); + +-- Regressions Master Table +CREATE TABLE IF NOT EXISTS regression_master ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + regression_text TEXT NOT NULL, + applicable_therapies UUID[] NOT NULL +); + +-- Activities Master Table +CREATE TABLE IF NOT EXISTS activity_master ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + activity_text TEXT NOT NULL, + applicable_therapies UUID[] NOT NULL +); + +-- Daily Activities Table +CREATE TABLE IF NOT EXISTS daily_activities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ DEFAULT NOW(), + activity_name TEXT NOT NULL, + activity_list JSONB, + is_active BOOLEAN DEFAULT TRUE, + therapist_id UUID REFERENCES therapist(id) ON DELETE SET NULL, + patient_id UUID REFERENCES patient(id) ON DELETE CASCADE, + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + days_of_week INT2[] +); + +-- Daily Activity Logs Table +CREATE TABLE IF NOT EXISTS daily_activity_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + activity_id UUID REFERENCES daily_activities(id) ON DELETE CASCADE, + date TIMESTAMPTZ NOT NULL, + activity_items JSONB NOT NULL, + patient_id UUID REFERENCES patient(id) ON DELETE CASCADE +); + +-- Indexes on foreign keys for better performance +CREATE INDEX IF NOT EXISTS idx_patient_therapist_id ON patient(therapist_id); +CREATE INDEX IF NOT EXISTS idx_session_therapist_id ON session(therapist_id); +CREATE INDEX IF NOT EXISTS idx_session_patient_id ON session(patient_id); +CREATE INDEX IF NOT EXISTS idx_therapy_goal_therapist_id ON therapy_goal(therapist_id); +CREATE INDEX IF NOT EXISTS idx_therapy_goal_patient_id ON therapy_goal(patient_id); diff --git a/supabase/scripts/seed_dev_reports.js b/supabase/scripts/seed_dev_reports.js new file mode 100644 index 0000000..38a5bf6 --- /dev/null +++ b/supabase/scripts/seed_dev_reports.js @@ -0,0 +1,248 @@ +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); +const { v4: uuidv4 } = require('uuid'); + +// Validate required environment variables +const { SUPABASE_URL, SUPABASE_KEY, ALLOW_DEV_SEED, DEV_EMAIL, DEV_PASSWORD } = process.env; + +if (!SUPABASE_URL || !SUPABASE_KEY) { + console.error('Missing required environment variables: SUPABASE_URL and SUPABASE_KEY'); + console.error('Please check your .env file and ensure these variables are set.'); + process.exit(1); +} + +// Safety gate to prevent running in production +if (ALLOW_DEV_SEED !== 'true') { + console.error('🚨 Safety Gate: Refusing to run seed script.'); + console.error('This script modifies the database and should only run in development.'); + console.error('Set ALLOW_DEV_SEED=true in your environment to proceed.'); + process.exit(1); +} + +// Validate dev credentials are provided (no defaults for security) +if (!DEV_EMAIL || !DEV_PASSWORD) { + console.error('🚨 Security Check: DEV_EMAIL and DEV_PASSWORD must be explicitly set.'); + console.error('This prevents accidentally using predictable default credentials.'); + console.error('Please set DEV_EMAIL and DEV_PASSWORD in your environment variables.'); + process.exit(1); +} + +// Additional safety: warn if URL looks like production +if (SUPABASE_URL.includes('prod') || SUPABASE_URL.includes('live') || SUPABASE_URL.includes('main')) { + console.error('🚨 Production URL Detected: SUPABASE_URL appears to be a production endpoint.'); + console.error('Refusing to run seed script against production database.'); + console.error('URL:', SUPABASE_URL); + process.exit(1); +} + +console.log('✅ Safety checks passed. Proceeding with development seeding...'); + +const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); + +function isoAtLocalMidday(date) { + const d = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0); + return d.toISOString(); +} + +// Paginated user lookup to handle projects with >50 users +async function findUserByEmail(email) { + let page = 1; + const perPage = 200; // Use higher page size for efficiency + + while (true) { + const { data, error } = await supabase.auth.admin.listUsers({ page, perPage }); + if (error) throw new Error(`listUsers failed: ${error.message}`); + + const foundUser = data.users.find((u) => u.email === email); + if (foundUser) return foundUser; + + // If we get fewer users than perPage, we've reached the end + if (!data.users.length || data.users.length < perPage) { + return null; + } + + page += 1; + } +} + +async function getOrCreateDevUser() { + const existing = await findUserByEmail(DEV_EMAIL); + if (existing) return existing; + + const { data: created, error: createError } = await supabase.auth.admin.createUser({ + email: DEV_EMAIL, + password: DEV_PASSWORD, + email_confirm: true, + }); + if (createError) throw new Error(`createUser failed: ${createError.message}`); + return created.user; +} + +async function upsertPatient(user) { + const payload = { + id: user.id, + patient_name: 'Dev User', + age: 22, + is_adult: true, + phone: '9999999999', + email: DEV_EMAIL, + gender: 'other', + country: 'India', + onboarded_on: new Date().toISOString(), + }; + + const { error } = await supabase.from('patient').upsert(payload, { onConflict: 'id' }); + if (error) throw new Error(`upsert patient failed: ${error.message}`); +} + +async function seedReportsForPatient(patientId) { + // Reset only report-related demo rows for deterministic local tests. + const { error: delLogsErr } = await supabase.from('daily_activity_logs').delete().eq('patient_id', patientId); + if (delLogsErr) throw new Error(`delete daily_activity_logs failed: ${delLogsErr.message}`); + + const { error: delActErr } = await supabase.from('daily_activities').delete().eq('patient_id', patientId); + if (delActErr) throw new Error(`delete daily_activities failed: ${delActErr.message}`); + + const { error: delGoalErr } = await supabase.from('therapy_goal').delete().eq('patient_id', patientId); + if (delGoalErr) throw new Error(`delete therapy_goal failed: ${delGoalErr.message}`); + + const activityRows = [ + { + id: uuidv4(), + activity_name: 'Morning Communication Practice', + activity_list: [ + { id: uuidv4(), activity: 'Eye contact for 10 seconds', is_completed: true }, + { id: uuidv4(), activity: 'Name response in 3 tries', is_completed: true }, + ], + is_active: true, + patient_id: patientId, + }, + { + id: uuidv4(), + activity_name: 'Sensory Regulation Routine', + activity_list: [ + { id: uuidv4(), activity: 'Breathing set', is_completed: true }, + { id: uuidv4(), activity: 'Weighted blanket 15 min', is_completed: false }, + ], + is_active: true, + patient_id: patientId, + }, + { + id: uuidv4(), + activity_name: 'Social Story Review', + activity_list: [ + { id: uuidv4(), activity: 'Read social story', is_completed: true }, + { id: uuidv4(), activity: 'Answer 3 follow-up questions', is_completed: true }, + ], + is_active: true, + patient_id: patientId, + }, + ]; + + const { error: activityError } = await supabase.from('daily_activities').insert(activityRows); + if (activityError) throw new Error(`insert daily_activities failed: ${activityError.message}`); + + const today = new Date(); + // Dates spread across the month PLUS today so screens show data immediately + const logDates = [ + new Date(today.getFullYear(), today.getMonth(), Math.max(1, today.getDate() - 6)), + new Date(today.getFullYear(), today.getMonth(), Math.max(1, today.getDate() - 3)), + new Date(today.getFullYear(), today.getMonth(), Math.max(1, today.getDate() - 1)), + today, // always include today so getTodayActivities finds a log + ]; + + const logs = [ + { + id: uuidv4(), + activity_id: activityRows[0].id, + patient_id: patientId, + date: isoAtLocalMidday(logDates[0]), + activity_items: activityRows[0].activity_list, + }, + { + id: uuidv4(), + activity_id: activityRows[1].id, + patient_id: patientId, + date: isoAtLocalMidday(logDates[1]), + activity_items: activityRows[1].activity_list, + }, + { + id: uuidv4(), + activity_id: activityRows[2].id, + patient_id: patientId, + date: isoAtLocalMidday(logDates[2]), + activity_items: activityRows[2].activity_list, + }, + // Today's log — makes Daily Activities screen show data on first open + { + id: uuidv4(), + activity_id: activityRows[0].id, + patient_id: patientId, + date: isoAtLocalMidday(logDates[3]), + activity_items: activityRows[0].activity_list, + }, + ]; + + const { error: logsError } = await supabase.from('daily_activity_logs').insert(logs); + if (logsError) throw new Error(`insert daily_activity_logs failed: ${logsError.message}`); + + // Therapy goals: one older entry + one for TODAY so getTherapyGoals(today) finds data + const { error: oldGoalError } = await supabase.from('therapy_goal').insert({ + id: uuidv4(), + patient_id: patientId, + performed_on: isoAtLocalMidday(logDates[2]), + regressions: [ + { id: uuidv4(), name: 'Eye contact dropped during transitions' }, + { id: uuidv4(), name: 'Delayed response to verbal cue' }, + ], + }); + if (oldGoalError) throw new Error(`insert therapy_goal (old) failed: ${oldGoalError.message}`); + + const { error: goalError } = await supabase.from('therapy_goal').insert({ + id: uuidv4(), + patient_id: patientId, + performed_on: isoAtLocalMidday(today), + goals: [ + { id: uuidv4(), name: 'Improve eye contact duration' }, + { id: uuidv4(), name: 'Respond to name within 2 tries' } + ], + observations: [ + { id: uuidv4(), name: 'Patient showed progress in structured settings' } + ], + regressions: [ + { id: uuidv4(), name: 'Distraction in noisy environments' }, + ], + activities: activityRows.map(a => ({ id: uuidv4(), name: a.activity_name })), + }); + if (goalError) throw new Error(`insert therapy_goal (today) failed: ${goalError.message}`); +} + +(async () => { + try { + const user = await getOrCreateDevUser(); + await upsertPatient(user); + await seedReportsForPatient(user.id); + + const [activities, logs, goals] = await Promise.all([ + supabase.from('daily_activities').select('id', { count: 'exact', head: true }).eq('patient_id', user.id), + supabase.from('daily_activity_logs').select('id', { count: 'exact', head: true }).eq('patient_id', user.id), + supabase.from('therapy_goal').select('id', { count: 'exact', head: true }).eq('patient_id', user.id), + ]); + + // Validate count query results to ensure errors surface during verification + if (activities.error) throw new Error(`count daily_activities failed: ${activities.error.message}`); + if (logs.error) throw new Error(`count daily_activity_logs failed: ${logs.error.message}`); + if (goals.error) throw new Error(`count therapy_goal failed: ${goals.error.message}`); + + console.log(JSON.stringify({ + email: DEV_EMAIL, + userId: user.id, + dailyActivities: activities.count ?? 0, + dailyActivityLogs: logs.count ?? 0, + therapyGoals: goals.count ?? 0, + }, null, 2)); + } catch (error) { + console.error(error.message || error); + process.exit(1); + } +})();