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),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
154
lib/features/home/presentation/widgets/revenue_chart.dart
Normal file
154
lib/features/home/presentation/widgets/revenue_chart.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
141
lib/features/home/presentation/widgets/revenue_chart_new.dart
Normal file
141
lib/features/home/presentation/widgets/revenue_chart_new.dart
Normal 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});
|
||||
}
|
||||
Reference in New Issue
Block a user