Atualização tela semanal |Adicoinar item com imagem
This commit is contained in:
BIN
assets/logoDayMaker.png
Normal file
BIN
assets/logoDayMaker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
@@ -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<AddItemScreen> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
|
||||
|
||||
ItemCategory? _selectedCategory;
|
||||
Subcategory? _selectedSubcategory;
|
||||
final Set<String> _selectedTags = {};
|
||||
|
||||
XFile? _selectedImage;
|
||||
|
||||
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
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
@@ -31,7 +46,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
||||
_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<AddItemScreen> {
|
||||
setState(() {
|
||||
_selectedSubcategory = subcategory;
|
||||
_selectedTags.clear();
|
||||
|
||||
|
||||
if (_selectedCategory != null && subcategory != null) {
|
||||
_autoAssignTags(_selectedCategory!.id, subcategory.id);
|
||||
}
|
||||
@@ -87,17 +102,49 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
||||
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<AddItemScreen> {
|
||||
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<AddItemScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
@@ -211,7 +302,10 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
||||
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<AddItemScreen> {
|
||||
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<AddItemScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
|
||||
SizedBox(
|
||||
height: 60,
|
||||
child: ListView.builder(
|
||||
@@ -245,25 +340,34 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
||||
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<AddItemScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: 8),
|
||||
if (_selectedSubcategory != null)
|
||||
Text(
|
||||
@@ -299,7 +403,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
@@ -313,19 +417,18 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
||||
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<AddItemScreen> {
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
|
||||
@@ -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<HomeScreen> {
|
||||
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<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
|
||||
void 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 {
|
||||
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<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) {
|
||||
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<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();
|
||||
|
||||
@override
|
||||
State<_ItemsScreen> createState() => _ItemsScreenState();
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
722
lib/Screens/item_screen.dart
Normal file
722
lib/Screens/item_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
544
lib/Screens/week_screen.dart
Normal file
544
lib/Screens/week_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
|
||||
ItemCategory(
|
||||
id: 'clothing',
|
||||
name: 'Roupa',
|
||||
icon: '👕',
|
||||
icon: '',
|
||||
description: 'Peças de vestuário',
|
||||
subcategories: [
|
||||
Subcategory(
|
||||
@@ -79,7 +79,7 @@ final List<ItemCategory> 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<ItemCategory> ITEM_CATEGORIES = [
|
||||
ItemCategory(
|
||||
id: 'footwear',
|
||||
name: 'Calçado',
|
||||
icon: '👟',
|
||||
icon: '',
|
||||
description: 'Sapatos, botas, sandálias',
|
||||
subcategories: [
|
||||
Subcategory(
|
||||
@@ -145,7 +145,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
|
||||
ItemCategory(
|
||||
id: 'accessories',
|
||||
name: 'Acessórios',
|
||||
icon: '🎒',
|
||||
icon: '',
|
||||
description: 'Bolsas, relógios, óculos, bijuteria',
|
||||
subcategories: [
|
||||
Subcategory(
|
||||
@@ -175,7 +175,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
|
||||
ItemCategory(
|
||||
id: 'documents',
|
||||
name: 'Documentos',
|
||||
icon: '📄',
|
||||
icon: '',
|
||||
description: 'Passaporte, cartões, papéis importantes',
|
||||
subcategories: [
|
||||
Subcategory(
|
||||
@@ -203,7 +203,7 @@ final List<ItemCategory> ITEM_CATEGORIES = [
|
||||
ItemCategory(
|
||||
id: 'other',
|
||||
name: 'Outros',
|
||||
icon: '📦',
|
||||
icon: '',
|
||||
description: 'Tudo o resto',
|
||||
subcategories: [],
|
||||
),
|
||||
|
||||
@@ -33,24 +33,12 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
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<LoginScreen> {
|
||||
? 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<LoginScreen> {
|
||||
? 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<LoginScreen> {
|
||||
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<LoginScreen> {
|
||||
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<LoginScreen> {
|
||||
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<LoginScreen> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0066CC),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(42),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
BIN
logoDayMaker.png
BIN
logoDayMaker.png
Binary file not shown.
|
Before Width: | Height: | Size: 268 KiB |
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user