Skip to content

Commit 4786a8f

Browse files
committed
Add feature controllers, models, repos & health updates
Adds a large set of feature-focused controllers, data models, and repositories plus related view/route and pubspec updates. Key additions: - New controllers/providers: gear_reviews, knowledge_base, pet_breed (breed identifier + history), pet_events, pet_insurance, pet_memorial, pet_nutrition, pet_sitter, pet_training and related providers. - New models: GearReview, KnowledgeArticle, PetEvent, PetFriendlyPlace, PetMemorialEntry and others used by feature repos. - New repositories: adoption, community_group, and a consolidated feature_repositories (training, nutrition, gear reviews, memorials, etc.) backed by Supabase utilities. - HealthController enhancements: include upcomingAppointments in state, overdue appointment counting, and an idempotent 30-day medication dose generation flow (times/frequency helpers and repo call). Also triggers dose regeneration when medications change. - PetController updates: photo removal method (storage cleanup + DB clear) and a breed suggestions provider for autocomplete. - Added docs: remaining_issues_plan.md and analysis_results.txt. - Misc: multiple view files, routes, and package files updated to wire in new features. These changes implement many previously-mocked/stubbed features (events, groups, adoption, nutrition, training, gear reviews, memorials, breed ID) and improve health/medication handling and pet media management.
1 parent 1cd0f4f commit 4786a8f

43 files changed

Lines changed: 6152 additions & 1730 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

analysis_results.txt

14.1 KB
Binary file not shown.

