579 lines
24 KiB
Dart
579 lines
24 KiB
Dart
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),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|