477 lines
14 KiB
Dart
477 lines
14 KiB
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 {
|
||
const HomeScreen({super.key});
|
||
|
||
@override
|
||
State<HomeScreen> createState() => _HomeScreenState();
|
||
}
|
||
|
||
class _HomeScreenState extends State<HomeScreen> {
|
||
int _selectedIndex = 0;
|
||
|
||
final List<Widget> _screens = [
|
||
const _HomeContent(),
|
||
const _ItemsScreen(),
|
||
const _WeekScreen(),
|
||
const _ProfileScreen(),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFFFFE5CC),
|
||
body: _screens[_selectedIndex],
|
||
bottomNavigationBar: BottomNavigationBar(
|
||
currentIndex: _selectedIndex,
|
||
onTap: (index) => setState(() => _selectedIndex = index),
|
||
selectedItemColor: const Color(0xFF0066CC),
|
||
unselectedItemColor: const Color(0xFF666666),
|
||
backgroundColor: Colors.white,
|
||
type: BottomNavigationBarType.fixed,
|
||
items: const [
|
||
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'InÃcio'),
|
||
BottomNavigationBarItem(
|
||
icon: Icon(Icons.inventory_2_outlined),
|
||
label: 'Itens',
|
||
),
|
||
BottomNavigationBarItem(
|
||
icon: Icon(Icons.calendar_today_outlined),
|
||
label: 'Semana',
|
||
),
|
||
BottomNavigationBarItem(
|
||
icon: Icon(Icons.person_outline),
|
||
label: 'Perfil',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _HomeContent extends StatefulWidget {
|
||
const _HomeContent();
|
||
|
||
@override
|
||
State<_HomeContent> createState() => _HomeContentState();
|
||
}
|
||
|
||
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();
|
||
_loadData();
|
||
}
|
||
|
||
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) return;
|
||
|
||
// 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 home: $e');
|
||
setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SafeArea(
|
||
child: Stack(
|
||
children: [
|
||
Column(
|
||
children: [
|
||
// App Bar
|
||
Container(
|
||
color: const Color(0xFF0066CC),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 20,
|
||
vertical: 16,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text(
|
||
'DayMaker',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'$_itemCount itens',
|
||
style: const TextStyle(
|
||
color: Colors.white70,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
IconButton(
|
||
icon: const Icon(
|
||
Icons.notifications_outlined,
|
||
color: Colors.white,
|
||
),
|
||
onPressed: () {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('Notificações'),
|
||
backgroundColor: Color(0xFF0066CC),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
Expanded(
|
||
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(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
// Floating Action Button
|
||
Positioned(
|
||
bottom: 80,
|
||
right: 20,
|
||
child: FloatingActionButton(
|
||
onPressed: () {
|
||
Navigator.of(context)
|
||
.push(
|
||
MaterialPageRoute(builder: (_) => const AddItemScreen()),
|
||
)
|
||
.then((_) => _loadData());
|
||
},
|
||
backgroundColor: const Color(0xFF0066CC),
|
||
child: const Icon(Icons.add, color: Colors.white),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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 StatelessWidget {
|
||
const _ItemsScreen();
|
||
|
||
@override
|
||
Widget build(BuildContext context) => const ItemScreen();
|
||
}
|
||
|
||
class _WeekScreen extends StatelessWidget {
|
||
const _WeekScreen();
|
||
|
||
@override
|
||
Widget build(BuildContext context) => const WeekScreen();
|
||
}
|
||
|
||
class _ProfileScreen extends StatelessWidget {
|
||
const _ProfileScreen();
|
||
|
||
@override
|
||
Widget build(BuildContext context) => const PerfilScreen();
|
||
}
|