docs/plan/remaining_issues_plan.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# PetSphere – Remaining Issues & Implementation Plan
2+
3+
> **Generated:** May 7, 2026 | Based on GitHub issues + docs/codebase cross-check
4+
5+
## ✅ Already Closed (18 issues)
6+
7+
| # | Title | Epic |
8+
|---|-------|------|
9+
| #23 | Cart persistence (SharedPreferences) | #57 |
10+
| #24 | FCM deep-link navigation | #59 |
11+
| #25 | Appointment sync (HealthController → PetCare) | #54 |
12+
| #26 | Server-side stock validation | #57 |
13+
| #27 | Feed repo compile error | #58 |
14+
| #28 | Auth: profile upsert fatal | #58 |
15+
| #30 | Testing foundation | #58 |
16+
| #31 | Supabase migrations workflow | #58 |
17+
| #32 | Stripe payment integration | #57 |
18+
| #35 | Offline support baseline | #55 |
19+
| #38 | Route hardening | #58 |
20+
| #40 | Stories expiry filter | #60 |
21+
| #42 | Comment author pet details | #60 |
22+
| #54 | EPIC: Care & Health Improvements ||
23+
| #57 | EPIC: Marketplace Reliability ||
24+
| #58 | EPIC: Foundation & Release Blockers ||
25+
| #59 | EPIC: Notifications & FCM ||
26+
27+
---
28+
29+
## 🔴 Open Issues — 29 remaining
30+
31+
### Group A: Close-ready (implemented but not closed)
32+
These were implemented in the codebase but GitHub issues left open.
33+
34+
| # | Title | Action |
35+
|---|-------|--------|
36+
| #29 | Follower count N+1 → batch `fetchPetFollowerCounts` done | Close |
37+
| #39 | Care cache flicker → payload dedup done | Close |
38+
| #41 | Realtime channel cleanup → feed + chat lifecycle done | Close |
39+
| #48 | Supabase debug fallback config → `SUPABASE_ALLOW_EMBEDDED_DEBUG_FALLBACK` done | Close |
40+
41+
### Group B: Health sub-issues (P1)
42+
43+
| # | Title | Status |
44+
|---|-------|--------|
45+
| #43 | Appointment overdue alerts in UI state | ❌ Not implemented |
46+
| #44 | Auto-generate medication doses (30-day schedule) | ❌ Not implemented |
47+
| #47 | Vaccination recurrence via reference schedule table | ❌ Not implemented |
48+
49+
### Group C: Pet management (P2)
50+
51+
| # | Title | Status |
52+
|---|-------|--------|
53+
| #45 | Pet photo delete → storage cleanup | ❌ Not implemented |
54+
| #46 | Breed autocomplete + validation | ❌ Not implemented |
55+
56+
### Group D: All stub screens (Epic #56 — P1-P2)
57+
58+
| # | Screen | Status |
59+
|---|--------|--------|
60+
| #33 | Vet Booking (reuse `pet_vet_appointments` table) | ❌ Mock only |
61+
| #34 | Lost & Found (new tables + workflow) | ❌ Mock only |
62+
| #36 | Community Groups (groups + membership) | ❌ Mock only |
63+
| #37 | Adoption Center (listings + applications) | ❌ Mock only |
64+
| #49 | Meta: complete all remaining mock screens | ❌ Open |
65+
| #50 | Pet Training (programs + progress per pet) | ❌ Mock only |
66+
| #51 | Pet Sitter Dashboard (profiles + booking) | ❌ Mock only |
67+
| #52 | Pet Insurance Hub (claims + status) | ❌ Mock only |
68+
| #62 | Pet Friendly Places (data-driven list/map) | ❌ Mock only |
69+
| #63 | Nutrition Planner (personalized + persistent) | ❌ Mock only |
70+
| #64 | Knowledge Base (articles + search) | ❌ Mock only |
71+
| #65 | Gear Reviews (CRUD + ratings) | ❌ Mock only |
72+
| #66 | Breed Identifier (real ML/API, not fake scan) | ❌ Fake scan |
73+
| #67 | Pet Memorial (archival + entries) | ❌ Mock only |
74+
| #68 | Pet Events Discovery (events + RSVP) | ❌ Mock only |
75+
76+
### Open Epics (need sub-issues done first)
77+
78+
| # | Title | Blocking Sub-issues |
79+
|---|-------|---------------------|
80+
| #55 | EPIC: Offline, Performance & Query Efficiency | #29 done → close |
81+
| #60 | EPIC: Social Feed, Stories & Realtime Quality | #40,#42 done; #41 close |
82+
| #61 | EPIC: Security & Config Hygiene | #48 close |
83+
| #69 | EPIC: Pet Profile Quality | #45, #46 |
84+
| #56 | EPIC: Complete Stub Screens | All #33–68 sub-issues |
85+
86+
---
87+
88+
## 📋 Sequential Implementation Order
89+
90+
```
91+
Phase 1 (immediate): Close already-done issues #29, #39, #41, #48
92+
Phase 2: Health issues #43, #44, #47
93+
Phase 3: Pet mgmt #45, #46
94+
Phase 4 (stub screens):
95+
4a. #33 Vet Booking
96+
4b. #34 Lost & Found
97+
4c. #36 Community Groups
98+
4d. #37 Adoption Center
99+
4e. #50 Pet Training
100+
4f. #52 Pet Insurance Hub
101+
4g. #51 Pet Sitter Dashboard
102+
4h. #63 Nutrition Planner
103+
4i. #62 Pet Friendly Places
104+
4j. #64 Knowledge Base
105+
4k. #65 Gear Reviews
106+
4l. #67 Pet Memorial
107+
4m. #68 Pet Events Discovery
108+
4n. #66 Breed Identifier (API-based)
109+
Phase 5: Close epics #55, #60, #61, #69, #56
110+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import '../models/gear_review_models.dart';
3+
import '../repositories/feature_repositories.dart';
4+
5+
final gearReviewsRepositoryProvider = Provider<GearReviewsRepository>((ref) {
6+
return GearReviewsRepository();
7+
});
8+
9+
final gearReviewsProvider = FutureProvider.family<List<GearReview>, String?>((ref, category) async {
10+
final repository = ref.watch(gearReviewsRepositoryProvider);
11+
return repository.fetchReviews(category: category);
12+
});
13+
14+
class SelectedGearCategoryNotifier extends Notifier<String?> {
15+
@override
16+
String? build() => null;
17+
void set(String? category) => state = category;
18+
}
19+
20+
final selectedGearCategoryProvider =
21+
NotifierProvider<SelectedGearCategoryNotifier, String?>(() {
22+
return SelectedGearCategoryNotifier();
23+
});
24+
25+
final filteredGearReviewsProvider = FutureProvider<List<GearReview>>((ref) async {
26+
final category = ref.watch(selectedGearCategoryProvider);
27+
return ref.watch(gearReviewsProvider(category).future);
28+
});

lib/controllers/health_controller.dart

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:developer';
2+
13
import 'package:flutter/foundation.dart';
24
import 'package:flutter_riverpod/flutter_riverpod.dart';
35

@@ -18,6 +20,8 @@ class HealthState {
1820
final List<PetAllergy> allergies;
1921
final List<ParasitePrevention> parasitePrevention;
2022
final List<DentalLog> dentalLogs;
23+
/// Upcoming/overdue vet appointments (scheduled status only).
24+
final List<PetVetAppointment> upcomingAppointments;
2125
final bool isLoading;
2226
final String? error;
2327
final String? activePetId;
@@ -28,6 +32,7 @@ class HealthState {
2832
this.allergies = const [],
2933
this.parasitePrevention = const [],
3034
this.dentalLogs = const [],
35+
this.upcomingAppointments = const [],
3136
this.isLoading = false,
3237
this.error,
3338
this.activePetId,
@@ -71,11 +76,17 @@ class HealthState {
7176
return matches.first;
7277
}
7378

74-
/// Number of active health alerts (overdue medications, parasite, etc.)
79+
/// Appointments past their scheduled_at that are still 'scheduled' → overdue.
80+
List<PetVetAppointment> get overdueAppointments => upcomingAppointments
81+
.where((a) => a.status == 'scheduled' && a.scheduledAt.isBefore(DateTime.now()))
82+
.toList();
83+
84+
/// Number of active health alerts (overdue medications, parasite, overdue appts).
7585
int get alertCount {
7686
int count = 0;
7787
count += overdueParasite.length;
7888
count += todayDoses.where((d) => d.isOverdue).length;
89+
count += overdueAppointments.length;
7990
return count;
8091
}
8192

@@ -94,6 +105,7 @@ class HealthState {
94105
List<PetAllergy>? allergies,
95106
List<ParasitePrevention>? parasitePrevention,
96107
List<DentalLog>? dentalLogs,
108+
List<PetVetAppointment>? upcomingAppointments,
97109
bool? isLoading,
98110
String? error,
99111
bool clearError = false,
@@ -105,6 +117,7 @@ class HealthState {
105117
allergies: allergies ?? this.allergies,
106118
parasitePrevention: parasitePrevention ?? this.parasitePrevention,
107119
dentalLogs: dentalLogs ?? this.dentalLogs,
120+
upcomingAppointments: upcomingAppointments ?? this.upcomingAppointments,
108121
isLoading: isLoading ?? this.isLoading,
109122
error: clearError ? null : (error ?? this.error),
110123
activePetId: activePetId ?? this.activePetId,
@@ -142,6 +155,7 @@ class HealthNotifier extends Notifier<HealthState> {
142155
_repo.fetchAllergies(petId),
143156
_repo.fetchParasitePrevention(petId),
144157
_repo.fetchDentalLogs(petId),
158+
_repo.fetchUpcomingAppointments(petId),
145159
]);
146160
if (!ref.mounted) return;
147161
state = state.copyWith(
@@ -150,6 +164,7 @@ class HealthNotifier extends Notifier<HealthState> {
150164
allergies: results[2] as List<PetAllergy>,
151165
parasitePrevention: results[3] as List<ParasitePrevention>,
152166
dentalLogs: results[4] as List<DentalLog>,
167+
upcomingAppointments: results[5] as List<PetVetAppointment>,
153168
isLoading: false,
154169
);
155170
} catch (e) {
@@ -171,6 +186,8 @@ class HealthNotifier extends Notifier<HealthState> {
171186
state = state.copyWith(
172187
medications: [saved, ...state.medications],
173188
);
189+
// Generate dose schedule for the next 30 days (#44).
190+
await _generateUpcomingDoses(saved);
174191
} catch (e) {
175192
state = state.copyWith(error: e.toString());
176193
}
@@ -183,6 +200,8 @@ class HealthNotifier extends Notifier<HealthState> {
183200
medications:
184201
state.medications.map((m) => m.id == saved.id ? saved : m).toList(),
185202
);
203+
// Regenerate future doses when frequency/schedule changes (#44).
204+
await _generateUpcomingDoses(saved);
186205
} catch (e) {
187206
state = state.copyWith(error: e.toString());
188207
}
@@ -350,6 +369,78 @@ class HealthNotifier extends Notifier<HealthState> {
350369
state = state.copyWith(error: e.toString());
351370
}
352371
}
372+
373+
// ── Dose generation (#44) ────────────────────────────────────────────────
374+
375+
/// Generates scheduled dose rows for [med] for the next 30 days.
376+
/// Idempotent: existing doses for a time slot are not duplicated.
377+
Future<void> _generateUpcomingDoses(PetMedication med) async {
378+
if (!med.isActive) return;
379+
if (med.frequency == 'as_needed') return;
380+
381+
final now = DateTime.now();
382+
final end = now.add(const Duration(days: 30));
383+
final doses = <MedicationDose>[];
384+
385+
DateTime cursor = med.startDate.isAfter(now) ? med.startDate : now;
386+
cursor = DateTime(cursor.year, cursor.month, cursor.day);
387+
388+
while (cursor.isBefore(end)) {
389+
if (med.endDate != null && cursor.isAfter(med.endDate!)) break;
390+
391+
final timesForDay = _timesForFrequency(med);
392+
for (final hour in timesForDay) {
393+
final scheduled = cursor.add(Duration(hours: hour));
394+
if (scheduled.isBefore(now.subtract(const Duration(hours: 1)))) continue;
395+
doses.add(MedicationDose(
396+
id: '',
397+
medicationId: med.id,
398+
petId: med.petId,
399+
scheduledFor: scheduled,
400+
skipped: false,
401+
));
402+
}
403+
cursor = _nextCursor(med.frequency, cursor);
404+
}
405+
406+
if (doses.isEmpty) return;
407+
try {
408+
await _repo.generateDosesIdempotent(doses);
409+
// Refresh today's doses so UI stays in sync.
410+
if (!ref.mounted) return;
411+
final today = await _repo.fetchTodayDoses(med.petId);
412+
state = state.copyWith(todayDoses: today);
413+
} catch (e) {
414+
log('Dose generation failed: $e', name: 'HealthNotifier');
415+
}
416+
}
417+
418+
List<int> _timesForFrequency(PetMedication med) {
419+
if (med.timesOfDay.isNotEmpty) {
420+
return med.timesOfDay.map((t) {
421+
switch (t) {
422+
case 'morning': return 8;
423+
case 'noon': return 12;
424+
case 'evening': return 18;
425+
case 'night': return 21;
426+
default: return 8;
427+
}
428+
}).toList();
429+
}
430+
switch (med.frequency) {
431+
case 'twice_daily': return [8, 20];
432+
case 'three_times_daily': return [8, 14, 20];
433+
default: return [8];
434+
}
435+
}
436+
437+
DateTime _nextCursor(String frequency, DateTime from) {
438+
switch (frequency) {
439+
case 'weekly': return from.add(const Duration(days: 7));
440+
case 'monthly': return DateTime(from.year, from.month + 1, from.day);
441+
default: return from.add(const Duration(days: 1)); // daily variants
442+
}
443+
}
353444
}
354445

355446
// ─────────────────────────────────────────────────────────────────────────────
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import '../models/knowledge_base_models.dart';
3+
import '../repositories/feature_repositories.dart';
4+
5+
class KnowledgeBaseCategoryNotifier extends Notifier<String> {
6+
@override
7+
String build() => 'All Topics';
8+
void set(String category) => state = category;
9+
}
10+
11+
final knowledgeBaseCategoryProvider =
12+
NotifierProvider<KnowledgeBaseCategoryNotifier, String>(() {
13+
return KnowledgeBaseCategoryNotifier();
14+
});
15+
16+
class KnowledgeBaseSearchQueryNotifier extends Notifier<String> {
17+
@override
18+
String build() => '';
19+
void set(String query) => state = query;
20+
}
21+
22+
final knowledgeBaseSearchQueryProvider =
23+
NotifierProvider<KnowledgeBaseSearchQueryNotifier, String>(() {
24+
return KnowledgeBaseSearchQueryNotifier();
25+
});
26+
27+
final featuredArticlesProvider = FutureProvider<List<KnowledgeArticle>>((ref) async {
28+
return ref.watch(knowledgeBaseRepositoryProvider).fetchFeaturedArticles();
29+
});
30+
31+
final knowledgeBaseArticlesProvider = FutureProvider<List<KnowledgeArticle>>((ref) async {
32+
final category = ref.watch(knowledgeBaseCategoryProvider);
33+
final query = ref.watch(knowledgeBaseSearchQueryProvider);
34+
return ref.watch(knowledgeBaseRepositoryProvider).fetchArticles(
35+
category: category,
36+
query: query,
37+
);
38+
});
39+
40+
final knowledgeBaseRepositoryProvider = Provider((ref) => knowledgeBaseRepository);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import '../repositories/feature_repositories.dart';
3+
4+
class PetBreedController extends Notifier<AsyncValue<BreedScan?>> {
5+
@override
6+
AsyncValue<BreedScan?> build() {
7+
return const AsyncValue.data(null);
8+
}
9+
10+
Future<void> identifyBreed(String imagePath) async {
11+
state = const AsyncValue.loading();
12+
try {
13+
final repository = ref.read(breedIdentifierRepositoryProvider);
14+
final result = await repository.identifyBreed(imagePath);
15+
state = AsyncValue.data(result);
16+
17+
// Save to history
18+
await repository.saveScan(result);
19+
20+
// Refresh history provider
21+
ref.invalidate(breedScanHistoryProvider);
22+
} catch (e, st) {
23+
state = AsyncValue.error(e, st);
24+
}
25+
}
26+
27+
void reset() {
28+
state = const AsyncValue.data(null);
29+
}
30+
}
31+
32+
final breedIdentifierRepositoryProvider = Provider((ref) => breedIdentifierRepository);
33+
34+
final breedIdentifierControllerProvider =
35+
NotifierProvider<PetBreedController, AsyncValue<BreedScan?>>(() {
36+
return PetBreedController();
37+
});
38+
39+
final breedScanHistoryProvider = FutureProvider<List<BreedScan>>((ref) async {
40+
final repository = ref.watch(breedIdentifierRepositoryProvider);
41+
return repository.fetchScanHistory();
42+
});

0 commit comments

Comments
 (0)