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: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';
|
||||||
|
|
||||||
@@ -16,9 +18,22 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
|||||||
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();
|
||||||
@@ -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(
|
||||||
@@ -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(
|
||||||
@@ -251,19 +346,28 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
|||||||
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -313,7 +417,9 @@ 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(),
|
||||||
@@ -322,10 +428,7 @@ class _AddItemScreenState extends State<AddItemScreen> {
|
|||||||
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(
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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(
|
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: [],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user