From 917b4e213c8c4cc72e94f66eb0a19b8ffe39e4b6 Mon Sep 17 00:00:00 2001 From: Suriya Date: Wed, 18 Feb 2026 18:44:16 +0530 Subject: [PATCH] reports page m4 ui done --- .../reports_repository_impl.dart | 32 +- .../src/domain/entities/daily_ops_report.dart | 3 + .../lib/src/domain/entities/spend_report.dart | 24 +- .../pages/coverage_report_page.dart | 124 ++-- .../pages/daily_ops_report_page.dart | 52 +- .../pages/no_show_report_page.dart | 30 +- .../pages/performance_report_page.dart | 15 +- .../presentation/pages/spend_report_page.dart | 561 +++++++----------- .../dataconnect/connector/reports/queries.gql | 6 +- 9 files changed, 410 insertions(+), 437 deletions(-) diff --git a/apps/mobile/packages/features/client/reports/lib/src/data/repositories_impl/reports_repository_impl.dart b/apps/mobile/packages/features/client/reports/lib/src/data/repositories_impl/reports_repository_impl.dart index 46d8b323..d395f8b8 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/data/repositories_impl/reports_repository_impl.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/data/repositories_impl/reports_repository_impl.dart @@ -90,6 +90,7 @@ class ReportsRepositoryImpl implements ReportsRepository { final List spendInvoices = []; final Map dailyAggregates = {}; + final Map industryAggregates = {}; for (final inv in invoices) { final amount = (inv.amount ?? 0.0).toDouble(); @@ -104,6 +105,9 @@ class ReportsRepositoryImpl implements ReportsRepository { overdueInvoices++; } + final industry = inv.vendor?.serviceSpecialty ?? 'Other'; + industryAggregates[industry] = (industryAggregates[industry] ?? 0.0) + amount; + final issueDateTime = inv.issueDate.toDateTime(); spendInvoices.add(SpendInvoice( id: inv.id, @@ -112,6 +116,7 @@ class ReportsRepositoryImpl implements ReportsRepository { amount: amount, status: statusStr, vendorName: inv.vendor?.companyName ?? 'Unknown', + industry: industry, )); // Chart data aggregation @@ -119,19 +124,40 @@ class ReportsRepositoryImpl implements ReportsRepository { dailyAggregates[date] = (dailyAggregates[date] ?? 0.0) + amount; } - final List chartData = dailyAggregates.entries + // Ensure chart data covers all days in range + final Map completeDailyAggregates = {}; + for (int i = 0; i <= endDate.difference(startDate).inDays; i++) { + final date = startDate.add(Duration(days: i)); + final normalizedDate = DateTime(date.year, date.month, date.day); + completeDailyAggregates[normalizedDate] = + dailyAggregates[normalizedDate] ?? 0.0; + } + + final List chartData = completeDailyAggregates.entries .map((e) => SpendChartPoint(date: e.key, amount: e.value)) .toList() - ..sort((a, b) => a.date.compareTo(b.date)); + ..sort((a, b) => a.date.compareTo(b.date)); + + final List industryBreakdown = industryAggregates.entries + .map((e) => SpendIndustryCategory( + name: e.key, + amount: e.value, + percentage: totalSpend > 0 ? (e.value / totalSpend * 100) : 0, + )) + .toList() + ..sort((a, b) => b.amount.compareTo(a.amount)); + + final daysCount = endDate.difference(startDate).inDays + 1; return SpendReport( totalSpend: totalSpend, - averageCost: invoices.isEmpty ? 0 : totalSpend / invoices.length, + averageCost: daysCount > 0 ? totalSpend / daysCount : 0, paidInvoices: paidInvoices, pendingInvoices: pendingInvoices, overdueInvoices: overdueInvoices, invoices: spendInvoices, chartData: chartData, + industryBreakdown: industryBreakdown, ); }); } diff --git a/apps/mobile/packages/features/client/reports/lib/src/domain/entities/daily_ops_report.dart b/apps/mobile/packages/features/client/reports/lib/src/domain/entities/daily_ops_report.dart index dbc22ba6..fabf262d 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/domain/entities/daily_ops_report.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/domain/entities/daily_ops_report.dart @@ -34,6 +34,7 @@ class DailyOpsShift extends Equatable { final int workersNeeded; final int filled; final String status; + final double? hourlyRate; const DailyOpsShift({ required this.id, @@ -44,6 +45,7 @@ class DailyOpsShift extends Equatable { required this.workersNeeded, required this.filled, required this.status, + this.hourlyRate, }); @override @@ -56,5 +58,6 @@ class DailyOpsShift extends Equatable { workersNeeded, filled, status, + hourlyRate, ]; } diff --git a/apps/mobile/packages/features/client/reports/lib/src/domain/entities/spend_report.dart b/apps/mobile/packages/features/client/reports/lib/src/domain/entities/spend_report.dart index 2e8b0829..3e342c00 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/domain/entities/spend_report.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/domain/entities/spend_report.dart @@ -17,8 +17,11 @@ class SpendReport extends Equatable { required this.overdueInvoices, required this.invoices, required this.chartData, + required this.industryBreakdown, }); + final List industryBreakdown; + @override List get props => [ totalSpend, @@ -28,9 +31,25 @@ class SpendReport extends Equatable { overdueInvoices, invoices, chartData, + industryBreakdown, ]; } +class SpendIndustryCategory extends Equatable { + final String name; + final double amount; + final double percentage; + + const SpendIndustryCategory({ + required this.name, + required this.amount, + required this.percentage, + }); + + @override + List get props => [name, amount, percentage]; +} + class SpendInvoice extends Equatable { final String id; final String invoiceNumber; @@ -46,10 +65,13 @@ class SpendInvoice extends Equatable { required this.amount, required this.status, required this.vendorName, + this.industry, }); + final String? industry; + @override - List get props => [id, invoiceNumber, issueDate, amount, status, vendorName]; + List get props => [id, invoiceNumber, issueDate, amount, status, vendorName, industry]; } class SpendChartPoint extends Equatable { diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart index 06031d10..7ee23f6a 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/coverage_report_page.dart @@ -56,7 +56,7 @@ class _CoverageReportPageState extends State { top: 60, left: 20, right: 20, - bottom: 32, + bottom: 80, // Increased bottom padding for overlap background ), decoration: const BoxDecoration( gradient: LinearGradient( @@ -160,53 +160,60 @@ class _CoverageReportPageState extends State { ), ], ), - - const SizedBox(height: 24), - - // ── 3 summary stat chips (matches prototype) ── - Row( - children: [ - _HeaderStatChip( - icon: UiIcons.trendingUp, - label: 'Avg Coverage', - value: - '${report.overallCoverage.toStringAsFixed(0)}%', - ), - const SizedBox(width: 12), - _HeaderStatChip( - icon: UiIcons.checkCircle, - label: 'Full', - value: fullDays.toString(), - ), - const SizedBox(width: 12), - _HeaderStatChip( - icon: UiIcons.warning, - label: 'Needs Help', - value: needsHelpDays.toString(), - isAlert: needsHelpDays > 0, - ), - ], - ), ], ), ), + // ── 3 summary stat chips (Moved here for overlap) ── + Transform.translate( + offset: const Offset(0, -60), // Pull up to overlap header + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + _CoverageStatCard( + icon: UiIcons.trendingUp, + label: 'Avg Coverage', + value: '${report.overallCoverage.toStringAsFixed(0)}%', + iconColor: UiColors.primary, + ), + const SizedBox(width: 12), + _CoverageStatCard( + icon: UiIcons.checkCircle, + label: 'Full', + value: fullDays.toString(), + iconColor: UiColors.success, + ), + const SizedBox(width: 12), + _CoverageStatCard( + icon: UiIcons.warning, + label: 'Needs Help', + value: needsHelpDays.toString(), + iconColor: UiColors.error, + ), + ], + ), + ), + ), + // ── Content ────────────────────────────────────────── Transform.translate( - offset: const Offset(0, -16), + offset: const Offset(0, -60), // Pull up to overlap header child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 32), + // Section label const Text( 'NEXT 7 DAYS', style: TextStyle( - fontSize: 11, + fontSize: 14, fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 1.2, + color: UiColors.textPrimary, + letterSpacing: 0.5, ), ), const SizedBox(height: 16), @@ -250,57 +257,68 @@ class _CoverageReportPageState extends State { } // ── Header stat chip (inside the blue header) ───────────────────────────────── -class _HeaderStatChip extends StatelessWidget { +// ── Header stat card (boxes inside the blue header overlap) ─────────────────── +class _CoverageStatCard extends StatelessWidget { final IconData icon; final String label; final String value; - final bool isAlert; + final Color iconColor; - const _HeaderStatChip({ + const _CoverageStatCard({ required this.icon, required this.label, required this.value, - this.isAlert = false, + required this.iconColor, }); @override Widget build(BuildContext context) { return Expanded( child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10), + padding: const EdgeInsets.all(16), // Increased padding decoration: BoxDecoration( - color: UiColors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(10), + color: UiColors.white, + borderRadius: BorderRadius.circular(16), // More rounded + boxShadow: [ + BoxShadow( + color: UiColors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( icon, - size: 12, - color: isAlert - ? const Color(0xFFFFD580) - : UiColors.white.withOpacity(0.8), + size: 14, + color: iconColor, ), - const SizedBox(width: 4), - Text( - label, - style: TextStyle( - fontSize: 10, - color: UiColors.white.withOpacity(0.8), + const SizedBox(width: 6), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 11, + color: UiColors.textSecondary, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, ), ), ], ), - const SizedBox(height: 4), + const SizedBox(height: 8), Text( value, style: const TextStyle( - fontSize: 20, + fontSize: 20, // Slightly smaller to fit if needed fontWeight: FontWeight.bold, - color: UiColors.white, + color: UiColors.textPrimary, ), ), ], @@ -344,7 +362,7 @@ class _DayCoverageCard extends StatelessWidget { final badgeBg = percentage >= 95 ? UiColors.tagSuccess : percentage >= 80 - ? UiColors.tagInProgress + ? UiColors.primary.withOpacity(0.1) // Blue tint : UiColors.tagError; return Container( diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart index 4e677a6f..66772cef 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/daily_ops_report_page.dart @@ -243,7 +243,7 @@ class _DailyOpsReportPageState extends State { physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: 12, crossAxisSpacing: 12, - childAspectRatio: 1.4, + childAspectRatio: 1.2, children: [ _OpsStatCard( label: context.t.client_reports @@ -314,16 +314,16 @@ class _DailyOpsReportPageState extends State { ], ), - const SizedBox(height: 24), + const SizedBox(height: 8), Text( context.t.client_reports.daily_ops_report .all_shifts_title .toUpperCase(), style: const TextStyle( - fontSize: 12, + fontSize: 14, fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 1.2, + color: UiColors.textPrimary, + letterSpacing: 0.5, ), ), const SizedBox(height: 12), @@ -341,9 +341,12 @@ class _DailyOpsReportPageState extends State { title: shift.title, location: shift.location, time: - '${DateFormat('hh:mm a').format(shift.startTime)} - ${DateFormat('hh:mm a').format(shift.endTime)}', + '${DateFormat('HH:mm').format(shift.startTime)} - ${DateFormat('HH:mm').format(shift.endTime)}', workers: '${shift.filled}/${shift.workersNeeded}', + rate: shift.hourlyRate != null + ? '\$${shift.hourlyRate!.toStringAsFixed(0)}/hr' + : '-', status: shift.status.replaceAll('_', ' '), statusColor: shift.status == 'COMPLETED' ? UiColors.success @@ -399,15 +402,15 @@ class _OpsStatCard extends StatelessWidget { offset: const Offset(0, 2), ), ], - border: Border(left: BorderSide(color: color, width: 4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 8), Expanded( child: Text( label, @@ -420,7 +423,6 @@ class _OpsStatCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), ), - Icon(icon, size: 14, color: color), ], ), Column( @@ -429,16 +431,29 @@ class _OpsStatCard extends StatelessWidget { Text( value, style: const TextStyle( - fontSize: 24, + fontSize: 28, fontWeight: FontWeight.bold, color: UiColors.textPrimary, ), ), - Text( - subValue, - style: const TextStyle( - fontSize: 10, - color: UiColors.textSecondary, + const SizedBox(height: 6), + // Colored pill badge (matches prototype) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: color.withOpacity(0.12), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + subValue, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: color, + ), ), ), ], @@ -454,6 +469,7 @@ class _ShiftListItem extends StatelessWidget { final String location; final String time; final String workers; + final String rate; final String status; final Color statusColor; @@ -462,6 +478,7 @@ class _ShiftListItem extends StatelessWidget { required this.location, required this.time, required this.workers, + required this.rate, required this.status, required this.statusColor, }); @@ -557,6 +574,11 @@ class _ShiftListItem extends StatelessWidget { UiIcons.users, context.t.client_reports.daily_ops_report.shift_item.workers, workers), + _infoItem( + context, + UiIcons.trendingUp, + 'Rate', + rate), ], ), ], diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart index 9a735022..d70c8d79 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/no_show_report_page.dart @@ -296,7 +296,7 @@ class _SummaryChip extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 12), decoration: BoxDecoration( color: UiColors.white, borderRadius: BorderRadius.circular(12), @@ -311,24 +311,32 @@ class _SummaryChip extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, size: 16, color: iconColor), + Row( + children: [ + Icon(icon, size: 12, color: iconColor), + const SizedBox(width: 4), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 10, + color: iconColor, + fontWeight: FontWeight.w600, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), const SizedBox(height: 8), Text( value, style: const TextStyle( - fontSize: 22, + fontSize: 26, fontWeight: FontWeight.bold, color: UiColors.textPrimary, ), ), - const SizedBox(height: 2), - Text( - label, - style: const TextStyle( - fontSize: 11, - color: UiColors.textSecondary, - ), - ), ], ), ); diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart index cba7597a..4dae406e 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/performance_report_page.dart @@ -432,26 +432,27 @@ class _KpiRow extends StatelessWidget { ], ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + // Value + badge inline (matches prototype) + Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( kpi.displayValue, style: const TextStyle( - fontSize: 15, + fontSize: 16, fontWeight: FontWeight.bold, color: UiColors.textPrimary, ), ), - const SizedBox(height: 2), + const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( - horizontal: 8, + horizontal: 7, vertical: 3, ), decoration: BoxDecoration( color: badgeBg, - borderRadius: BorderRadius.circular(10), + borderRadius: BorderRadius.circular(8), ), child: Text( badgeText, @@ -473,7 +474,7 @@ class _KpiRow extends StatelessWidget { value: (kpi.value / 100).clamp(0.0, 1.0), backgroundColor: UiColors.bgSecondary, valueColor: AlwaysStoppedAnimation(kpi.barColor), - minHeight: 5, + minHeight: 6, ), ), ], diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart index d4266da2..9f20bcdd 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/spend_report_page.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/intl.dart'; +import 'package:client_reports/src/domain/entities/spend_report.dart'; class SpendReportPage extends StatefulWidget { const SpendReportPage({super.key}); @@ -17,8 +18,19 @@ class SpendReportPage extends StatefulWidget { } class _SpendReportPageState extends State { - DateTime _startDate = DateTime.now().subtract(const Duration(days: 6)); - DateTime _endDate = DateTime.now(); + late DateTime _startDate; + late DateTime _endDate; + + @override + void initState() { + super.initState(); + final now = DateTime.now(); + // Monday alignment logic + final diff = now.weekday - DateTime.monday; + final monday = now.subtract(Duration(days: diff)); + _startDate = DateTime(monday.year, monday.month, monday.day); + _endDate = _startDate.add(const Duration(days: 6, hours: 23, minutes: 59, seconds: 59)); + } @override Widget build(BuildContext context) { @@ -48,14 +60,10 @@ class _SpendReportPageState extends State { top: 60, left: 20, right: 20, - bottom: 32, + bottom: 80, // Overlap space ), decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [UiColors.success, UiColors.tagSuccess], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), + color: UiColors.primary, // Blue background per prototype ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -128,7 +136,7 @@ class _SpendReportPageState extends State { const Icon( UiIcons.download, size: 14, - color: UiColors.success, + color: UiColors.primary, ), const SizedBox(width: 6), Text( @@ -137,7 +145,7 @@ class _SpendReportPageState extends State { .split(' ') .first, style: const TextStyle( - color: UiColors.success, + color: UiColors.primary, fontSize: 12, fontWeight: FontWeight.bold, ), @@ -152,50 +160,50 @@ class _SpendReportPageState extends State { // Content Transform.translate( - offset: const Offset(0, -16), + offset: const Offset(0, -60), // Pull up to overlap child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Summary Cards + // Summary Cards (New Style) Row( children: [ Expanded( - child: _SpendSummaryCard( + child: _SpendStatCard( label: context.t.client_reports.spend_report .summary.total_spend, - value: NumberFormat.currency(symbol: r'$') + value: NumberFormat.currency( + symbol: r'$', decimalDigits: 0) .format(report.totalSpend), - change: '', // Can be calculated if needed - period: context.t.client_reports + pillText: context.t.client_reports .spend_report.summary.this_week, - color: UiColors.textSuccess, + themeColor: UiColors.success, icon: UiIcons.dollar, ), ), const SizedBox(width: 12), Expanded( - child: _SpendSummaryCard( + child: _SpendStatCard( label: context.t.client_reports.spend_report .summary.avg_daily, - value: NumberFormat.currency(symbol: r'$') + value: NumberFormat.currency( + symbol: r'$', decimalDigits: 0) .format(report.averageCost), - change: '', - period: context.t.client_reports + pillText: context.t.client_reports .spend_report.summary.per_day, - color: UiColors.primary, - icon: UiIcons.chart, + themeColor: UiColors.primary, + icon: UiIcons.trendingUp, ), ), ], ), const SizedBox(height: 24), - // Chart Section + // Daily Spend Trend Chart Container( - height: 300, - padding: const EdgeInsets.all(16), + height: 320, + padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: UiColors.white, borderRadius: BorderRadius.circular(16), @@ -210,50 +218,15 @@ class _SpendReportPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - context.t.client_reports.spend_report - .chart_title, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: UiColors.textPrimary, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: - BorderRadius.circular(8), - ), - child: Row( - children: [ - Text( - context.t.client_reports.tabs.week, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - ), - ), - const Icon( - UiIcons.chevronDown, - size: 10, - color: UiColors.textSecondary, - ), - ], - ), - ), - ], + const Text( + 'Daily Spend Trend', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: UiColors.textPrimary, + ), ), - const SizedBox(height: 24), + const SizedBox(height: 32), Expanded( child: _SpendBarChart( chartData: report.chartData), @@ -263,71 +236,11 @@ class _SpendReportPageState extends State { ), const SizedBox(height: 24), - // Status Distribution - Row( - children: [ - Expanded( - child: _StatusMiniCard( - label: 'Paid', - value: report.paidInvoices.toString(), - color: UiColors.success, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _StatusMiniCard( - label: 'Pending', - value: report.pendingInvoices.toString(), - color: UiColors.textWarning, - ), - ), - const SizedBox(width: 8), - Expanded( - child: _StatusMiniCard( - label: 'Overdue', - value: report.overdueInvoices.toString(), - color: UiColors.error, - ), - ), - ], - ), - const SizedBox(height: 32), - Text( - 'RECENT INVOICES', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 1.2, - ), + // Spend by Industry + _SpendByIndustryCard( + industries: report.industryBreakdown, ), - const SizedBox(height: 16), - - // Invoice List - if (report.invoices.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 40), - child: Center( - child: - Text('No invoices found for this period'), - ), - ) - else - ...report.invoices.map((inv) => _InvoiceListItem( - invoice: inv.invoiceNumber, - vendor: inv.vendorName, - date: DateFormat('MMM dd, yyyy') - .format(inv.issueDate), - amount: NumberFormat.currency(symbol: r'$') - .format(inv.amount), - status: inv.status, - statusColor: inv.status == 'PAID' - ? UiColors.success - : inv.status == 'PENDING' - ? UiColors.textWarning - : UiColors.error, - )), const SizedBox(height: 100), ], @@ -356,8 +269,9 @@ class _SpendBarChart extends StatelessWidget { return BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, - maxY: (chartData.fold( - 0, (prev, element) => element.amount > prev ? element.amount : prev) * + maxY: (chartData.fold(0, + (prev, element) => + element.amount > prev ? element.amount : prev) * 1.2) .ceilToDouble(), barTouchData: BarTouchData( @@ -379,14 +293,34 @@ class _SpendBarChart extends StatelessWidget { bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, + reservedSize: 30, getTitlesWidget: (value, meta) { if (value.toInt() >= chartData.length) return const SizedBox(); final date = chartData[value.toInt()].date; return SideTitleWidget( axisSide: meta.axisSide, - space: 4, + space: 8, child: Text( DateFormat('E').format(date), + style: const TextStyle( + color: UiColors.textSecondary, + fontSize: 11, + ), + ), + ); + }, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: (value, meta) { + if (value == 0) return const SizedBox(); + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text( + '\$${(value / 1000).toStringAsFixed(0)}k', style: const TextStyle( color: UiColors.textSecondary, fontSize: 10, @@ -396,9 +330,6 @@ class _SpendBarChart extends StatelessWidget { }, ), ), - leftTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), @@ -406,7 +337,15 @@ class _SpendBarChart extends StatelessWidget { sideTitles: SideTitles(showTitles: false), ), ), - gridData: const FlGridData(show: false), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 1000, + getDrawingHorizontalLine: (value) => FlLine( + color: UiColors.bgSecondary, + strokeWidth: 1, + ), + ), borderData: FlBorderData(show: false), barGroups: List.generate( chartData.length, @@ -416,7 +355,7 @@ class _SpendBarChart extends StatelessWidget { BarChartRodData( toY: chartData[index].amount, color: UiColors.success, - width: 16, + width: 12, borderRadius: const BorderRadius.vertical( top: Radius.circular(4), ), @@ -429,20 +368,18 @@ class _SpendBarChart extends StatelessWidget { } } -class _SpendSummaryCard extends StatelessWidget { +class _SpendStatCard extends StatelessWidget { final String label; final String value; - final String change; - final String period; - final Color color; + final String pillText; + final Color themeColor; final IconData icon; - const _SpendSummaryCard({ + const _SpendStatCard({ required this.label, required this.value, - required this.change, - required this.period, - required this.color, + required this.pillText, + required this.themeColor, required this.icon, }); @@ -450,6 +387,78 @@ class _SpendSummaryCard extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: UiColors.black.withOpacity(0.06), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 14, color: themeColor), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 12, + color: UiColors.textSecondary, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + value, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: UiColors.textPrimary, + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: themeColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + pillText, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: themeColor, + ), + ), + ), + ], + ), + ); + } +} + +class _SpendByIndustryCard extends StatelessWidget { + final List industries; + + const _SpendByIndustryCard({required this.industries}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: UiColors.white, borderRadius: BorderRadius.circular(16), @@ -464,209 +473,73 @@ class _SpendSummaryCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon(icon, size: 16, color: color), - ), - if (change.isNotEmpty) - Text( - change, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: change.startsWith('+') - ? UiColors.error - : UiColors.success, - ), - ), - ], - ), - const SizedBox(height: 12), - Text( - label, - style: const TextStyle( - fontSize: 12, - color: UiColors.textSecondary, - ), - ), - const SizedBox(height: 4), - Text( - value, - style: const TextStyle( - fontSize: 18, + const Text( + 'Spend by Industry', + style: TextStyle( + fontSize: 14, fontWeight: FontWeight.bold, color: UiColors.textPrimary, ), ), - const SizedBox(height: 4), - Text( - period, - style: const TextStyle( - fontSize: 10, - color: UiColors.textDescription, - ), - ), - ], - ), - ); - } -} - -class _StatusMiniCard extends StatelessWidget { - final String label; - final String value; - final Color color; - - const _StatusMiniCard({ - required this.label, - required this.value, - required this.color, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - decoration: BoxDecoration( - color: color.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.1)), - ), - child: Column( - children: [ - Text( - value, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: color, - ), - ), - const SizedBox(height: 2), - Text( - label, - style: const TextStyle( - fontSize: 10, - color: UiColors.textSecondary, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } -} - -class _InvoiceListItem extends StatelessWidget { - final String invoice; - final String vendor; - final String date; - final String amount; - final String status; - final Color statusColor; - - const _InvoiceListItem({ - required this.invoice, - required this.vendor, - required this.date, - required this.amount, - required this.status, - required this.statusColor, - }); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: UiColors.black.withOpacity(0.02), - blurRadius: 10, - ), - ], - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon(UiIcons.file, size: 20, color: statusColor), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - invoice, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: UiColors.textPrimary, - ), - ), - const SizedBox(height: 2), - Text( - vendor, - style: const TextStyle( - fontSize: 12, - color: UiColors.textSecondary, - ), - ), - const SizedBox(height: 4), - Text( - date, - style: const TextStyle( - fontSize: 11, - color: UiColors.textDescription, - ), - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - amount, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: UiColors.textPrimary, - ), - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(6), - ), + const SizedBox(height: 24), + if (industries.isEmpty) + const Center( + child: Padding( + padding: EdgeInsets.all(16.0), child: Text( - status.toUpperCase(), - style: TextStyle( - color: statusColor, - fontSize: 9, - fontWeight: FontWeight.bold, - ), + 'No industry data available', + style: TextStyle(color: UiColors.textSecondary), ), ), - ], - ), + ) + else + ...industries.map((ind) => Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + ind.name, + style: const TextStyle( + fontSize: 13, + color: UiColors.textSecondary, + ), + ), + Text( + NumberFormat.currency(symbol: r'$', decimalDigits: 0) + .format(ind.amount), + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: UiColors.textPrimary, + ), + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: ind.percentage / 100, + backgroundColor: UiColors.bgSecondary, + color: UiColors.success, + minHeight: 6, + ), + ), + const SizedBox(height: 6), + Text( + '${ind.percentage.toStringAsFixed(1)}% of total', + style: const TextStyle( + fontSize: 10, + color: UiColors.textDescription, + ), + ), + ], + ), + )), ], ), ); diff --git a/backend/dataconnect/connector/reports/queries.gql b/backend/dataconnect/connector/reports/queries.gql index 10bceae5..84238101 100644 --- a/backend/dataconnect/connector/reports/queries.gql +++ b/backend/dataconnect/connector/reports/queries.gql @@ -281,7 +281,7 @@ query listInvoicesForSpendByBusiness( status invoiceNumber - vendor { id companyName } + vendor { id companyName serviceSpecialty } business { id businessName } order { id eventName } } @@ -306,7 +306,7 @@ query listInvoicesForSpendByVendor( status invoiceNumber - vendor { id companyName } + vendor { id companyName serviceSpecialty } business { id businessName } order { id eventName } } @@ -332,7 +332,7 @@ query listInvoicesForSpendByOrder( status invoiceNumber - vendor { id companyName } + vendor { id companyName serviceSpecialty } business { id businessName } order { id eventName } }