Files
dayMaker_lp/lib/Screens/week_screen.dart
2026-05-17 17:05:01 +01:00

552 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../constants/item_categories.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 _weekdayNames = [
'Seg',
'Ter',
'Qua',
'Qui',
'Sex',
'Sáb',
'Dom',
];
static const _weekdayNamesLong = [
'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) {
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();
setState(() {
_dayItems = items;
_isLoading = false;
});
} catch (e) {
debugPrint('Error loading day items: $e');
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: (ctx) => _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) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red),
);
}
}
}
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) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red),
);
}
}
}
@override
Widget build(BuildContext context) {
final start = _startOfWeek;
final days = List.generate(7, (i) => start.add(Duration(days: i)));
return Scaffold(
backgroundColor: const Color(0xFFFFE5CC),
appBar: AppBar(
backgroundColor: const Color(0xFF0066CC),
elevation: 0,
title: const Text(
'Minha Semana',
style: TextStyle(color: Colors.white, fontSize: 20),
),
centerTitle: true,
),
body: Column(
children: [
// Week selector
Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
child: Row(
children: days.map((day) {
final isSelected =
day.year == _selectedDay.year &&
day.month == _selectedDay.month &&
day.day == _selectedDay.day;
final isToday =
day.year == DateTime.now().year &&
day.month == DateTime.now().month &&
day.day == DateTime.now().day;
return Expanded(
child: GestureDetector(
onTap: () {
setState(() => _selectedDay = day);
_loadDayItems();
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF0066CC)
: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isToday
? const Color(0xFF0066CC)
: Colors.transparent,
width: 2,
),
),
child: Column(
children: [
Text(
_weekdayNames[day.weekday - 1],
style: TextStyle(
fontSize: 12,
color: isSelected
? Colors.white
: const Color(0xFF666666),
),
),
const SizedBox(height: 4),
Text(
'${day.day}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isSelected
? Colors.white
: const Color(0xFF333333),
),
),
],
),
),
),
);
}).toList(),
),
),
// Day title
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 12),
child: Row(
children: [
Text(
'${_weekdayNamesLong[_selectedDay.weekday - 1]}, '
'${_selectedDay.day}/${_selectedDay.month}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const Spacer(),
Text(
'${_dayItems.length} ${_dayItems.length == 1 ? "item" : "itens"}',
style: const TextStyle(
color: Color(0xFF666666),
fontSize: 13,
),
),
],
),
),
// Items list
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _dayItems.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today_outlined,
size: 56,
color: Colors.grey[400],
),
const SizedBox(height: 12),
const Text(
'Nenhum item para este dia',
style: TextStyle(
color: Color(0xFF666666),
fontSize: 16,
),
),
const SizedBox(height: 4),
const Text(
'Toque em + para adicionar',
style: TextStyle(
color: Color(0xFF999999),
fontSize: 13,
),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _dayItems.length,
itemBuilder: (_, i) => _buildItemTile(_dayItems[i]),
),
),
],
),
floatingActionButton: FloatingActionButton(
backgroundColor: const Color(0xFF0066CC),
onPressed: _addItemsToDay,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildItemTile(Map<String, dynamic> item) {
final images = item['item_images'] as List?;
final imageUrl = (images != null && images.isNotEmpty)
? images.first['image_url']
: null;
final category = itemCategories.firstWhere(
(c) => c.id == item['categoria'],
orElse: () => itemCategories.last,
);
return Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
leading: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: imageUrl != null
? Image.network(
imageUrl,
width: 56,
height: 56,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_icon(category.icon),
)
: _icon(category.icon),
),
title: Text(
item['nome'] ?? 'Sem nome',
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(category.name),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () => _removeItem(item),
),
),
);
}
Widget _icon(String icon) {
return Container(
width: 56,
height: 56,
color: const Color(0xFF0066CC).withValues(alpha: 0.1),
alignment: Alignment.center,
child: Text(icon, style: const TextStyle(fontSize: 26)),
);
}
}
// =============================================
// Bottom sheet para escolher itens
// =============================================
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: Color(0xFFFFE5CC),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 8),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(2),
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Adicionar itens ao dia',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
),
child: TextField(
onChanged: (v) => setState(() => _query = v),
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'Pesquisar...',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12),
),
),
),
),
Expanded(
child: filtered.isEmpty
? const Center(child: Text('Nenhum item disponível'))
: ListView.builder(
controller: scrollController,
itemCount: filtered.length,
itemBuilder: (_, i) {
final item = filtered[i];
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']
: null;
final category = itemCategories.firstWhere(
(c) => c.id == item['categoria'],
orElse: () => itemCategories.last,
);
return CheckboxListTile(
value: selected,
activeColor: const Color(0xFF0066CC),
onChanged: (v) {
setState(() {
if (v == true) {
_selected.add(id);
} else {
_selected.remove(id);
}
});
},
title: Text(item['nome'] ?? ''),
subtitle: Text(category.name),
secondary: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: imageUrl != null
? Image.network(
imageUrl,
width: 48,
height: 48,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
Container(
width: 48,
height: 48,
color: const Color(
0xFF0066CC,
).withValues(alpha: 0.1),
alignment: Alignment.center,
child: Text(category.icon),
),
)
: Container(
width: 48,
height: 48,
color: const Color(
0xFF0066CC,
).withValues(alpha: 0.1),
alignment: Alignment.center,
child: Text(category.icon),
),
),
);
},
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _selected.isEmpty
? null
: () => Navigator.pop(context, _selected.toList()),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: Text(
'Adicionar (${_selected.length})',
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
),
),
),
],
),
),
);
}
}