Atualização tela semanal |Adicoinar item com imagem

This commit is contained in:
Carlos Correia
2026-05-15 12:39:29 +01:00
parent bb0c4fc2a5
commit e8fa39c594
10 changed files with 1731 additions and 425 deletions

BIN
assets/logoDayMaker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

@@ -1,4 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../constants/item_categories.dart'; import '../constants/item_categories.dart';
@@ -12,13 +14,26 @@ class AddItemScreen extends StatefulWidget {
class _AddItemScreenState extends State<AddItemScreen> { class _AddItemScreenState extends State<AddItemScreen> {
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController();
ItemCategory? _selectedCategory; ItemCategory? _selectedCategory;
Subcategory? _selectedSubcategory; Subcategory? _selectedSubcategory;
final Set<String> _selectedTags = {}; final Set<String> _selectedTags = {};
XFile? _selectedImage;
bool _isLoading = false; bool _isLoading = false;
Future<void> _pickImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1024,
imageQuality: 80,
);
if (image != null) {
setState(() => _selectedImage = image);
}
}
@override @override
void dispose() { void dispose() {
_nameController.dispose(); _nameController.dispose();
@@ -31,7 +46,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
_selectedCategory = category; _selectedCategory = category;
_selectedSubcategory = null; _selectedSubcategory = null;
_selectedTags.clear(); _selectedTags.clear();
if (category != null && category.subcategories.isNotEmpty) { if (category != null && category.subcategories.isNotEmpty) {
_selectedSubcategory = category.subcategories.first; _selectedSubcategory = category.subcategories.first;
_autoAssignTags(category.id, category.subcategories.first.id); _autoAssignTags(category.id, category.subcategories.first.id);
@@ -43,7 +58,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
setState(() { setState(() {
_selectedSubcategory = subcategory; _selectedSubcategory = subcategory;
_selectedTags.clear(); _selectedTags.clear();
if (_selectedCategory != null && subcategory != null) { if (_selectedCategory != null && subcategory != null) {
_autoAssignTags(_selectedCategory!.id, subcategory.id); _autoAssignTags(_selectedCategory!.id, subcategory.id);
} }
@@ -87,17 +102,49 @@ class _AddItemScreenState extends State<AddItemScreen> {
return; return;
} }
await Supabase.instance.client.from('items').insert({ // Ensure user profile row exists in public.users (FK requirement)
'user_id': user.id, await Supabase.instance.client.from('users').upsert({
'name': _nameController.text.trim(), 'id': user.id,
'description': _descriptionController.text.trim(), 'nome':
'category_id': _selectedCategory!.id, user.userMetadata?['username'] ??
'subcategory_id': _selectedSubcategory?.id, user.email?.split('@').first ??
'context_tags': _selectedTags.toList(), '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!'); _showSuccessSnackBar('Item adicionado com sucesso!');
Navigator.pop(context); if (mounted) Navigator.pop(context);
} catch (e) { } catch (e) {
_showErrorSnackBar('Erro ao salvar item: $e'); _showErrorSnackBar('Erro ao salvar item: $e');
} finally { } finally {
@@ -135,6 +182,50 @@ class _AddItemScreenState extends State<AddItemScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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 // Name field
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -192,7 +283,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
@@ -211,7 +302,10 @@ class _AddItemScreenState extends State<AddItemScreen> {
value: category, value: category,
child: Row( child: Row(
children: [ children: [
Text(category.icon, style: const TextStyle(fontSize: 24)), Text(
category.icon,
style: const TextStyle(fontSize: 24),
),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(category.name), Text(category.name),
], ],
@@ -226,7 +320,8 @@ class _AddItemScreenState extends State<AddItemScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Subcategory selection // Subcategory selection
if (_selectedCategory != null && _selectedCategory!.subcategories.isNotEmpty) ...[ if (_selectedCategory != null &&
_selectedCategory!.subcategories.isNotEmpty) ...[
const Text( const Text(
'Subcategoria', 'Subcategoria',
style: TextStyle( style: TextStyle(
@@ -236,7 +331,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( SizedBox(
height: 60, height: 60,
child: ListView.builder( child: ListView.builder(
@@ -245,25 +340,34 @@ class _AddItemScreenState extends State<AddItemScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final subcategory = _selectedCategory!.subcategories[index]; final subcategory = _selectedCategory!.subcategories[index];
final isSelected = _selectedSubcategory == subcategory; final isSelected = _selectedSubcategory == subcategory;
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: GestureDetector( child: GestureDetector(
onTap: () => _onSubcategoryChanged(subcategory), onTap: () => _onSubcategoryChanged(subcategory),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? const Color(0xFF0066CC) : Colors.white, color: isSelected
? const Color(0xFF0066CC)
: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
border: Border.all( border: Border.all(
color: isSelected ? const Color(0xFF0066CC) : const Color(0xFFE0E0E0), color: isSelected
? const Color(0xFF0066CC)
: const Color(0xFFE0E0E0),
), ),
), ),
child: Center( child: Center(
child: Text( child: Text(
subcategory.name, subcategory.name,
style: TextStyle( style: TextStyle(
color: isSelected ? Colors.white : const Color(0xFF333333), color: isSelected
? Colors.white
: const Color(0xFF333333),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -274,7 +378,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
}, },
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (_selectedSubcategory != null) if (_selectedSubcategory != null)
Text( Text(
@@ -299,7 +403,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
@@ -313,19 +417,18 @@ class _AddItemScreenState extends State<AddItemScreen> {
checkmarkColor: const Color(0xFF0066CC), checkmarkColor: const Color(0xFF0066CC),
backgroundColor: Colors.white, backgroundColor: Colors.white,
labelStyle: TextStyle( labelStyle: TextStyle(
color: isSelected ? const Color(0xFF0066CC) : const Color(0xFF333333), color: isSelected
? const Color(0xFF0066CC)
: const Color(0xFF333333),
), ),
); );
}).toList(), }).toList(),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Tags selecionadas: ${_selectedTags.length}/10', 'Tags selecionadas: ${_selectedTags.length}/10',
style: const TextStyle( style: const TextStyle(fontSize: 12, color: Color(0xFF666666)),
fontSize: 12,
color: Color(0xFF666666),
),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
@@ -349,7 +452,9 @@ class _AddItemScreenState extends State<AddItemScreen> {
width: 20, width: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
), ),
) )
: const Text( : const Text(

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'perfil_screen.dart'; import 'perfil_screen.dart';
import 'add_item_screen.dart'; import 'add_item_screen.dart';
import 'item_screen.dart';
import 'week_screen.dart';
import '../constants/item_categories.dart'; import '../constants/item_categories.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -34,7 +36,7 @@ class _HomeScreenState extends State<HomeScreen> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
items: const [ items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'), BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.inventory_2_outlined), icon: Icon(Icons.inventory_2_outlined),
label: 'Itens', label: 'Itens',
@@ -62,28 +64,80 @@ class _HomeContent extends StatefulWidget {
class _HomeContentState extends State<_HomeContent> { class _HomeContentState extends State<_HomeContent> {
int _itemCount = 0; int _itemCount = 0;
List<Map<String, dynamic>> _todayItems = [];
List<Map<String, dynamic>> _recentItems = [];
bool _isLoading = true;
static const _weekdayLong = [
'Segunda',
'Terça',
'Quarta',
'Quinta',
'Sexta',
'Sábado',
'Domingo',
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadItemCount(); _loadData();
} }
Future<void> _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<void> _loadData() async {
setState(() => _isLoading = true);
try { try {
final user = Supabase.instance.client.auth.currentUser; final user = Supabase.instance.client.auth.currentUser;
if (user != null) { if (user == null) return;
final response = await Supabase.instance.client
.from('items')
.select('id')
.eq('user_id', user.id);
setState(() { // total item count
_itemCount = response.length; 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<Map<String, dynamic>> todayItems = [];
if (plan != null) {
final planItems = plan['plan_items'] as List? ?? [];
todayItems = planItems
.where((pi) => pi['items'] != null)
.map<Map<String, dynamic>>(
(pi) => Map<String, dynamic>.from(pi['items']),
)
.toList();
} }
setState(() {
_itemCount = all.length;
_recentItems = List<Map<String, dynamic>>.from(recent);
_todayItems = todayItems;
_isLoading = false;
});
} catch (e) { } 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: () { onPressed: () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Notificações'), content: Text('Notificações'),
backgroundColor: Color(0xFF0066CC), backgroundColor: Color(0xFF0066CC),
), ),
); );
@@ -144,138 +198,28 @@ class _HomeContentState extends State<_HomeContent> {
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: RefreshIndicator(
padding: const EdgeInsets.all(20), onRefresh: _loadData,
child: Column( child: SingleChildScrollView(
crossAxisAlignment: CrossAxisAlignment.start, physics: const AlwaysScrollableScrollPhysics(),
children: [ padding: const EdgeInsets.all(20),
// Today Section child: Column(
Container( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.all(16), children: [
decoration: BoxDecoration( _buildTodaySection(),
color: Colors.white, const SizedBox(height: 24),
borderRadius: BorderRadius.circular(12), const Text(
), 'Itens Recentes',
child: Column( style: TextStyle(
crossAxisAlignment: CrossAxisAlignment.start, fontSize: 18,
children: [ fontWeight: FontWeight.bold,
const Text( color: Color(0xFF333333),
'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)),
), ),
), ),
), const SizedBox(height: 12),
], _buildRecentItems(),
],
),
), ),
), ),
), ),
@@ -288,9 +232,11 @@ class _HomeContentState extends State<_HomeContent> {
right: 20, right: 20,
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (_) => const AddItemScreen()), .push(
); MaterialPageRoute(builder: (_) => const AddItemScreen()),
)
.then((_) => _loadData());
}, },
backgroundColor: const Color(0xFF0066CC), backgroundColor: const Color(0xFF0066CC),
child: const Icon(Icons.add, color: Colors.white), 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<String, dynamic> 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(); const _ItemsScreen();
@override @override
State<_ItemsScreen> createState() => _ItemsScreenState(); Widget build(BuildContext context) => const ItemScreen();
}
class _ItemsScreenState extends State<_ItemsScreen> {
List<Map<String, dynamic>> _items = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadItems();
}
Future<void> _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<Map<String, dynamic>>.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<String>.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),
),
);
}
} }
class _WeekScreen extends StatelessWidget { class _WeekScreen extends StatelessWidget {
const _WeekScreen(); const _WeekScreen();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => const WeekScreen();
return const Center(child: Text('Semana'));
}
} }
class _ProfileScreen extends StatelessWidget { class _ProfileScreen extends StatelessWidget {
const _ProfileScreen(); const _ProfileScreen();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => const PerfilScreen();
return const PerfilScreen();
}
} }

View File

@@ -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<ItemScreen> createState() => _ItemScreenState();
}
class _ItemScreenState extends State<ItemScreen> {
List<Map<String, dynamic>> _items = [];
bool _isLoading = true;
String _searchQuery = '';
String? _selectedCategoryFilter;
List<Map<String, dynamic>> get _filteredItems {
return _items.where((item) {
final name = (item['nome'] ?? '').toString().toLowerCase();
final tags = List<String>.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<void> _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<Map<String, dynamic>>.from(response);
_isLoading = false;
});
} catch (e) {
print('Error loading items: $e');
setState(() => _isLoading = false);
}
}
String? _imageUrl(Map<String, dynamic> 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<void> _deleteItem(Map<String, dynamic> item) async {
final confirmed = await showDialog<bool>(
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<String, dynamic> item) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ItemDetailScreen(item: item, imageUrl: _imageUrl(item)),
),
).then((_) => _loadItems());
}
void _editItem(Map<String, dynamic> 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<String, dynamic> item) {
final categoryName = _categoryName(item['categoria']);
final categoryIcon = _categoryIcon(item['categoria']);
final tags = List<String>.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<String>(
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<String, dynamic> 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<String>.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<String, dynamic> item;
const EditItemScreen({super.key, required this.item});
@override
State<EditItemScreen> createState() => _EditItemScreenState();
}
class _EditItemScreenState extends State<EditItemScreen> {
late TextEditingController _nameController;
ItemCategory? _selectedCategory;
final Set<String> _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<String>.from(widget.item['tags'] ?? []);
_selectedTags.addAll(tags);
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _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<ItemCategory>(
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),
),
),
),
],
),
),
);
}
}

View File

@@ -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<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) {
print('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 = 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<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 = 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,
),
),
),
),
),
),
],
),
),
);
}
}

