Initial commit - app-padrao-1.0

This commit is contained in:
Erik Silva
2025-12-19 23:29:24 -03:00
commit ec76d3d633
205 changed files with 13131 additions and 0 deletions

View 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),
],
),
);
},
);
}
}