Initial commit - app-padrao-1.0
This commit is contained in:
578
lib/features/home/presentation/pages/home_page.dart
Normal file
578
lib/features/home/presentation/pages/home_page.dart
Normal file
@@ -0,0 +1,578 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:barber_app/core/database/database_service.dart';
|
||||
import 'package:barber_app/core/theme/app_theme.dart';
|
||||
import 'package:barber_app/features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import 'package:barber_app/features/finances/data/repositories/transaction_repository.dart';
|
||||
import 'package:barber_app/features/haircuts/data/models/haircut_model.dart';
|
||||
import 'package:barber_app/features/products/data/models/product_model.dart';
|
||||
import 'package:barber_app/features/finances/data/models/transaction_model.dart';
|
||||
import 'package:barber_app/features/haircuts/presentation/pages/haircuts_page.dart';
|
||||
import 'package:barber_app/features/products/presentation/pages/products_page.dart';
|
||||
import 'package:barber_app/features/finances/presentation/pages/finances_page.dart';
|
||||
import 'package:barber_app/features/settings/presentation/pages/settings_page.dart';
|
||||
import 'package:barber_app/shared/widgets/stat_card.dart';
|
||||
import 'package:barber_app/features/home/presentation/widgets/revenue_chart_new.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<Widget> _pages = [
|
||||
const HomeContent(),
|
||||
const HaircutsPage(),
|
||||
const ProductsPage(),
|
||||
const FinancesPage(),
|
||||
const SettingsPage(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
extendBody: true, // Importante para a barra flutuante
|
||||
body: IndexedStack(index: _currentIndex, children: _pages),
|
||||
bottomNavigationBar: Container(
|
||||
margin: const EdgeInsets.fromLTRB(24, 0, 24, 30),
|
||||
// height: 70, // REMOVIDO: Altura fixa causava overflow. O child define a altura.
|
||||
constraints: const BoxConstraints(maxHeight: 100),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface.withAlpha(230),
|
||||
borderRadius: BorderRadius.circular(35),
|
||||
border: Border.all(color: Colors.white.withAlpha(20), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(100),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(35),
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeBottom: true,
|
||||
child: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) => setState(() => _currentIndex = index),
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
selectedItemColor: AppColors.primaryColor,
|
||||
unselectedItemColor: AppColors.textSecondary,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
showSelectedLabels: false,
|
||||
showUnselectedLabels: false,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.grid_view_rounded),
|
||||
activeIcon: Icon(Icons.grid_view_rounded),
|
||||
label: 'Início',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.content_cut_rounded),
|
||||
label: 'Cortes',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.inventory_2_rounded),
|
||||
label: 'Produtos',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.account_balance_wallet_rounded),
|
||||
label: 'Finanças',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.tune_rounded),
|
||||
label: 'Ajustes',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomeContent extends StatefulWidget {
|
||||
const HomeContent({super.key});
|
||||
|
||||
@override
|
||||
State<HomeContent> createState() => _HomeContentState();
|
||||
}
|
||||
|
||||
class _HomeContentState extends State<HomeContent> {
|
||||
late TransactionRepository _transactionRepo;
|
||||
late NumberFormat _currencyFormat;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_transactionRepo = TransactionRepository();
|
||||
_currencyFormat = NumberFormat.currency(locale: 'pt_BR', symbol: 'R\$');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = context.watch<AuthBloc>().state is AuthAuthenticated
|
||||
? (context.watch<AuthBloc>().state as AuthAuthenticated).user
|
||||
: null;
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: DatabaseService.transactionsBoxInstance.listenable(),
|
||||
builder: (context, Box<TransactionModel> box, _) {
|
||||
final transactions = _transactionRepo
|
||||
.getAllTransactions()
|
||||
.where(
|
||||
(t) =>
|
||||
t.createdAt.day == DateTime.now().day &&
|
||||
t.createdAt.month == DateTime.now().month &&
|
||||
t.createdAt.year == DateTime.now().year,
|
||||
)
|
||||
.toList();
|
||||
|
||||
final totalBalance = _transactionRepo.getBalance();
|
||||
final todayRevenue = transactions
|
||||
.where((t) => t.type == TransactionType.revenue)
|
||||
.fold(0.0, (sum, t) => sum + t.amount);
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: DatabaseService.haircutsBoxInstance.listenable(),
|
||||
builder: (context, Box<HaircutModel> haircutBox, _) {
|
||||
final todayHaircuts = haircutBox.values
|
||||
.where(
|
||||
(h) =>
|
||||
h.userId == user?.id &&
|
||||
h.dateTime.day == DateTime.now().day &&
|
||||
h.dateTime.month == DateTime.now().month &&
|
||||
h.dateTime.year == DateTime.now().year,
|
||||
)
|
||||
.toList();
|
||||
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: DatabaseService.productsBoxInstance.listenable(),
|
||||
builder: (context, Box<ProductModel> productBox, _) {
|
||||
final lowStockProducts = productBox.values
|
||||
.where(
|
||||
(p) => p.userId == user?.id && p.quantity <= p.minStock,
|
||||
)
|
||||
.toList();
|
||||
|
||||
final pendingReceivables = _transactionRepo
|
||||
.getPendingReceivables();
|
||||
|
||||
return Container(
|
||||
color: AppColors.background,
|
||||
child: CustomScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
slivers: [
|
||||
// Header Imersivo sem AppBar
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 60, 24, 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'BOM DIA, ${(user?.barberName ?? "BARBEIRO").toUpperCase()}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppColors.primaryColor,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DatabaseService.settingsBoxInstance
|
||||
.get(user?.id)
|
||||
?.appName ??
|
||||
user?.barberShopName ??
|
||||
'Barber App',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.displayLarge
|
||||
?.copyWith(fontSize: 28),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white.withAlpha(20),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.notifications_none_rounded,
|
||||
size: 28,
|
||||
),
|
||||
onPressed: () => _showNotifications(
|
||||
context,
|
||||
lowStockProducts,
|
||||
pendingReceivables,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Card Principal Estilo FinTech Elite
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
final homeState = context
|
||||
.findAncestorStateOfType<_HomePageState>();
|
||||
if (homeState != null) {
|
||||
homeState.setState(
|
||||
() => homeState._currentIndex = 3,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.surfaceLight,
|
||||
AppColors.surface,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(36),
|
||||
border: Border.all(
|
||||
color: Colors.white.withAlpha(10),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: AppColors.premiumShadow,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -30,
|
||||
top: -30,
|
||||
child: Icon(
|
||||
Icons.account_balance_wallet_rounded,
|
||||
size: 180,
|
||||
color: AppColors.primaryColor.withAlpha(
|
||||
10,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'SALDO TOTAL',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 12,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_currencyFormat.format(totalBalance),
|
||||
style: TextStyle(
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.textPrimary,
|
||||
fontFamily: 'Outfit',
|
||||
letterSpacing: -1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success.withAlpha(
|
||||
20,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
100,
|
||||
),
|
||||
border: Border.all(
|
||||
color: AppColors.success
|
||||
.withAlpha(40),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.trending_up_rounded,
|
||||
size: 14,
|
||||
color: AppColors.success,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${_currencyFormat.format(todayRevenue)} HOJE',
|
||||
style: const TextStyle(
|
||||
color: AppColors.success,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Atalhos de Ação Rápida
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildActionButton(
|
||||
context,
|
||||
'NOVO CORTE',
|
||||
Icons.add_rounded,
|
||||
() {
|
||||
final homeState = context
|
||||
.findAncestorStateOfType<
|
||||
_HomePageState
|
||||
>();
|
||||
if (homeState != null) {
|
||||
homeState.setState(
|
||||
() => homeState._currentIndex = 1,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildActionButton(
|
||||
context,
|
||||
'PRODUTO',
|
||||
Icons.inventory_2_rounded,
|
||||
() {
|
||||
final homeState = context
|
||||
.findAncestorStateOfType<
|
||||
_HomePageState
|
||||
>();
|
||||
if (homeState != null) {
|
||||
homeState.setState(
|
||||
() => homeState._currentIndex = 2,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Grid de Métricas
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
title: 'Cortes Hoje',
|
||||
value: todayHaircuts.length.toString(),
|
||||
icon: Icons.content_cut_rounded,
|
||||
isPrimary: true, // Agora todos são premium
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: StatCard(
|
||||
title: 'A Receber',
|
||||
value: _currencyFormat.format(
|
||||
pendingReceivables,
|
||||
),
|
||||
icon: Icons.timer_outlined,
|
||||
isPrimary: false,
|
||||
iconColor: AppColors.warning,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'DESEMPENHO SEMANAL',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColors.textSecondary,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
height: 240,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
border: Border.all(
|
||||
color: Colors.white.withAlpha(10),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: const RevenueChartNew(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 120,
|
||||
), // Espaço para a navbar flutuante
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(
|
||||
BuildContext context,
|
||||
String label,
|
||||
IconData icon,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Colors.white.withAlpha(10), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 20, color: AppColors.primaryColor),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 11,
|
||||
fontFamily: 'Outfit',
|
||||
letterSpacing: 1,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNotifications(
|
||||
BuildContext context,
|
||||
List<ProductModel> lowStock,
|
||||
double pending,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Notificações',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (lowStock.isEmpty && pending == 0)
|
||||
const Center(child: Text('Tudo sob controle!')),
|
||||
if (lowStock.isNotEmpty)
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.inventory_2,
|
||||
color: AppColors.error,
|
||||
),
|
||||
title: Text('${lowStock.length} produtos em estoque baixo'),
|
||||
subtitle: const Text('Verifique seu inventário'),
|
||||
),
|
||||
if (pending > 0)
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.attach_money,
|
||||
color: AppColors.warning,
|
||||
),
|
||||
title: const Text('Valores pendentes'),
|
||||
subtitle: Text(_currencyFormat.format(pending)),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user