View File

@@ -41,7 +41,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
ItemCategory( ItemCategory(
id: 'clothing', id: 'clothing',
name: 'Roupa', name: 'Roupa',
icon: '👕', icon: '',
description: 'Peças de vestuário', description: 'Peças de vestuário',
subcategories: [ subcategories: [
Subcategory( Subcategory(
@@ -79,7 +79,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
ItemCategory( ItemCategory(
id: 'electronics', id: 'electronics',
name: 'Eletrónica', name: 'Eletrónica',
icon: '💻', icon: '',
description: 'Dispositivos e acessórios tecnológicos', description: 'Dispositivos e acessórios tecnológicos',
subcategories: [ subcategories: [
Subcategory( Subcategory(
@@ -117,7 +117,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
ItemCategory( ItemCategory(
id: 'footwear', id: 'footwear',
name: 'Calçado', name: 'Calçado',
icon: '👟', icon: '',
description: 'Sapatos, botas, sandálias', description: 'Sapatos, botas, sandálias',
subcategories: [ subcategories: [
Subcategory( Subcategory(
@@ -145,7 +145,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
ItemCategory( ItemCategory(
id: 'accessories', id: 'accessories',
name: 'Acessórios', name: 'Acessórios',
icon: '🎒', icon: '',
description: 'Bolsas, relógios, óculos, bijuteria', description: 'Bolsas, relógios, óculos, bijuteria',
subcategories: [ subcategories: [
Subcategory( Subcategory(
@@ -175,7 +175,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
ItemCategory( ItemCategory(
id: 'documents', id: 'documents',
name: 'Documentos', name: 'Documentos',
icon: '📄', icon: '',
description: 'Passaporte, cartões, papéis importantes', description: 'Passaporte, cartões, papéis importantes',
subcategories: [ subcategories: [
Subcategory( Subcategory(
@@ -203,7 +203,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
ItemCategory( ItemCategory(
id: 'other', id: 'other',
name: 'Outros', name: 'Outros',
icon: '📦', icon: '',
description: 'Tudo o resto', description: 'Tudo o resto',
subcategories: [], subcategories: [],
), ),

View File

@@ -33,24 +33,12 @@ class _LoginScreenState extends State<LoginScreen> {
children: [ children: [
const SizedBox(height: 40), const SizedBox(height: 40),
// Blue icon with white box outline // Logo
Container( Image.asset(
width: 80, 'assets/logoDayMaker.png',
height: 80, width: 220,
decoration: BoxDecoration( height: 220,
color: const Color(0xFF0066CC), fit: BoxFit.contain,
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),
),
),
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -88,8 +76,8 @@ class _LoginScreenState extends State<LoginScreen> {
? const Color(0xFF0066CC) ? const Color(0xFF0066CC)
: Colors.transparent, : Colors.transparent,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8), topLeft: Radius.circular(20),
bottomLeft: Radius.circular(8), bottomLeft: Radius.circular(20),
), ),
border: Border.all( border: Border.all(
color: const Color(0xFF0066CC), color: const Color(0xFF0066CC),
@@ -120,8 +108,8 @@ class _LoginScreenState extends State<LoginScreen> {
? const Color(0xFF0066CC) ? const Color(0xFF0066CC)
: Colors.transparent, : Colors.transparent,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(8), topRight: Radius.circular(20),
bottomRight: Radius.circular(8), bottomRight: Radius.circular(20),
), ),
border: Border.all( border: Border.all(
color: const Color(0xFF0066CC), color: const Color(0xFF0066CC),
@@ -151,7 +139,7 @@ class _LoginScreenState extends State<LoginScreen> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFFE0E0E0)), border: Border.all(color: const Color(0xFFE0E0E0)),
), ),
child: TextField( child: TextField(
@@ -177,7 +165,7 @@ class _LoginScreenState extends State<LoginScreen> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFFE0E0E0)), border: Border.all(color: const Color(0xFFE0E0E0)),
), ),
child: TextField( child: TextField(
@@ -201,7 +189,7 @@ class _LoginScreenState extends State<LoginScreen> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFFE0E0E0)), border: Border.all(color: const Color(0xFFE0E0E0)),
), ),
child: TextField( child: TextField(
@@ -239,7 +227,7 @@ class _LoginScreenState extends State<LoginScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0066CC), backgroundColor: const Color(0xFF0066CC),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(42),
), ),
elevation: 0, elevation: 0,
), ),

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'login/login_screen.dart'; import 'login/login_screen.dart';
import 'Screens/home_screen.dart';
import 'supabase_config.dart'; import 'supabase_config.dart';
void main() async { void main() async {
@@ -15,13 +17,14 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final session = Supabase.instance.client.auth.currentSession;
return MaterialApp( return MaterialApp(
title: 'DayMaker', title: 'DayMaker',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0066CC)), colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0066CC)),
useMaterial3: true, useMaterial3: true,
), ),
home: const LoginScreen(), home: session != null ? const HomeScreen() : const LoginScreen(),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
); );
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -60,9 +60,8 @@ flutter:
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: assets:
# - images/a_dot_burr.jpeg - assets/logoDayMaker.png
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images