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:
590
.planning/phases/02-household-creation/02-04-PLAN.md
Normal file
590
.planning/phases/02-household-creation/02-04-PLAN.md
Normal 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>
|
||||
Reference in New Issue
Block a user