Files
dayMaker_lp/lib/Screens/week_screen.dart
Carlos Correia 967584f083 IA Funcinonal
2026-05-21 11:53:35 +01:00

678 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../constants/item_categories.dart';
import '../theme/app_theme.dart';
class WeekScreen extends StatefulWidget {
const WeekScreen({super.key});
@override
State<WeekScreen> createState() => _WeekScreenState();
}
class _WeekScreenState extends State<WeekScreen> {
DateTime _selectedDay = DateTime.now();
List<Map<String, dynamic>> _dayItems = [];
bool _isLoading = false;
static const _weekdayShort = [
'Seg',
'Ter',
'Qua',
'Qui',
'Sex',
'Sáb',
'Dom',
];
static const _weekdayLong = [
'Segunda',
'Terça',
'Quarta',
'Quinta',
'Sexta',
'Sábado',
'Domingo',
];
@override
void initState() {
super.initState();
_loadDayItems();
}
DateTime get _startOfWeek {
final now = DateTime.now();
return DateTime(
now.year,
now.month,
now.day,
).subtract(Duration(days: now.weekday - 1));
}
String _dateKey(DateTime d) =>
'${d.year.toString().padLeft(4, '0')}-'
'${d.month.toString().padLeft(2, '0')}-'
'${d.day.toString().padLeft(2, '0')}';
Future<int> _getOrCreatePlanId(DateTime day) async {
final user = Supabase.instance.client.auth.currentUser!;
final dateStr = _dateKey(day);
final existing = await Supabase.instance.client
.from('plans')
.select('id')
.eq('user_id', user.id)
.eq('data', dateStr)
.maybeSingle();
if (existing != null) return existing['id'] as int;
final created = await Supabase.instance.client
.from('plans')
.insert({'user_id': user.id, 'data': dateStr})
.select()
.single();
return created['id'] as int;
}
Future<void> _loadDayItems() async {
setState(() => _isLoading = true);
try {
final user = Supabase.instance.client.auth.currentUser;
if (user == null) return;
final dateStr = _dateKey(_selectedDay);
final plan = await Supabase.instance.client
.from('plans')
.select('id, plan_items(item_id, items(*, item_images(image_url)))')
.eq('user_id', user.id)
.eq('data', dateStr)
.maybeSingle();
if (plan == null) {
if (!mounted) return;
setState(() {
_dayItems = [];
_isLoading = false;
});
return;
}
final planItems = plan['plan_items'] as List? ?? [];
final items = planItems
.where((pi) => pi['items'] != null)
.map<Map<String, dynamic>>(
(pi) => Map<String, dynamic>.from(pi['items']),
)
.toList();
if (!mounted) return;
setState(() {
_dayItems = items;
_isLoading = false;
});
} catch (e) {
debugPrint('Error loading day: $e');
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _addItemsToDay() async {
final user = Supabase.instance.client.auth.currentUser;
if (user == null) return;
final allItems = await Supabase.instance.client
.from('items')
.select('*, item_images(image_url)')
.eq('user_id', user.id);
final available = List<Map<String, dynamic>>.from(allItems);
final existingIds = _dayItems.map((i) => i['id']).toSet();
final toShow = available
.where((i) => !existingIds.contains(i['id']))
.toList();
if (!mounted) return;
final selected = await showModalBottomSheet<List<int>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _ItemPickerSheet(items: toShow),
);
if (selected == null || selected.isEmpty) return;
try {
final planId = await _getOrCreatePlanId(_selectedDay);
final rows = selected
.map((id) => {'plan_id': planId, 'item_id': id})
.toList();
await Supabase.instance.client.from('plan_items').insert(rows);
_loadDayItems();
} catch (e) {
if (mounted) AppSnack.error(context, 'Erro: $e');
}
}
Future<void> _removeItem(Map<String, dynamic> item) async {
try {
final planId = await _getOrCreatePlanId(_selectedDay);
await Supabase.instance.client
.from('plan_items')
.delete()
.eq('plan_id', planId)
.eq('item_id', item['id']);
_loadDayItems();
} catch (e) {
if (mounted) AppSnack.error(context, 'Erro: $e');
}
}
@override
Widget build(BuildContext context) {
final days = List.generate(7, (i) => _startOfWeek.add(Duration(days: i)));
return Scaffold(
backgroundColor: AppColors.background,
body: SafeArea(
bottom: false,
child: Column(
children: [
_buildHeader(),
_buildDaysRow(days),
const SizedBox(height: 12),
_buildDayTitle(),
const SizedBox(height: 8),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _dayItems.isEmpty
? _buildEmpty()
: ListView.builder(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 120),
itemCount: _dayItems.length,
itemBuilder: (_, i) => _buildItemTile(_dayItems[i]),
),
),
],
),
),
floatingActionButton: _buildFab(),
);
}
Widget _buildHeader() {
return const Padding(
padding: EdgeInsets.fromLTRB(20, 12, 20, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Minha Semana', style: AppText.h2),
SizedBox(height: 2),
Text('Planeie o que precisa para cada dia', style: AppText.caption),
],
),
);
}
Widget _buildDaysRow(List<DateTime> days) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: days.map((day) {
final isSelected =
day.year == _selectedDay.year &&
day.month == _selectedDay.month &&
day.day == _selectedDay.day;
final today = DateTime.now();
final isToday =
day.year == today.year &&
day.month == today.month &&
day.day == today.day;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.md),
onTap: () {
setState(() => _selectedDay = day);
_loadDayItems();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
gradient: isSelected ? AppColors.brandGradient : null,
color: isSelected ? null : AppColors.surface,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: isToday && !isSelected
? AppColors.primary
: AppColors.border,
width: isToday ? 1.5 : 1,
),
boxShadow: isSelected ? AppShadows.brand : null,
),
child: Column(
children: [
Text(
_weekdayShort[day.weekday - 1],
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.white
: AppColors.textSecondary,
),
),
const SizedBox(height: 4),
Text(
'${day.day}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected
? Colors.white
: AppColors.textPrimary,
),
),
],
),
),
),
),
),
);
}).toList(),
),
);
}
Widget _buildDayTitle() {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
child: Row(
children: [
Text(
'${_weekdayLong[_selectedDay.weekday - 1]}, '
'${_selectedDay.day}/${_selectedDay.month}',
style: AppText.h3,
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppRadius.pill),
),
child: Text(
'${_dayItems.length} ${_dayItems.length == 1 ? "item" : "itens"}',
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
),
],
),
);
}
Widget _buildEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.event_available_rounded,
color: AppColors.primary,
size: 36,
),
),
const SizedBox(height: 16),
const Text('Nada planeado', style: AppText.h3),
const SizedBox(height: 4),
Text('Toque em + para adicionar itens', style: AppText.caption),
],
),
);
}
Widget _buildItemTile(Map<String, dynamic> item) {
final images = item['item_images'] as List?;
final imageUrl = (images != null && images.isNotEmpty)
? images.first['image_url'] as String?
: null;
final cat = categoryById(item['categoria'] as String?);
return Dismissible(
key: ValueKey(item['id']),
direction: DismissDirection.endToStart,
background: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.symmetric(horizontal: 24),
alignment: Alignment.centerRight,
decoration: BoxDecoration(
color: AppColors.error,
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: const Icon(Icons.delete_outline, color: Colors.white),
),
onDismissed: (_) => _removeItem(item),
child: Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: AppDecorations.card(),
clipBehavior: Clip.antiAlias,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.md),
child: Container(
width: 56,
height: 56,
color: cat.color.withValues(alpha: 0.15),
child: imageUrl != null
? Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (_, _, _) =>
Icon(cat.icon, color: cat.color, size: 24),
)
: Icon(cat.icon, color: cat.color, size: 24),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nome'] ?? '',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
color: AppColors.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
Icon(cat.icon, size: 12, color: cat.color),
const SizedBox(width: 4),
Text(
cat.name,
style: TextStyle(
fontSize: 12,
color: cat.color,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
IconButton(
icon: const Icon(
Icons.close_rounded,
color: AppColors.textTertiary,
),
onPressed: () => _removeItem(item),
),
],
),
),
),
);
}
Widget _buildFab() {
return Container(
decoration: BoxDecoration(
gradient: AppColors.brandGradient,
borderRadius: BorderRadius.circular(AppRadius.lg),
boxShadow: AppShadows.brand,
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.lg),
onTap: _addItemsToDay,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 18, vertical: 14),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add_rounded, color: Colors.white),
SizedBox(width: 6),
Text(
'Adicionar',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
);
}
}
// ============================================================
// Item picker bottom sheet
// ============================================================
class _ItemPickerSheet extends StatefulWidget {
final List<Map<String, dynamic>> items;
const _ItemPickerSheet({required this.items});
@override
State<_ItemPickerSheet> createState() => _ItemPickerSheetState();
}
class _ItemPickerSheetState extends State<_ItemPickerSheet> {
final Set<int> _selected = {};
String _query = '';
@override
Widget build(BuildContext context) {
final filtered = widget.items
.where(
(i) => (i['nome'] ?? '').toString().toLowerCase().contains(
_query.toLowerCase(),
),
)
.toList();
return DraggableScrollableSheet(
initialChildSize: 0.85,
maxChildSize: 0.95,
minChildSize: 0.5,
expand: false,
builder: (_, scrollController) => Container(
decoration: const BoxDecoration(
color: AppColors.background,
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 10),
width: 42,
height: 4,
decoration: BoxDecoration(
color: AppColors.border,
borderRadius: BorderRadius.circular(2),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 4),
child: Align(
alignment: Alignment.centerLeft,
child: Text('Adicionar itens ao dia', style: AppText.h3),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 8),
child: Container(
decoration: AppDecorations.outlined(radius: AppRadius.pill),
child: TextField(
onChanged: (v) => setState(() => _query = v),
decoration: const InputDecoration(
prefixIcon: Icon(
Icons.search_rounded,
color: AppColors.textSecondary,
),
hintText: 'Pesquisar...',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 14),
),
),
),
),
Expanded(
child: filtered.isEmpty
? Center(
child: Text(
'Sem itens disponíveis',
style: AppText.caption,
),
)
: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
itemCount: filtered.length,
itemBuilder: (_, i) => _buildPickerTile(filtered[i]),
),
),
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.all(20),
child: AppButton(
label: _selected.isEmpty
? 'Selecione itens'
: 'Adicionar (${_selected.length})',
icon: Icons.check_rounded,
onPressed: _selected.isEmpty
? null
: () => Navigator.pop(context, _selected.toList()),
),
),
),
],
),
),
);
}
Widget _buildPickerTile(Map<String, dynamic> item) {
final id = item['id'] as int;
final selected = _selected.contains(id);
final images = item['item_images'] as List?;
final imageUrl = (images != null && images.isNotEmpty)
? images.first['image_url'] as String?
: null;
final cat = categoryById(item['categoria'] as String?);
return Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(AppRadius.md),
border: Border.all(
color: selected ? AppColors.primary : AppColors.border,
width: selected ? 1.5 : 1,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(AppRadius.md),
onTap: () {
setState(() {
if (selected) {
_selected.remove(id);
} else {
_selected.add(id);
}
});
},
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.sm),
child: Container(
width: 48,
height: 48,
color: cat.color.withValues(alpha: 0.15),
child: imageUrl != null
? Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (_, _, _) =>
Icon(cat.icon, color: cat.color, size: 22),
)
: Icon(cat.icon, color: cat.color, size: 22),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['nome'] ?? '',
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
cat.name,
style: TextStyle(
fontSize: 12,
color: cat.color,
fontWeight: FontWeight.w600,
),
),
],
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 180),
width: 24,
height: 24,
decoration: BoxDecoration(
color: selected ? AppColors.primary : Colors.transparent,
shape: BoxShape.circle,
border: Border.all(
color: selected ? AppColors.primary : AppColors.border,
width: 2,
),
),
child: selected
? const Icon(
Icons.check_rounded,
color: Colors.white,
size: 16,
)
: null,
),
],
),
),
),
),
);
}
}