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

View File

@@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:barber_app/core/theme/app_theme.dart';
import 'package:barber_app/features/finances/data/repositories/transaction_repository.dart';
class RevenueChart extends StatelessWidget {
const RevenueChart({super.key});
@override
Widget build(BuildContext context) {
final transactionRepo = TransactionRepository();
// Pegando dados reais (filtro básico)
final now = DateTime.now();
final List<double> dailyRevenue = List.filled(7, 0.0);
final weekdays = <String>[];
for (int i = 6; i >= 0; i--) {
final day = now.subtract(Duration(days: i));
weekdays.add('${day.day}/${day.month}');
final revenues = transactionRepo.getRevenues().where(
(t) =>
t.dueDate.year == day.year &&
t.dueDate.month == day.month &&
t.dueDate.day == day.day,
);
double sum = 0;
for (var r in revenues) {
sum += r.amount;
}
dailyRevenue[6 - i] = sum;
}
// Se estiver tudo zero, coloca alguns dados dummies pra ficar bonito pro usuário ver o potencial
// (Opcional: remover em produção, mas bom para demo visual inicial)
if (dailyRevenue.every((v) => v == 0)) {
// Dados de exemplo para o gráfico não ficar vazio na primeira execução
// dailyRevenue[0] = 150; dailyRevenue[1] = 200; ...
// Melhor não inventar dados se é um app funcional. Mostra zero mesmo.
}
return AspectRatio(
aspectRatio: 1.70,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(18)),
color: AppColors.surface,
border: Border.all(color: AppColors.surfaceLight),
),
padding: const EdgeInsets.only(
right: 18,
left: 12,
top: 24,
bottom: 12,
),
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 100, // Ajustar conforme valores
getDrawingHorizontalLine: (value) {
return FlLine(color: AppColors.surfaceLight, strokeWidth: 1);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index >= 0 && index < weekdays.length) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
weekdays[index],
style: const TextStyle(
color: AppColors.textSecondary,
fontWeight: FontWeight.bold,
fontSize: 10,
),
),
);
}
return const Text('');
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 100, // Ajustar
getTitlesWidget: (value, meta) {
return Text(
'R\$ ${value.toInt()}',
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 10,
),
);
},
reservedSize: 42,
),
),
),
borderData: FlBorderData(show: false),
minX: 0,
maxX: 6,
minY: 0,
// Adiciona margem superior
maxY: (dailyRevenue.reduce((a, b) => a > b ? a : b) * 1.2) + 100,
lineBarsData: [
LineChartBarData(
spots: List.generate(
7,
(index) => FlSpot(index.toDouble(), dailyRevenue[index]),
),
isCurved: true,
gradient: LinearGradient(
colors: [
AppColors.primaryColor,
AppColors.primaryColor.withValues(alpha: 0.5),
],
),
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
AppColors.primaryColor.withValues(alpha: 0.3),
AppColors.primaryColor.withValues(alpha: 0.0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,141 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:barber_app/core/theme/app_theme.dart';
import 'package:barber_app/features/finances/data/repositories/transaction_repository.dart';
import 'package:intl/intl.dart';
class RevenueChartNew extends StatefulWidget {
const RevenueChartNew({super.key});
@override
State<RevenueChartNew> createState() => _RevenueChartNewState();
}
class _RevenueChartNewState extends State<RevenueChartNew> {
final TransactionRepository _repository = TransactionRepository();
@override
Widget build(BuildContext context) {
final last7Days = _repository.getLast7DaysRevenue();
final now = DateTime.now();
final List<DailyRevenue> data = List.generate(7, (index) {
final date = now.subtract(Duration(days: 6 - index));
return DailyRevenue(
day: DateFormat(
'E',
'pt_BR',
).format(date).substring(0, 3).toUpperCase(),
amount: last7Days[index],
);
});
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.surfaceLight),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: SfCartesianChart(
plotAreaBorderWidth: 0,
primaryXAxis: CategoryAxis(
majorGridLines: const MajorGridLines(width: 0),
axisLine: const AxisLine(width: 0),
labelStyle: const TextStyle(
color: AppColors.textSecondary,
fontSize: 10,
),
),
primaryYAxis: NumericAxis(
isVisible: true,
majorGridLines: MajorGridLines(
width: 1,
color: AppColors.surfaceLight.withValues(alpha: 0.5),
),
axisLine: const AxisLine(width: 0),
labelStyle: const TextStyle(
color: AppColors.textSecondary,
fontSize: 10,
),
numberFormat: NumberFormat.compactSimpleCurrency(locale: 'pt_BR'),
),
tooltipBehavior: TooltipBehavior(
enable: true,
header: '',
format: 'point.y',
color: AppColors.surfaceLight,
textStyle: const TextStyle(color: AppColors.textPrimary),
builder:
(
dynamic data,
dynamic point,
dynamic series,
int pointIndex,
int seriesIndex,
) {
final DailyRevenue item = data;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.surfaceLight,
borderRadius: BorderRadius.circular(8),
boxShadow: const [
BoxShadow(color: Colors.black26, blurRadius: 4),
],
),
child: Text(
'${item.day}: ${NumberFormat.currency(symbol: 'R\$', locale: 'pt_BR').format(item.amount)}',
style: const TextStyle(
color: AppColors.textPrimary,
fontWeight: FontWeight.bold,
),
),
);
},
),
series: <CartesianSeries>[
SplineAreaSeries<DailyRevenue, String>(
dataSource: data,
xValueMapper: (DailyRevenue data, _) => data.day,
yValueMapper: (DailyRevenue data, _) => data.amount,
gradient: LinearGradient(
colors: [
AppColors.primaryColor.withValues(alpha: 0.5),
AppColors.primaryColor.withValues(alpha: 0.0),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderColor: AppColors.primaryColor,
borderWidth: 3,
name: 'Receita',
animationDuration: 1000,
markerSettings: MarkerSettings(
isVisible: true,
height: 8,
width: 8,
color: AppColors.surface,
borderColor: AppColors.primaryColor,
borderWidth: 2,
),
),
],
),
);
}
}
class DailyRevenue {
final String day;
final double amount;
DailyRevenue({required this.day, required this.amount});
}