diff --git a/assets/logoDayMaker.png b/assets/logoDayMaker.png new file mode 100644 index 0000000..65d132f Binary files /dev/null and b/assets/logoDayMaker.png differ diff --git a/lib/Screens/add_item_screen.dart b/lib/Screens/add_item_screen.dart index 19a487d..9f87bed 100644 --- a/lib/Screens/add_item_screen.dart +++ b/lib/Screens/add_item_screen.dart @@ -1,4 +1,6 @@ +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../constants/item_categories.dart'; @@ -12,13 +14,26 @@ class AddItemScreen extends StatefulWidget { class _AddItemScreenState extends State { final TextEditingController _nameController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); - + ItemCategory? _selectedCategory; Subcategory? _selectedSubcategory; final Set _selectedTags = {}; - + XFile? _selectedImage; + bool _isLoading = false; + Future _pickImage() async { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1024, + imageQuality: 80, + ); + if (image != null) { + setState(() => _selectedImage = image); + } + } + @override void dispose() { _nameController.dispose(); @@ -31,7 +46,7 @@ class _AddItemScreenState extends State { _selectedCategory = category; _selectedSubcategory = null; _selectedTags.clear(); - + if (category != null && category.subcategories.isNotEmpty) { _selectedSubcategory = category.subcategories.first; _autoAssignTags(category.id, category.subcategories.first.id); @@ -43,7 +58,7 @@ class _AddItemScreenState extends State { setState(() { _selectedSubcategory = subcategory; _selectedTags.clear(); - + if (_selectedCategory != null && subcategory != null) { _autoAssignTags(_selectedCategory!.id, subcategory.id); } @@ -87,17 +102,49 @@ class _AddItemScreenState extends State { return; } - await Supabase.instance.client.from('items').insert({ - 'user_id': user.id, - 'name': _nameController.text.trim(), - 'description': _descriptionController.text.trim(), - 'category_id': _selectedCategory!.id, - 'subcategory_id': _selectedSubcategory?.id, - 'context_tags': _selectedTags.toList(), - }); + // Ensure user profile row exists in public.users (FK requirement) + await Supabase.instance.client.from('users').upsert({ + 'id': user.id, + 'nome': + user.userMetadata?['username'] ?? + user.email?.split('@').first ?? + 'Usuário', + }, onConflict: 'id'); + + final inserted = await Supabase.instance.client + .from('items') + .insert({ + 'user_id': user.id, + 'nome': _nameController.text.trim(), + 'categoria': _selectedCategory!.id, + 'tags': [ + if (_selectedSubcategory != null) _selectedSubcategory!.id, + ..._selectedTags, + ], + }) + .select() + .single(); + + // Upload image if selected + if (_selectedImage != null) { + final itemId = inserted['id']; + final file = File(_selectedImage!.path); + final fileName = + 'item_${itemId}_${DateTime.now().millisecondsSinceEpoch}.jpg'; + await Supabase.instance.client.storage + .from('avatars') + .upload(fileName, file); + final imageUrl = Supabase.instance.client.storage + .from('avatars') + .getPublicUrl(fileName); + await Supabase.instance.client.from('item_images').insert({ + 'item_id': itemId, + 'image_url': imageUrl, + }); + } _showSuccessSnackBar('Item adicionado com sucesso!'); - Navigator.pop(context); + if (mounted) Navigator.pop(context); } catch (e) { _showErrorSnackBar('Erro ao salvar item: $e'); } finally { @@ -135,6 +182,50 @@ class _AddItemScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Image picker + Center( + child: GestureDetector( + onTap: _pickImage, + child: Container( + width: 140, + height: 140, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE0E0E0)), + ), + child: _selectedImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.file( + File(_selectedImage!.path), + fit: BoxFit.cover, + ), + ) + : const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add_a_photo_outlined, + size: 40, + color: Color(0xFF0066CC), + ), + SizedBox(height: 8), + Text( + 'Adicionar foto', + style: TextStyle( + color: Color(0xFF666666), + fontSize: 12, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(height: 20), + // Name field Container( decoration: BoxDecoration( @@ -192,7 +283,7 @@ class _AddItemScreenState extends State { ), ), const SizedBox(height: 8), - + Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12), @@ -211,7 +302,10 @@ class _AddItemScreenState extends State { value: category, child: Row( children: [ - Text(category.icon, style: const TextStyle(fontSize: 24)), + Text( + category.icon, + style: const TextStyle(fontSize: 24), + ), const SizedBox(width: 12), Text(category.name), ], @@ -226,7 +320,8 @@ class _AddItemScreenState extends State { const SizedBox(height: 16), // Subcategory selection - if (_selectedCategory != null && _selectedCategory!.subcategories.isNotEmpty) ...[ + if (_selectedCategory != null && + _selectedCategory!.subcategories.isNotEmpty) ...[ const Text( 'Subcategoria', style: TextStyle( @@ -236,7 +331,7 @@ class _AddItemScreenState extends State { ), ), const SizedBox(height: 8), - + SizedBox( height: 60, child: ListView.builder( @@ -245,25 +340,34 @@ class _AddItemScreenState extends State { itemBuilder: (context, index) { final subcategory = _selectedCategory!.subcategories[index]; final isSelected = _selectedSubcategory == subcategory; - + return Padding( padding: const EdgeInsets.only(right: 8), child: GestureDetector( onTap: () => _onSubcategoryChanged(subcategory), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), decoration: BoxDecoration( - color: isSelected ? const Color(0xFF0066CC) : Colors.white, + color: isSelected + ? const Color(0xFF0066CC) + : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( - color: isSelected ? const Color(0xFF0066CC) : const Color(0xFFE0E0E0), + color: isSelected + ? const Color(0xFF0066CC) + : const Color(0xFFE0E0E0), ), ), child: Center( child: Text( subcategory.name, style: TextStyle( - color: isSelected ? Colors.white : const Color(0xFF333333), + color: isSelected + ? Colors.white + : const Color(0xFF333333), fontWeight: FontWeight.w500, ), ), @@ -274,7 +378,7 @@ class _AddItemScreenState extends State { }, ), ), - + const SizedBox(height: 8), if (_selectedSubcategory != null) Text( @@ -299,7 +403,7 @@ class _AddItemScreenState extends State { ), ), const SizedBox(height: 8), - + Wrap( spacing: 8, runSpacing: 8, @@ -313,19 +417,18 @@ class _AddItemScreenState extends State { checkmarkColor: const Color(0xFF0066CC), backgroundColor: Colors.white, labelStyle: TextStyle( - color: isSelected ? const Color(0xFF0066CC) : const Color(0xFF333333), + color: isSelected + ? const Color(0xFF0066CC) + : const Color(0xFF333333), ), ); }).toList(), ), - + const SizedBox(height: 8), Text( 'Tags selecionadas: ${_selectedTags.length}/10', - style: const TextStyle( - fontSize: 12, - color: Color(0xFF666666), - ), + style: const TextStyle(fontSize: 12, color: Color(0xFF666666)), ), const SizedBox(height: 32), @@ -349,7 +452,9 @@ class _AddItemScreenState extends State { width: 20, child: CircularProgressIndicator( strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), ), ) : const Text( diff --git a/lib/Screens/home_screen.dart b/lib/Screens/home_screen.dart index 5e68307..8dc6e32 100644 --- a/lib/Screens/home_screen.dart +++ b/lib/Screens/home_screen.dart @@ -1,7 +1,9 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'perfil_screen.dart'; import 'add_item_screen.dart'; +import 'item_screen.dart'; +import 'week_screen.dart'; import '../constants/item_categories.dart'; class HomeScreen extends StatefulWidget { @@ -34,7 +36,7 @@ class _HomeScreenState extends State { backgroundColor: Colors.white, type: BottomNavigationBarType.fixed, items: const [ - BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'), + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'), BottomNavigationBarItem( icon: Icon(Icons.inventory_2_outlined), label: 'Itens', @@ -62,28 +64,80 @@ class _HomeContent extends StatefulWidget { class _HomeContentState extends State<_HomeContent> { int _itemCount = 0; + List> _todayItems = []; + List> _recentItems = []; + bool _isLoading = true; + + static const _weekdayLong = [ + 'Segunda', + 'Terça', + 'Quarta', + 'Quinta', + 'Sexta', + 'Sábado', + 'Domingo', + ]; @override void initState() { super.initState(); - _loadItemCount(); + _loadData(); } - Future _loadItemCount() async { + String _dateKey(DateTime d) => + '${d.year.toString().padLeft(4, '0')}-' + '${d.month.toString().padLeft(2, '0')}-' + '${d.day.toString().padLeft(2, '0')}'; + + Future _loadData() async { + setState(() => _isLoading = true); try { final user = Supabase.instance.client.auth.currentUser; - if (user != null) { - final response = await Supabase.instance.client - .from('items') - .select('id') - .eq('user_id', user.id); + if (user == null) return; - setState(() { - _itemCount = response.length; - }); + // total item count + final all = await Supabase.instance.client + .from('items') + .select('id') + .eq('user_id', user.id); + + // recent 5 items + final recent = await Supabase.instance.client + .from('items') + .select('*, item_images(image_url)') + .eq('user_id', user.id) + .order('id', ascending: false) + .limit(5); + + // today's plan + final today = DateTime.now(); + final plan = await Supabase.instance.client + .from('plans') + .select('plan_items(items(*, item_images(image_url)))') + .eq('user_id', user.id) + .eq('data', _dateKey(today)) + .maybeSingle(); + + List> todayItems = []; + if (plan != null) { + final planItems = plan['plan_items'] as List? ?? []; + todayItems = planItems + .where((pi) => pi['items'] != null) + .map>( + (pi) => Map.from(pi['items']), + ) + .toList(); } + + setState(() { + _itemCount = all.length; + _recentItems = List>.from(recent); + _todayItems = todayItems; + _isLoading = false; + }); } catch (e) { - print('Error loading item count: $e'); + print('Error loading home: $e'); + setState(() => _isLoading = false); } } @@ -133,7 +187,7 @@ class _HomeContentState extends State<_HomeContent> { onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Notificações'), + content: Text('Notificações'), backgroundColor: Color(0xFF0066CC), ), ); @@ -144,138 +198,28 @@ class _HomeContentState extends State<_HomeContent> { ), Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Today Section - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Hoje - Sexta', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF0066CC), - ), - ), - const SizedBox(height: 8), - const Text( - '2 itens planejados', - style: TextStyle( - fontSize: 14, - color: Color(0xFF666666), - ), - ), - const SizedBox(height: 16), - // Placeholder for planned items - Container( - height: 80, - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(8), - ), - child: const Center( - child: Text( - 'Itens planejados aparecerão aqui', - style: TextStyle(color: Color(0xFF999999)), - ), - ), - ), - ], - ), - ), - - const SizedBox(height: 24), - - // AI Recommendations Button - Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFF0066CC), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Recomendações IA', - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - const Text( - 'Descubra o que levar', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), - ), - const SizedBox(height: 12), - Row( - children: [ - const Icon( - Icons.auto_awesome, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Container( - height: 4, - decoration: BoxDecoration( - color: Colors.white30, - borderRadius: BorderRadius.circular(2), - ), - ), - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 24), - - // Recent Items Section - const Text( - 'Itens Recentes', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 16), - - // Placeholder for recent items - Container( - height: 100, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE0E0E0)), - ), - child: const Center( - child: Text( - 'Itens recentes aparecerão aqui', - style: TextStyle(color: Color(0xFF999999)), + child: RefreshIndicator( + onRefresh: _loadData, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTodaySection(), + const SizedBox(height: 24), + const Text( + 'Itens Recentes', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), ), ), - ), - ], + const SizedBox(height: 12), + _buildRecentItems(), + ], + ), ), ), ), @@ -288,9 +232,11 @@ class _HomeContentState extends State<_HomeContent> { right: 20, child: FloatingActionButton( onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const AddItemScreen()), - ); + Navigator.of(context) + .push( + MaterialPageRoute(builder: (_) => const AddItemScreen()), + ) + .then((_) => _loadData()); }, backgroundColor: const Color(0xFF0066CC), child: const Icon(Icons.add, color: Colors.white), @@ -300,232 +246,231 @@ class _HomeContentState extends State<_HomeContent> { ), ); } + + Widget _buildTodaySection() { + final today = DateTime.now(); + final dayName = _weekdayLong[today.weekday - 1]; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Hoje - $dayName', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF0066CC), + ), + ), + Text( + '${today.day}/${today.month}', + style: const TextStyle(fontSize: 14, color: Color(0xFF666666)), + ), + ], + ), + const SizedBox(height: 6), + Text( + '${_todayItems.length} ${_todayItems.length == 1 ? "item planejado" : "itens planejados"}', + style: const TextStyle(fontSize: 14, color: Color(0xFF666666)), + ), + const SizedBox(height: 12), + if (_isLoading) + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center(child: CircularProgressIndicator()), + ) + else if (_todayItems.isEmpty) + Container( + padding: const EdgeInsets.symmetric(vertical: 20), + alignment: Alignment.center, + child: const Text( + 'Nada planejado para hoje', + style: TextStyle(color: Color(0xFF999999)), + ), + ) + else + SizedBox( + height: 90, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _todayItems.length, + itemBuilder: (_, i) => _buildTodayChip(_todayItems[i]), + ), + ), + ], + ), + ); + } + + Widget _buildTodayChip(Map item) { + final images = item['item_images'] as List?; + final imageUrl = (images != null && images.isNotEmpty) + ? images.first['image_url'] + : null; + final category = ITEM_CATEGORIES.firstWhere( + (c) => c.id == item['categoria'], + orElse: () => ITEM_CATEGORIES.last, + ); + return Container( + width: 80, + margin: const EdgeInsets.only(right: 10), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: imageUrl != null + ? Image.network( + imageUrl, + width: 60, + height: 60, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _placeholder(category.icon), + ) + : _placeholder(category.icon), + ), + const SizedBox(height: 4), + Text( + item['nome'] ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 11, color: Color(0xFF333333)), + ), + ], + ), + ); + } + + Widget _placeholder(String icon) { + return Container( + width: 60, + height: 60, + color: const Color(0xFF0066CC).withOpacity(0.1), + alignment: Alignment.center, + child: Text(icon, style: const TextStyle(fontSize: 26)), + ); + } + + Widget _buildRecentItems() { + if (_isLoading) { + return const SizedBox( + height: 100, + child: Center(child: CircularProgressIndicator()), + ); + } + if (_recentItems.isEmpty) { + return Container( + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Text( + 'Sem itens ainda', + style: TextStyle(color: Color(0xFF999999)), + ), + ), + ); + } + return SizedBox( + height: 130, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _recentItems.length, + itemBuilder: (_, i) { + final item = _recentItems[i]; + final images = item['item_images'] as List?; + final imageUrl = (images != null && images.isNotEmpty) + ? images.first['image_url'] + : null; + final category = ITEM_CATEGORIES.firstWhere( + (c) => c.id == item['categoria'], + orElse: () => ITEM_CATEGORIES.last, + ); + return Container( + width: 110, + margin: const EdgeInsets.only(right: 10), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: imageUrl != null + ? Image.network( + imageUrl, + width: double.infinity, + height: 70, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + _placeholder(category.icon), + ) + : Container( + width: double.infinity, + height: 70, + color: const Color(0xFF0066CC).withOpacity(0.1), + alignment: Alignment.center, + child: Text( + category.icon, + style: const TextStyle(fontSize: 30), + ), + ), + ), + const SizedBox(height: 6), + Text( + item['nome'] ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + Text( + category.name, + style: const TextStyle( + fontSize: 10, + color: Color(0xFF666666), + ), + ), + ], + ), + ); + }, + ), + ); + } } -class _ItemsScreen extends StatefulWidget { +class _ItemsScreen extends StatelessWidget { const _ItemsScreen(); @override - State<_ItemsScreen> createState() => _ItemsScreenState(); -} - -class _ItemsScreenState extends State<_ItemsScreen> { - List> _items = []; - bool _isLoading = true; - - @override - void initState() { - super.initState(); - _loadItems(); - } - - Future _loadItems() async { - try { - final user = Supabase.instance.client.auth.currentUser; - if (user != null) { - final response = await Supabase.instance.client - .from('items') - .select() - .eq('user_id', user.id) - .order('created_at', ascending: false); - - setState(() { - _items = List>.from(response); - _isLoading = false; - }); - } - } catch (e) { - print('Error loading items: $e'); - setState(() => _isLoading = false); - } - } - - String _getCategoryName(String categoryId) { - final category = ITEM_CATEGORIES.firstWhere( - (cat) => cat.id == categoryId, - orElse: () => ITEM_CATEGORIES.last, - ); - return category.name; - } - - String _getCategoryIcon(String categoryId) { - final category = ITEM_CATEGORIES.firstWhere( - (cat) => cat.id == categoryId, - orElse: () => ITEM_CATEGORIES.last, - ); - return category.icon; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFFFE5CC), - appBar: AppBar( - backgroundColor: const Color(0xFF0066CC), - elevation: 0, - title: const Text( - 'Meus Itens', - style: TextStyle(color: Colors.white, fontSize: 20), - ), - centerTitle: true, - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : _items.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.inventory_2_outlined, - size: 64, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - const Text( - 'Nenhum item ainda', - style: TextStyle(fontSize: 18, color: Color(0xFF666666)), - ), - const SizedBox(height: 8), - const Text( - 'Toque no + para adicionar', - style: TextStyle(fontSize: 14, color: Color(0xFF999999)), - ), - ], - ), - ) - : ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: _items.length, - itemBuilder: (context, index) { - final item = _items[index]; - final categoryName = _getCategoryName(item['category_id']); - final categoryIcon = _getCategoryIcon(item['category_id']); - final tags = List.from(item['context_tags'] ?? []); - - return Card( - margin: const EdgeInsets.only(bottom: 12), - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: const Color(0xFF0066CC).withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Center( - child: Text( - categoryIcon, - style: const TextStyle(fontSize: 24), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item['name'] ?? 'Sem nome', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 4), - Text( - categoryName, - style: const TextStyle( - fontSize: 14, - color: Color(0xFF666666), - ), - ), - ], - ), - ), - ], - ), - if (item['description'] != null && - item['description'].toString().isNotEmpty) ...[ - const SizedBox(height: 12), - Text( - item['description'], - style: const TextStyle( - fontSize: 14, - color: Color(0xFF666666), - ), - ), - ], - if (tags.isNotEmpty) ...[ - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: tags.map((tag) { - final contextTag = CONTEXT_TAGS.firstWhere( - (t) => t.id == tag, - orElse: () => CONTEXT_TAGS.first, - ); - return Chip( - label: Text(contextTag.name), - backgroundColor: const Color( - 0xFF0066CC, - ).withOpacity(0.1), - labelStyle: const TextStyle( - color: Color(0xFF0066CC), - fontSize: 12, - ), - padding: EdgeInsets.zero, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ); - }).toList(), - ), - ], - ], - ), - ), - ); - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => const AddItemScreen())) - .then((_) => _loadItems()); - }, - backgroundColor: const Color(0xFF0066CC), - child: const Icon(Icons.add, color: Colors.white), - ), - ); - } + Widget build(BuildContext context) => const ItemScreen(); } class _WeekScreen extends StatelessWidget { const _WeekScreen(); @override - Widget build(BuildContext context) { - return const Center(child: Text('Semana')); - } + Widget build(BuildContext context) => const WeekScreen(); } class _ProfileScreen extends StatelessWidget { const _ProfileScreen(); @override - Widget build(BuildContext context) { - return const PerfilScreen(); - } + Widget build(BuildContext context) => const PerfilScreen(); } diff --git a/lib/Screens/item_screen.dart b/lib/Screens/item_screen.dart new file mode 100644 index 0000000..b4b3da3 --- /dev/null +++ b/lib/Screens/item_screen.dart @@ -0,0 +1,722 @@ +import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../constants/item_categories.dart'; +import 'add_item_screen.dart'; + +class ItemScreen extends StatefulWidget { + const ItemScreen({super.key}); + + @override + State createState() => _ItemScreenState(); +} + +class _ItemScreenState extends State { + List> _items = []; + bool _isLoading = true; + String _searchQuery = ''; + String? _selectedCategoryFilter; + + List> get _filteredItems { + return _items.where((item) { + final name = (item['nome'] ?? '').toString().toLowerCase(); + final tags = List.from( + item['tags'] ?? [], + ).join(' ').toLowerCase(); + final matchesSearch = + _searchQuery.isEmpty || + name.contains(_searchQuery.toLowerCase()) || + tags.contains(_searchQuery.toLowerCase()); + final matchesCategory = + _selectedCategoryFilter == null || + item['categoria'] == _selectedCategoryFilter; + return matchesSearch && matchesCategory; + }).toList(); + } + + @override + void initState() { + super.initState(); + _loadItems(); + } + + Future _loadItems() async { + setState(() => _isLoading = true); + try { + final user = Supabase.instance.client.auth.currentUser; + if (user == null) return; + + final response = await Supabase.instance.client + .from('items') + .select('*, item_images(image_url)') + .eq('user_id', user.id) + .order('id', ascending: false); + + setState(() { + _items = List>.from(response); + _isLoading = false; + }); + } catch (e) { + print('Error loading items: $e'); + setState(() => _isLoading = false); + } + } + + String? _imageUrl(Map item) { + final images = item['item_images'] as List?; + if (images != null && images.isNotEmpty) { + return images.first['image_url'] as String?; + } + return null; + } + + String _categoryName(String? id) { + if (id == null) return 'Outros'; + return ITEM_CATEGORIES + .firstWhere((c) => c.id == id, orElse: () => ITEM_CATEGORIES.last) + .name; + } + + String _categoryIcon(String? id) { + if (id == null) return '📦'; + return ITEM_CATEGORIES + .firstWhere((c) => c.id == id, orElse: () => ITEM_CATEGORIES.last) + .icon; + } + + Future _deleteItem(Map item) async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Apagar item'), + content: Text('Tem certeza que deseja apagar "${item['nome']}"?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Cancelar'), + ), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text('Apagar'), + ), + ], + ), + ); + if (confirmed != true) return; + + try { + await Supabase.instance.client + .from('item_images') + .delete() + .eq('item_id', item['id']); + await Supabase.instance.client + .from('items') + .delete() + .eq('id', item['id']); + _loadItems(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Item apagado'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erro: $e'), backgroundColor: Colors.red), + ); + } + } + } + + void _viewItem(Map item) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ItemDetailScreen(item: item, imageUrl: _imageUrl(item)), + ), + ).then((_) => _loadItems()); + } + + void _editItem(Map item) { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => EditItemScreen(item: item)), + ).then((_) => _loadItems()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFFE5CC), + appBar: AppBar( + backgroundColor: const Color(0xFF0066CC), + elevation: 0, + title: const Text( + 'Meus Itens', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + centerTitle: true, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + _buildSearchAndFilters(), + Expanded( + child: _items.isEmpty + ? _buildEmpty() + : _filteredItems.isEmpty + ? const Center( + child: Text( + 'Nenhum item encontrado', + style: TextStyle(color: Color(0xFF666666)), + ), + ) + : RefreshIndicator( + onRefresh: _loadItems, + child: ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _filteredItems.length, + itemBuilder: (context, i) => + _buildItemCard(_filteredItems[i]), + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + backgroundColor: const Color(0xFF0066CC), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const AddItemScreen()), + ).then((_) => _loadItems()); + }, + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } + + Widget _buildSearchAndFilters() { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + color: const Color(0xFFFFE5CC), + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + child: TextField( + onChanged: (v) => setState(() => _searchQuery = v), + decoration: const InputDecoration( + prefixIcon: Icon(Icons.search, color: Color(0xFF666666)), + hintText: 'Pesquisar por nome ou tag...', + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 14, + ), + ), + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 38, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + _categoryChip(null, 'Todos', '🗂'), + ...ITEM_CATEGORIES.map( + (c) => _categoryChip(c.id, c.name, c.icon), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _categoryChip(String? id, String name, String icon) { + final selected = _selectedCategoryFilter == id; + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () => setState(() => _selectedCategoryFilter = id), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: selected ? const Color(0xFF0066CC) : Colors.white, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: selected + ? const Color(0xFF0066CC) + : const Color(0xFFE0E0E0), + ), + ), + child: Row( + children: [ + Text(icon, style: const TextStyle(fontSize: 14)), + const SizedBox(width: 6), + Text( + name, + style: TextStyle( + color: selected ? Colors.white : const Color(0xFF333333), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildEmpty() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + const Text( + 'Nenhum item ainda', + style: TextStyle(fontSize: 18, color: Color(0xFF666666)), + ), + const SizedBox(height: 8), + const Text( + 'Toque no + para adicionar', + style: TextStyle(fontSize: 14, color: Color(0xFF999999)), + ), + ], + ), + ); + } + + Widget _buildItemCard(Map item) { + final categoryName = _categoryName(item['categoria']); + final categoryIcon = _categoryIcon(item['categoria']); + final tags = List.from(item['tags'] ?? []); + final imageUrl = _imageUrl(item); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _viewItem(item), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: imageUrl != null + ? Image.network( + imageUrl, + width: 72, + height: 72, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + _iconPlaceholder(categoryIcon), + ) + : _iconPlaceholder(categoryIcon), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['nome'] ?? 'Sem nome', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + const SizedBox(height: 2), + Text( + categoryName, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF666666), + ), + ), + if (tags.isNotEmpty) ...[ + const SizedBox(height: 6), + Wrap( + spacing: 4, + runSpacing: 4, + children: tags.take(3).map((tag) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: const Color(0xFF0066CC).withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 10, + color: Color(0xFF0066CC), + ), + ), + ); + }).toList(), + ), + ], + ], + ), + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert, color: Color(0xFF666666)), + onSelected: (value) { + if (value == 'view') _viewItem(item); + if (value == 'edit') _editItem(item); + if (value == 'delete') _deleteItem(item); + }, + itemBuilder: (_) => [ + const PopupMenuItem( + value: 'view', + child: Row( + children: [ + Icon(Icons.visibility_outlined, size: 18), + SizedBox(width: 8), + Text('Ver'), + ], + ), + ), + const PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Icon(Icons.edit_outlined, size: 18), + SizedBox(width: 8), + Text('Editar'), + ], + ), + ), + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete_outline, size: 18, color: Colors.red), + SizedBox(width: 8), + Text('Apagar', style: TextStyle(color: Colors.red)), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _iconPlaceholder(String icon) { + return Container( + width: 72, + height: 72, + color: const Color(0xFF0066CC).withOpacity(0.1), + alignment: Alignment.center, + child: Text(icon, style: const TextStyle(fontSize: 32)), + ); + } +} + +// ============================================= +// Detalhe do item +// ============================================= +class ItemDetailScreen extends StatelessWidget { + final Map item; + final String? imageUrl; + + const ItemDetailScreen({super.key, required this.item, this.imageUrl}); + + @override + Widget build(BuildContext context) { + final categoryId = item['categoria'] as String?; + final categoryName = categoryId == null + ? 'Outros' + : ITEM_CATEGORIES + .firstWhere( + (c) => c.id == categoryId, + orElse: () => ITEM_CATEGORIES.last, + ) + .name; + final tags = List.from(item['tags'] ?? []); + + return Scaffold( + backgroundColor: const Color(0xFFFFE5CC), + appBar: AppBar( + backgroundColor: const Color(0xFF0066CC), + title: Text( + item['nome'] ?? 'Item', + style: const TextStyle(color: Colors.white), + ), + iconTheme: const IconThemeData(color: Colors.white), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (imageUrl != null) + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.network( + imageUrl!, + width: double.infinity, + height: 250, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + const SizedBox(height: 20), + _label('Nome'), + _value(item['nome'] ?? ''), + const SizedBox(height: 16), + _label('Categoria'), + _value(categoryName), + if (tags.isNotEmpty) ...[ + const SizedBox(height: 16), + _label('Tags'), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: tags + .map( + (t) => Chip( + label: Text(t), + backgroundColor: const Color( + 0xFF0066CC, + ).withOpacity(0.1), + labelStyle: const TextStyle(color: Color(0xFF0066CC)), + ), + ) + .toList(), + ), + ], + ], + ), + ), + ); + } + + Widget _label(String t) => Text( + t, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF666666), + ), + ); + + Widget _value(String t) => Container( + width: double.infinity, + margin: const EdgeInsets.only(top: 6), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Text(t, style: const TextStyle(fontSize: 16)), + ); +} + +// ============================================= +// Editar item +// ============================================= +class EditItemScreen extends StatefulWidget { + final Map item; + const EditItemScreen({super.key, required this.item}); + + @override + State createState() => _EditItemScreenState(); +} + +class _EditItemScreenState extends State { + late TextEditingController _nameController; + ItemCategory? _selectedCategory; + final Set _selectedTags = {}; + bool _isLoading = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.item['nome'] ?? ''); + final catId = widget.item['categoria'] as String?; + if (catId != null) { + _selectedCategory = ITEM_CATEGORIES.firstWhere( + (c) => c.id == catId, + orElse: () => ITEM_CATEGORIES.last, + ); + } + final tags = List.from(widget.item['tags'] ?? []); + _selectedTags.addAll(tags); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _save() async { + if (_nameController.text.trim().isEmpty) { + _snack('Nome não pode ser vazio', Colors.red); + return; + } + setState(() => _isLoading = true); + try { + await Supabase.instance.client + .from('items') + .update({ + 'nome': _nameController.text.trim(), + 'categoria': _selectedCategory?.id, + 'tags': _selectedTags.toList(), + }) + .eq('id', widget.item['id']); + _snack('Item atualizado!', Colors.green); + if (mounted) Navigator.pop(context); + } catch (e) { + _snack('Erro: $e', Colors.red); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _snack(String msg, Color color) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(msg), backgroundColor: color)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFFFE5CC), + appBar: AppBar( + backgroundColor: const Color(0xFF0066CC), + title: const Text('Editar Item', style: TextStyle(color: Colors.white)), + iconTheme: const IconThemeData(color: Colors.white), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: TextField( + controller: _nameController, + decoration: const InputDecoration( + labelText: 'Nome do item', + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Categoria', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedCategory, + isExpanded: true, + items: ITEM_CATEGORIES + .map( + (c) => DropdownMenuItem( + value: c, + child: Text('${c.icon} ${c.name}'), + ), + ) + .toList(), + onChanged: (v) => setState(() => _selectedCategory = v), + ), + ), + ), + const SizedBox(height: 16), + const Text( + 'Tags', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: CONTEXT_TAGS.map((tag) { + final selected = _selectedTags.contains(tag.id); + return FilterChip( + label: Text(tag.name), + selected: selected, + onSelected: (_) { + setState(() { + if (selected) { + _selectedTags.remove(tag.id); + } else { + _selectedTags.add(tag.id); + } + }); + }, + selectedColor: const Color(0xFF0066CC).withOpacity(0.2), + checkmarkColor: const Color(0xFF0066CC), + backgroundColor: Colors.white, + ); + }).toList(), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: _isLoading ? null : _save, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0066CC), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: _isLoading + ? const CircularProgressIndicator(color: Colors.white) + : const Text( + 'Guardar alterações', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/Screens/week_screen.dart b/lib/Screens/week_screen.dart new file mode 100644 index 0000000..09b2845 --- /dev/null +++ b/lib/Screens/week_screen.dart @@ -0,0 +1,544 @@ +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 createState() => _WeekScreenState(); +} + +class _WeekScreenState extends State { + DateTime _selectedDay = DateTime.now(); + List> _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 _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 _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>( + (pi) => Map.from(pi['items']), + ) + .toList(); + + setState(() { + _dayItems = items; + _isLoading = false; + }); + } catch (e) { + print('Error loading day items: $e'); + setState(() => _isLoading = false); + } + } + + Future _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>.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>( + 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 _removeItem(Map 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 item) { + final images = item['item_images'] as List?; + final imageUrl = + (images != null && images.isNotEmpty) ? images.first['image_url'] : null; + final category = ITEM_CATEGORIES.firstWhere( + (c) => c.id == item['categoria'], + orElse: () => ITEM_CATEGORIES.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: (_, __, ___) => _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).withOpacity(0.1), + alignment: Alignment.center, + child: Text(icon, style: const TextStyle(fontSize: 26)), + ); + } +} + +// ============================================= +// Bottom sheet para escolher itens +// ============================================= +class _ItemPickerSheet extends StatefulWidget { + final List> items; + const _ItemPickerSheet({required this.items}); + + @override + State<_ItemPickerSheet> createState() => _ItemPickerSheetState(); +} + +class _ItemPickerSheetState extends State<_ItemPickerSheet> { + final Set _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 = ITEM_CATEGORIES.firstWhere( + (c) => c.id == item['categoria'], + orElse: () => ITEM_CATEGORIES.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: (_, __, ___) => Container( + width: 48, + height: 48, + color: const Color(0xFF0066CC) + .withOpacity(0.1), + alignment: Alignment.center, + child: Text(category.icon), + ), + ) + : Container( + width: 48, + height: 48, + color: const Color(0xFF0066CC) + .withOpacity(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, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/constants/item_categories.dart b/lib/constants/item_categories.dart index 29c6182..4444985 100644 --- a/lib/constants/item_categories.dart +++ b/lib/constants/item_categories.dart @@ -41,7 +41,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'clothing', name: 'Roupa', - icon: '👕', + icon: '', description: 'Peças de vestuário', subcategories: [ Subcategory( @@ -79,7 +79,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'electronics', name: 'Eletrónica', - icon: '💻', + icon: '', description: 'Dispositivos e acessórios tecnológicos', subcategories: [ Subcategory( @@ -117,7 +117,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'footwear', name: 'Calçado', - icon: '👟', + icon: '', description: 'Sapatos, botas, sandálias', subcategories: [ Subcategory( @@ -145,7 +145,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'accessories', name: 'Acessórios', - icon: '🎒', + icon: '', description: 'Bolsas, relógios, óculos, bijuteria', subcategories: [ Subcategory( @@ -175,7 +175,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'documents', name: 'Documentos', - icon: '📄', + icon: '', description: 'Passaporte, cartões, papéis importantes', subcategories: [ Subcategory( @@ -203,7 +203,7 @@ final List ITEM_CATEGORIES = [ ItemCategory( id: 'other', name: 'Outros', - icon: '📦', + icon: '', description: 'Tudo o resto', subcategories: [], ), diff --git a/lib/login/login_screen.dart b/lib/login/login_screen.dart index d2395d8..9128a69 100644 --- a/lib/login/login_screen.dart +++ b/lib/login/login_screen.dart @@ -33,24 +33,12 @@ class _LoginScreenState extends State { children: [ const SizedBox(height: 40), - // Blue icon with white box outline - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: const Color(0xFF0066CC), - borderRadius: BorderRadius.circular(16), - ), - child: Center( - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 3), - borderRadius: BorderRadius.circular(8), - ), - ), - ), + // Logo + Image.asset( + 'assets/logoDayMaker.png', + width: 220, + height: 220, + fit: BoxFit.contain, ), const SizedBox(height: 24), @@ -88,8 +76,8 @@ class _LoginScreenState extends State { ? const Color(0xFF0066CC) : Colors.transparent, borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), + topLeft: Radius.circular(20), + bottomLeft: Radius.circular(20), ), border: Border.all( color: const Color(0xFF0066CC), @@ -120,8 +108,8 @@ class _LoginScreenState extends State { ? const Color(0xFF0066CC) : Colors.transparent, borderRadius: const BorderRadius.only( - topRight: Radius.circular(8), - bottomRight: Radius.circular(8), + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), ), border: Border.all( color: const Color(0xFF0066CC), @@ -151,7 +139,7 @@ class _LoginScreenState extends State { Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xFFE0E0E0)), ), child: TextField( @@ -177,7 +165,7 @@ class _LoginScreenState extends State { Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xFFE0E0E0)), ), child: TextField( @@ -201,7 +189,7 @@ class _LoginScreenState extends State { Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xFFE0E0E0)), ), child: TextField( @@ -239,7 +227,7 @@ class _LoginScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0066CC), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(42), ), elevation: 0, ), diff --git a/lib/main.dart b/lib/main.dart index e5c2031..fb9bccf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; import 'login/login_screen.dart'; +import 'Screens/home_screen.dart'; import 'supabase_config.dart'; void main() async { @@ -15,13 +17,14 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final session = Supabase.instance.client.auth.currentSession; return MaterialApp( title: 'DayMaker', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0066CC)), useMaterial3: true, ), - home: const LoginScreen(), + home: session != null ? const HomeScreen() : const LoginScreen(), debugShowCheckedModeBanner: false, ); } diff --git a/logoDayMaker.png b/logoDayMaker.png deleted file mode 100644 index 985d03c..0000000 Binary files a/logoDayMaker.png and /dev/null differ diff --git a/pubspec.yaml b/pubspec.yaml index 61ae117..eef423c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,9 +60,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/logoDayMaker.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images