docs(02): create phase 2 household creation plans

Phase 2: Household Creation & Invites
- 6 plans in 4 waves covering SHARE-01 through SHARE-05
- Data models, repository pattern, and Supabase integration
- Database schema with RLS policies for multi-tenant isolation
- State management with Riverpod and business logic use cases
- Complete UI components for household management
- Navigation integration with authentication flow

Ready for execution: /gsd:execute-phase 2
This commit is contained in:
Dani B
2026-01-28 15:50:09 -05:00
parent 80c59f1a5f
commit e20858f608
7 changed files with 3023 additions and 0 deletions

View File

@@ -0,0 +1,590 @@
---
phase: 02-household-creation
plan: 04
type: execute
wave: 3
depends_on: [02-03, 02-01]
files_modified:
- lib/features/household/presentation/pages/create_household_page.dart
- lib/features/household/presentation/pages/join_household_page.dart
- lib/features/household/presentation/pages/household_list_page.dart
- lib/features/household/presentation/widgets/invite_code_dialog.dart
- lib/features/household/presentation/widgets/household_card.dart
autonomous: true
must_haves:
truths:
- "Users can create households with validation and feedback"
- "Users can join households using 8-character invite codes"
- "Household management UI provides clear navigation and state feedback"
- "Invite code generation works with proper security and expiration"
- "All household operations handle loading states and errors gracefully"
artifacts:
- path: "lib/features/household/presentation/pages/create_household_page.dart"
provides: "Household creation interface"
contains: "CreateHouseholdPage", "form validation", "create button"
- path: "lib/features/household/presentation/pages/join_household_page.dart"
provides: "Household joining interface with invite code input"
contains: "JoinHouseholdPage", "invite code field", "join button"
- path: "lib/features/household/presentation/pages/household_list_page.dart"
provides: "Household overview and management"
contains: "HouseholdListPage", "household cards", "create/join buttons"
- path: "lib/features/household/presentation/widgets/invite_code_dialog.dart"
provides: "Invite code generation and display"
contains: "InviteCodeDialog", "code display", "share functionality"
- path: "lib/features/household/presentation/widgets/household_card.dart"
provides: "Household display component"
contains: "HouseholdCard", "member count", "role display"
key_links:
- from: "lib/features/household/presentation/pages/create_household_page.dart"
to: "lib/features/household/providers/household_provider.dart"
via: "Riverpod consumer"
pattern: "ref.read.*householdProvider"
- from: "lib/features/household/presentation/pages/household_list_page.dart"
to: "lib/providers/auth_provider.dart"
via: "authentication state"
pattern: "ref.watch.*authProvider"
---
<objective>
Create household management UI components for creating, joining, and managing households with proper validation and user feedback.
Purpose: Provide intuitive interfaces for all household operations from SHARE-01 through SHARE-05 with consistent UX and error handling.
Output: Complete household management UI ready for integration with navigation system.
</objective>
<execution_context>
@~/.opencode/get-shit-done/workflows/execute-plan.md
@~/.opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
# UI Pattern References
@lib/features/authentication/presentation/pages/signup_page.dart
@lib/features/authentication/presentation/pages/login_page.dart
@lib/features/authentication/presentation/widgets/auth_button.dart
</context>
<tasks>
<task type="auto">
<name>Create Household Card Widget</name>
<files>lib/features/household/presentation/widgets/household_card.dart</files>
<action>
Create household card component following auth UI patterns:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../domain/entities/household_entity.dart';
class HouseholdCard extends ConsumerWidget {
final HouseholdEntity household;
final VoidCallback? onTap;
final VoidCallback? onGenerateInvite;
final VoidCallback? onLeave;
final bool showActions;
const HouseholdCard({
Key? key,
required this.household,
this.onTap,
this.onGenerateInvite,
this.onLeave,
this.showActions = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
child: Text(
household.name.isNotEmpty ? household.name[0].toUpperCase() : 'H',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
household.name,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text('${household.members.length} member${household.members.length != 1 ? 's' : ''}'),
if (household.currentInvite != null)
Text(
'Invite active',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 12,
),
),
],
),
trailing: showActions ? _buildActions(context) : null,
onTap: onTap,
),
);
}
Widget _buildActions(BuildContext context) {
return PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'invite':
onGenerateInvite?.call();
break;
case 'leave':
onLeave?.call();
break;
}
},
itemBuilder: (context) => [
if (household.isCurrentUserOwner || household.members.any(m => m.isCurrentUser && m.role.name != 'viewer'))
const PopupMenuItem(
value: 'invite',
child: Row(
children: [
Icon(Icons.person_add),
SizedBox(width: 8),
Text('Generate Invite'),
],
),
),
if (!household.isCurrentUserOwner || household.members.length > 1)
const PopupMenuItem(
value: 'leave',
child: Row(
children: [
Icon(Icons.exit_to_app),
SizedBox(width: 8),
Text('Leave Household'),
],
),
),
],
);
}
}
```
Follow Material Design card patterns and authentication UI consistency.
Include role-based action visibility (only owners/editors can generate invites).
</action>
<verify>flutter analyze lib/features/household/presentation/widgets/household_card.dart passes</verify>
<done>Household card component displays household information with appropriate actions</done>
</task>
<task type="auto">
<name>Create Invite Code Dialog</name>
<files>lib/features/household/presentation/widgets/invite_code_dialog.dart</files>
<action>
Create dialog for generating and sharing invite codes:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import '../../domain/models/household_models.dart';
import '../../providers/household_provider.dart';
class InviteCodeDialog extends ConsumerStatefulWidget {
final String householdId;
final String householdName;
const InviteCodeDialog({
Key? key,
required this.householdId,
required this.householdName,
}) : super(key: key);
@override
ConsumerState<InviteCodeDialog> createState() => _InviteCodeDialogState();
}
class _InviteCodeDialogState extends ConsumerState<InviteCodeDialog> {
InviteCodeModel? _inviteCode;
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_generateInviteCode();
}
Future<void> _generateInviteCode() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final user = ref.read(authProvider).user;
if (user == null) throw Exception('User not authenticated');
final inviteCode = await ref.read(householdProvider.notifier)
.generateInviteCode(widget.householdId, user.id);
setState(() {
_inviteCode = inviteCode;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Invite to ${widget.householdName}'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isLoading)
const Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Generating invite code...'),
],
)
else if (_error != null)
Column(
children: [
Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
size: 48,
),
const SizedBox(height: 16),
Text(
'Failed to generate invite code',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(_error!),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _generateInviteCode,
child: const Text('Try Again'),
),
],
)
else if (_inviteCode != null)
Column(
children: [
Text(
'Share this code with household members:',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
_inviteCode!.code,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
Text(
'Expires: ${DateFormat('MMM dd, yyyy').format(_inviteCode!.expiresAt)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _copyToClipboard(_inviteCode!.code),
icon: const Icon(Icons.copy),
label: const Text('Copy Code'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _shareInvite(_inviteCode!.code),
icon: const Icon(Icons.share),
label: const Text('Share'),
),
),
],
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
Future<void> _copyToClipboard(String code) async {
await Clipboard.setData(ClipboardData(text: code));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invite code copied to clipboard')),
);
}
}
Future<void> _shareInvite(String code) async {
// Share functionality would be implemented here
await _copyToClipboard(code);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invite code copied - paste to share')),
);
}
}
}
```
Include proper loading states, error handling, and copy-to-clipboard functionality.
Follow Material Design dialog patterns from authentication flows.
</action>
<verify>flutter analyze lib/features/household/presentation/widgets/invite_code_dialog.dart passes</verify>
<done>Invite code dialog provides code generation, display, and sharing functionality</done>
</task>
<task type="auto">
<name>Create Create Household Page</name>
<files>lib/features/household/presentation/pages/create_household_page.dart</files>
<action>
Create household creation page following authentication UI patterns:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import '../../providers/household_provider.dart';
import '../../domain/exceptions/household_exceptions.dart';
class CreateHouseholdPage extends ConsumerStatefulWidget {
const CreateHouseholdPage({Key? key}) : super(key: key);
@override
ConsumerState<CreateHouseholdPage> createState() => _CreateHouseholdPageState();
}
class _CreateHouseholdPageState extends ConsumerState<CreateHouseholdPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
bool _isLoading = false;
String? _error;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _createHousehold() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final user = ref.read(authProvider).user;
if (user == null) throw Exception('User not authenticated');
final household = await ref.read(householdProvider.notifier)
.createHousehold(_nameController.text.trim(), user.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Household "${household.name}" created successfully!'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop(household);
}
} catch (e) {
setState(() {
_error = _getErrorMessage(e);
_isLoading = false;
});
}
}
String _getErrorMessage(dynamic error) {
if (error is HouseholdValidationException) {
return error.message;
} else if (error is HouseholdOperationException) {
return error.message;
}
return 'Failed to create household. Please try again.';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create Household'),
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.onSurface,
elevation: 0,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32),
Icon(
Icons.home,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'Create a New Household',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Start tracking your shared inventory with family or roommates',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Household Name',
hintText: 'e.g., The Smith Family',
prefixIcon: Icon(Icons.home),
),
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.name],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a household name';
}
if (value.trim().length > 100) {
return 'Household name too long (max 100 characters)';
}
return null;
},
onFieldSubmitted: (_) => _createHousehold(),
),
const SizedBox(height: 24),
if (_error != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _createHousehold,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create Household'),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Cancel'),
),
],
),
),
),
),
);
}
}
```
Follow authentication page patterns for consistency.
Include proper validation, loading states, and error handling.
</action>
<verify>flutter analyze lib/features/household/presentation/pages/create_household_page.dart passes</verify>
<done>Create household page provides form validation and user feedback</done>
</task>
</tasks>
<verification>
1. Household card component displays household information with role-based actions
2. Invite code dialog handles generation, display, copying, and sharing
3. Create household page follows authentication UI patterns with proper validation
4. All components handle loading states and errors appropriately
5. UI is consistent with existing authentication flows
</verification>
<success_criteria>
Household management UI components are complete with proper validation, error handling, and user feedback ready for integration.
</success_criteria>
<output>
After completion, create `.planning/phases/02-household-creation/02-04-SUMMARY.md` with household UI components implementation summary
</output>