added mytext

This commit is contained in:
Vaibhav Surve 2025-12-05 10:53:36 +05:30
parent 717f0c92af
commit 1717cd5e2b
2 changed files with 248 additions and 304 deletions

View File

@ -1,6 +1,5 @@
// collections_health_widget.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
// --- MAIN WIDGET FILE --- // --- MAIN WIDGET FILE ---
class CollectionsHealthWidget extends StatelessWidget { class CollectionsHealthWidget extends StatelessWidget {
@ -10,6 +9,7 @@ class CollectionsHealthWidget extends StatelessWidget {
const double totalDue = 34190.0; const double totalDue = 34190.0;
const double totalCollected = 5000.0; const double totalCollected = 5000.0;
const double totalValue = totalDue + totalCollected; const double totalValue = totalDue + totalCollected;
// Calculate Pending Percentage for Gauge // Calculate Pending Percentage for Gauge
final double pendingPercentage = final double pendingPercentage =
totalValue > 0 ? totalDue / totalValue : 0.0; totalValue > 0 ? totalDue / totalValue : 0.0;
@ -17,12 +17,10 @@ class CollectionsHealthWidget extends StatelessWidget {
// 1. MAIN CARD CONTAINER (White Theme) // 1. MAIN CARD CONTAINER (White Theme)
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
// Main card background color: WHITE
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
// Lighter shadow for white background
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(0.1),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 5), offset: const Offset(0, 5),
@ -60,7 +58,7 @@ class CollectionsHealthWidget extends StatelessWidget {
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// 3. NEW: AGING ANALYSIS SECTION // 3. AGING ANALYSIS SECTION
_buildAgingAnalysis(), _buildAgingAnalysis(),
], ],
), ),
@ -69,19 +67,24 @@ class CollectionsHealthWidget extends StatelessWidget {
// --- HELPER METHOD 1: HEADER --- // --- HELPER METHOD 1: HEADER ---
Widget _buildHeader() { Widget _buildHeader() {
return Row( return Row(mainAxisAlignment: MainAxisAlignment.start, children: [
children: const <Widget>[ Expanded(
Text( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(
'Collections Health Overview', 'Collections Health Overview',
style: TextStyle( fontWeight: 700,
// Text color: Dark for contrast on white background
color: Colors.black87,
fontSize: 18,
fontWeight: FontWeight.bold,
), ),
const SizedBox(height: 2),
MyText.bodySmall(
'View your collection health data.',
color: Colors.grey,
), ),
], ],
); ),
),
]);
} }
// --- HELPER METHOD 2: LEFT SECTION (CHARTS) --- // --- HELPER METHOD 2: LEFT SECTION (CHARTS) ---
@ -97,38 +100,33 @@ class CollectionsHealthWidget extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
// Top: Gauge Chart and Due Amount // Top: Gauge Chart
Row( Row(
children: <Widget>[ children: <Widget>[
// Use White-Theme Gauge Placeholder with calculated percentage
_GaugeChartPlaceholder( _GaugeChartPlaceholder(
backgroundColor: Colors.white, backgroundColor: Colors.white,
pendingPercentage: pendingPercentage), pendingPercentage: pendingPercentage,
),
const SizedBox(width: 12), const SizedBox(width: 12),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Total Due + Summary
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( MyText.bodyLarge(
'${totalDue.toStringAsFixed(0)} DUE', '${totalDue.toStringAsFixed(0)} DUE',
style: const TextStyle( fontWeight: 700,
// Text color: Dark for contrast
color: Colors.black87,
fontSize: 22,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( MyText.bodySmall(
'• Pending ($pendingPercentStr%) • ₹${totalCollected.toStringAsFixed(0)} Collected', '• Pending ($pendingPercentStr%) • ₹${totalCollected.toStringAsFixed(0)} Collected',
// Sub-text color: Lighter dark color color: Colors.black54,
style: const TextStyle(color: Colors.black54, fontSize: 12),
), ),
], ],
), ),
@ -136,6 +134,7 @@ class CollectionsHealthWidget extends StatelessWidget {
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Bottom: Timeline Charts (Trend Analysis) // Bottom: Timeline Charts (Trend Analysis)
Row( Row(
children: <Widget>[ children: <Widget>[
@ -144,34 +143,40 @@ class CollectionsHealthWidget extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const Text('Expected Collections Trend', MyText.bodySmall(
// Text color: Dark for contrast 'Expected Collections Trend',
style: TextStyle(color: Colors.black87, fontSize: 12)), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_TimelineChartPlaceholder( const _TimelineChartPlaceholder(
isBar: true, isBar: true,
barColor: const Color(0xFF2196F3)), // Blue Bar Chart barColor: Color(0xFF2196F3),
const Text('Week 16 Nov 2025', ),
style: TextStyle(color: Colors.black54, fontSize: 10)), MyText.bodySmall(
'Week 16 Nov 2025',
color: Colors.black54,
),
], ],
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
// Collection Rate Trend (Area Chart Placeholder for Analysis)
// Collection Rate Trend (Area Chart Placeholder)
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const Text('Collection Rate Trend', MyText.bodySmall(
// Text color: Dark for contrast 'Collection Rate Trend',
style: TextStyle(color: Colors.black87, fontSize: 12)), ),
const SizedBox(height: 8), const SizedBox(height: 8),
_TimelineChartPlaceholder( const _TimelineChartPlaceholder(
isBar: false, isBar: false,
areaColor: areaColor: Color(0xFF4CAF50),
const Color(0xFF4CAF50)), // Green Area Chart (Rate) ),
const Text('Week 14 Nov 2025', MyText.bodySmall(
style: TextStyle(color: Colors.black54, fontSize: 10)), 'Week 14 Nov 2025',
color: Colors.black54,
),
], ],
), ),
), ),
@ -188,7 +193,7 @@ class CollectionsHealthWidget extends StatelessWidget {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
// Metric Card 1: Top Client (From Invoice 1) // Metric Card 1: Top Client
_buildMetricCard( _buildMetricCard(
title: 'Top Client Balance', title: 'Top Client Balance',
value: 'Peninsula Land Limited', value: 'Peninsula Land Limited',
@ -197,7 +202,8 @@ class CollectionsHealthWidget extends StatelessWidget {
isDetailed: true, isDetailed: true,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Metric Card 2 (NEW): Total Collected (From Invoice 2)
// Metric Card 2: Total Collected (YTD)
_buildMetricCard( _buildMetricCard(
title: 'Total Collected (YTD)', title: 'Total Collected (YTD)',
value: '${totalCollected.toStringAsFixed(0)}', value: '${totalCollected.toStringAsFixed(0)}',
@ -206,7 +212,8 @@ class CollectionsHealthWidget extends StatelessWidget {
isDetailed: false, isDetailed: false,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Metric Card 3: Days Sales Outstanding (DSO) (Analytical Placeholder)
// Metric Card 3: DSO
_buildMetricCard( _buildMetricCard(
title: 'Days Sales Outstanding (DSO)', title: 'Days Sales Outstanding (DSO)',
value: '45 Days', value: '45 Days',
@ -215,7 +222,8 @@ class CollectionsHealthWidget extends StatelessWidget {
isDetailed: false, isDetailed: false,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Metric Card 4: Last Metric Card in the image (Repeating rate for layout)
// Metric Card 4: Bad Debt Ratio
_buildMetricCard( _buildMetricCard(
title: 'Bad Debt Ratio', title: 'Bad Debt Ratio',
value: '0.8%', value: '0.8%',
@ -227,7 +235,7 @@ class CollectionsHealthWidget extends StatelessWidget {
); );
} }
// --- HELPER METHOD 4: METRIC CARD WIDGET (Unchanged) --- // --- HELPER METHOD 4: METRIC CARD WIDGET ---
Widget _buildMetricCard({ Widget _buildMetricCard({
required String title, required String title,
required String value, required String value,
@ -238,60 +246,39 @@ class CollectionsHealthWidget extends StatelessWidget {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
// Light grey background for card separation on white theme
color: const Color(0xFFF5F5F5), color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( MyText.bodySmall(
title, title,
// Title text color: Soft dark grey color: Colors.black54,
style: const TextStyle(color: Colors.black54, fontSize: 10),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
// Conditional rendering for the detailed vs simple card structure
if (isDetailed) ...[ if (isDetailed) ...[
Text( MyText.bodySmall(
value, // Client Name value,
style: const TextStyle( fontWeight: 600,
// Value text color: Dark
color: Colors.black87,
fontSize: 12,
fontWeight: FontWeight.w600,
), ),
), MyText.bodyMedium(
Text( subValue,
subValue, // Due Amount
style: TextStyle(
// Color remains dynamic (Red/Green/etc.)
color: valueColor, color: valueColor,
fontSize: 16, fontWeight: 700,
fontWeight: FontWeight.bold,
),
), ),
] else ] else
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( MyText.bodySmall(
value, value,
style: const TextStyle( fontWeight: 600,
// Value text color: Dark
color: Colors.black87,
fontSize: 14,
fontWeight: FontWeight.w600,
), ),
), MyText.bodySmall(
Text(
subValue, subValue,
style: TextStyle(
// Color remains dynamic (Red/Green/etc.)
color: valueColor, color: valueColor,
fontSize: 14, fontWeight: 600,
fontWeight: FontWeight.w600,
),
), ),
], ],
), ),
@ -301,9 +288,8 @@ class CollectionsHealthWidget extends StatelessWidget {
} }
// --- NEW HELPER METHOD: AGING ANALYSIS --- // --- NEW HELPER METHOD: AGING ANALYSIS ---
// --- NEW HELPER METHOD: AGING ANALYSIS (MODIFIED) ---
Widget _buildAgingAnalysis() { Widget _buildAgingAnalysis() {
// Hardcoded data based on the JSON analysis // Hardcoded data
const double due0to20Days = 0.0; const double due0to20Days = 0.0;
const double due20to45Days = 34190.0; const double due20to45Days = 34190.0;
const double due45to90Days = 0.0; const double due45to90Days = 0.0;
@ -314,30 +300,38 @@ class CollectionsHealthWidget extends StatelessWidget {
// Define buckets with their risk color // Define buckets with their risk color
final List<AgingBucketData> buckets = [ final List<AgingBucketData> buckets = [
AgingBucketData('0-20 Days', due0to20Days, AgingBucketData(
const Color(0xFF4CAF50)), // Green (Low Risk) '0-20 Days',
AgingBucketData('20-45 Days', due20to45Days, due0to20Days,
const Color(0xFFFF9800)), // Orange (Medium Risk) const Color(0xFF4CAF50), // Green (Low Risk)
AgingBucketData('45-90 Days', due45to90Days, ),
const Color(0xFFF44336).withOpacity(0.7)), // Light Red (High Risk) AgingBucketData(
AgingBucketData('> 90 Days', dueOver90Days, '20-45 Days',
const Color(0xFFF44336)), // Dark Red (Very High Risk) due20to45Days,
const Color(0xFFFF9800), // Orange (Medium Risk)
),
AgingBucketData(
'45-90 Days',
due45to90Days,
const Color(0xFFF44336).withOpacity(0.7), // Light Red
),
AgingBucketData(
'> 90 Days',
dueOver90Days,
const Color(0xFFF44336), // Dark Red
),
]; ];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const Text( MyText.bodyMedium(
'Outstanding Collections Aging Analysis', 'Outstanding Collections Aging Analysis',
style: TextStyle( fontWeight: 700,
color: Colors.black87,
fontSize: 14,
fontWeight: FontWeight.bold,
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// NEW: STACKED BAR VISUALIZATION // Stacked bar visualization
_AgingStackedBar( _AgingStackedBar(
buckets: buckets, buckets: buckets,
totalOutstanding: totalOutstanding, totalOutstanding: totalOutstanding,
@ -345,21 +339,25 @@ class CollectionsHealthWidget extends StatelessWidget {
const SizedBox(height: 15), const SizedBox(height: 15),
// NEW: LEGEND/BUCKET DETAILS (Replaces the old _buildAgingBucket layout) // Legend / Bucket details
Wrap( Wrap(
spacing: 12, spacing: 12,
runSpacing: 8, runSpacing: 8,
children: buckets children: buckets
.map((bucket) => _buildAgingLegendItem( .map(
bucket.title, bucket.amount, bucket.color)) (bucket) => _buildAgingLegendItem(
bucket.title,
bucket.amount,
bucket.color,
),
)
.toList(), .toList(),
), ),
], ],
); );
} }
// Keep the old _buildAgingBucket helper and rename it to a legend item // Legend item for aging buckets
// This is more efficient than recreating the logic
Widget _buildAgingLegendItem(String title, double amount, Color color) { Widget _buildAgingLegendItem(String title, double amount, Color color) {
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -373,24 +371,25 @@ class CollectionsHealthWidget extends StatelessWidget {
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( MyText.bodySmall(
'${title}: ₹${amount.toStringAsFixed(0)}', '$title: ₹${amount.toStringAsFixed(0)}',
style: const TextStyle(color: Colors.black87, fontSize: 12),
), ),
], ],
); );
} }
} }
// --- CUSTOM PAINTERS / PLACEHOLDERS (Modified to accept percentage) --- // --- CUSTOM PAINTERS / PLACEHOLDERS ---
// Placeholder for the Semi-Circle Gauge Chart // Placeholder for the Semi-Circle Gauge Chart
class _GaugeChartPlaceholder extends StatelessWidget { class _GaugeChartPlaceholder extends StatelessWidget {
final Color backgroundColor; final Color backgroundColor;
final double pendingPercentage; final double pendingPercentage;
const _GaugeChartPlaceholder( const _GaugeChartPlaceholder({
{required this.backgroundColor, required this.pendingPercentage}); required this.backgroundColor,
required this.pendingPercentage,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -402,37 +401,22 @@ class _GaugeChartPlaceholder extends StatelessWidget {
// BACKGROUND GAUGE // BACKGROUND GAUGE
CustomPaint( CustomPaint(
size: const Size(120, 70), size: const Size(120, 70),
// Pass the pending percentage to the painter
painter: _SemiCirclePainter( painter: _SemiCirclePainter(
canvasColor: backgroundColor, canvasColor: backgroundColor,
pendingPercentage: pendingPercentage), pendingPercentage: pendingPercentage,
),
), ),
// CENTER CIRCLE // CENTER TEXT
Positioned( Align(
bottom: 0, alignment: Alignment.bottomCenter,
left: 30, child: Padding(
child: Container( padding: const EdgeInsets.only(bottom: 8),
height: 60, child: FittedBox(
width: 60, child: MyText.bodySmall(
decoration: BoxDecoration(
// Center circle background is the same as the main card background (white)
color: backgroundColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.black.withOpacity(0.1)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
// Icon color: Dark
Icon(Icons.work, color: Colors.black87, size: 20),
SizedBox(height: 2),
Text(
'RISK LEVEL', 'RISK LEVEL',
// Text color: Soft dark grey fontWeight: 600,
style: TextStyle(color: Colors.black54, fontSize: 8),
), ),
],
), ),
), ),
), ),
@ -442,75 +426,82 @@ class _GaugeChartPlaceholder extends StatelessWidget {
} }
} }
// Painter for the semi-circular gauge chart visualization (Simplified) // Painter for the semi-circular gauge chart visualization
class _SemiCirclePainter extends CustomPainter { class _SemiCirclePainter extends CustomPainter {
final Color canvasColor; final Color canvasColor;
final double pendingPercentage; final double pendingPercentage;
_SemiCirclePainter( _SemiCirclePainter({
{required this.canvasColor, required this.pendingPercentage}); required this.canvasColor,
required this.pendingPercentage,
});
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final rect = Rect.fromCircle( final rect = Rect.fromCircle(
center: Offset(size.width / 2, size.height), radius: size.width / 2); center: Offset(size.width / 2, size.height),
radius: size.width / 2,
);
// Radians for a semi-circle
const double totalArc = 3.14159; const double totalArc = 3.14159;
// Calculate the sweep angles
final double pendingSweepAngle = totalArc * pendingPercentage; final double pendingSweepAngle = totalArc * pendingPercentage;
final double collectedSweepAngle = totalArc * (1.0 - pendingPercentage); final double collectedSweepAngle = totalArc * (1.0 - pendingPercentage);
// Background Arc (Full semi-circle) // Background Arc
final backgroundPaint = Paint() final backgroundPaint = Paint()
// Background arc is a soft grey on a white theme
..color = Colors.black.withOpacity(0.1) ..color = Colors.black.withOpacity(0.1)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 10; ..strokeWidth = 10;
canvas.drawArc(rect, totalArc, totalArc, false, canvas.drawArc(rect, totalArc, totalArc, false, backgroundPaint);
backgroundPaint); // Start at 180 deg (Pi)
// Pending Arc (Red/Orange segment) // Pending Arc
final pendingPaint = Paint() final pendingPaint = Paint()
..color = const Color(0xFFF44336) // Red
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 10 ..strokeWidth = 10
..shader = const LinearGradient(colors: [ ..shader = const LinearGradient(
Color(0xFFFF9800), // Orange colors: [
Color(0xFFF44336), // Red Color(0xFFFF9800),
]).createShader(rect); Color(0xFFF44336),
// Start angle: 3.14 (180 deg) ],
).createShader(rect);
canvas.drawArc(rect, totalArc, pendingSweepAngle, false, pendingPaint); canvas.drawArc(rect, totalArc, pendingSweepAngle, false, pendingPaint);
// Collected Arc (Green segment) // Collected Arc
final collectedPaint = Paint() final collectedPaint = Paint()
..color = const Color(0xFF4CAF50) // Green ..color = const Color(0xFF4CAF50)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 10; ..strokeWidth = 10;
// Start angle: 3.14 + pendingSweepAngle canvas.drawArc(
canvas.drawArc(rect, totalArc + pendingSweepAngle, collectedSweepAngle, rect,
false, collectedPaint); totalArc + pendingSweepAngle,
collectedSweepAngle,
false,
collectedPaint,
);
} }
@override @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
// Placeholder for the Bar/Area Charts (Unchanged) // Placeholder for the Bar/Area Charts
class _TimelineChartPlaceholder extends StatelessWidget { class _TimelineChartPlaceholder extends StatelessWidget {
final bool isBar; final bool isBar;
final Color? barColor; final Color? barColor;
final Color? areaColor; final Color? areaColor;
const _TimelineChartPlaceholder( const _TimelineChartPlaceholder({
{required this.isBar, this.barColor, this.areaColor}); required this.isBar,
this.barColor,
this.areaColor,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 50, height: 50,
width: double.infinity, width: double.infinity,
color: Colors.transparent, // Transparent container for visual space color: Colors.transparent,
child: isBar child: isBar
? _BarChartVisual(barColor: barColor!) ? _BarChartVisual(barColor: barColor!)
: _AreaChartVisual(areaColor: areaColor!), : _AreaChartVisual(areaColor: areaColor!),
@ -528,12 +519,12 @@ class _BarChartVisual extends StatelessWidget {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: const [
_Bar(0.4, barColor), _Bar(0.4),
_Bar(0.7, barColor), _Bar(0.7),
_Bar(1.0, barColor), _Bar(1.0),
_Bar(0.6, barColor), _Bar(0.6),
_Bar(0.8, barColor), _Bar(0.8),
], ],
); );
} }
@ -541,17 +532,18 @@ class _BarChartVisual extends StatelessWidget {
class _Bar extends StatelessWidget { class _Bar extends StatelessWidget {
final double heightFactor; final double heightFactor;
final Color color;
const _Bar(this.heightFactor, this.color); const _Bar(this.heightFactor);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Bar color is taken from parent via DefaultTextStyle/Theme if needed;
// you can wrap with Theme if you want dynamic colors.
return Container( return Container(
width: 8, width: 8,
height: 50 * heightFactor, height: 50 * heightFactor,
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: Colors.grey.shade400,
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
); );
@ -587,7 +579,6 @@ class _AreaChartPainter extends CustomPainter {
Offset(size.width, size.height * 0.4), Offset(size.width, size.height * 0.4),
]; ];
// Path for the area
final path = Path() final path = Path()
..moveTo(points.first.dx, size.height) ..moveTo(points.first.dx, size.height)
..lineTo(points.first.dx, points.first.dy); ..lineTo(points.first.dx, points.first.dy);
@ -597,20 +588,18 @@ class _AreaChartPainter extends CustomPainter {
path.lineTo(points.last.dx, size.height); path.lineTo(points.last.dx, size.height);
path.close(); path.close();
// Paint for the gradient fill
final areaPaint = Paint() final areaPaint = Paint()
..shader = LinearGradient( ..shader = LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
areaColor.withOpacity(0.5), // Lighter opacity on white theme areaColor.withOpacity(0.5),
areaColor.withOpacity(0.0), // Transparent areaColor.withOpacity(0.0),
], ],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height)) ).createShader(Rect.fromLTWH(0, 0, size.width, size.height))
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
canvas.drawPath(path, areaPaint); canvas.drawPath(path, areaPaint);
// Paint for the line stroke
final linePaint = Paint() final linePaint = Paint()
..color = areaColor ..color = areaColor
..strokeWidth = 2 ..strokeWidth = 2
@ -622,7 +611,7 @@ class _AreaChartPainter extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
} }
// --- NEW DATA MODEL --- // --- DATA MODEL ---
class AgingBucketData { class AgingBucketData {
final String title; final String title;
final double amount; final double amount;
@ -631,7 +620,7 @@ class AgingBucketData {
AgingBucketData(this.title, this.amount, this.color); AgingBucketData(this.title, this.amount, this.color);
} }
// --- NEW HELPER WIDGET: STACKED BAR VISUAL --- // --- STACKED BAR VISUAL ---
class _AgingStackedBar extends StatelessWidget { class _AgingStackedBar extends StatelessWidget {
final List<AgingBucketData> buckets; final List<AgingBucketData> buckets;
final double totalOutstanding; final double totalOutstanding;
@ -650,21 +639,20 @@ class _AgingStackedBar extends StatelessWidget {
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Center( child: Center(
child: Text( child: MyText.bodySmall(
'No Outstanding Collections', 'No Outstanding Collections',
style: TextStyle(color: Colors.black54, fontSize: 10), color: Colors.black54,
), ),
), ),
); );
} }
// Build the segments for the Row
final List<Widget> segments = final List<Widget> segments =
buckets.where((b) => b.amount > 0).map((bucket) { buckets.where((b) => b.amount > 0).map((bucket) {
final double flexValue = bucket.amount / totalOutstanding; final double flexValue = bucket.amount / totalOutstanding;
return Expanded( return Expanded(
flex: (flexValue * 100).toInt(), // Use a scaled integer for flex flex: (flexValue * 100).toInt(),
child: Container( child: Container(
height: 16, height: 16,
color: bucket.color, color: bucket.color,

View File

@ -1,6 +1,8 @@
// lib/widgets/purchase_invoice_dashboard.dart // lib/widgets/purchase_invoice_dashboard.dart
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:on_field_work/helpers/widgets/my_text.dart';
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
/// ======================= /// =======================
/// INTERNAL DUMMY DATA /// INTERNAL DUMMY DATA
@ -111,7 +113,7 @@ class CompactPurchaseInvoiceDashboard extends StatelessWidget {
padding: const EdgeInsets.all(mainPadding), padding: const EdgeInsets.all(mainPadding),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(10), // Slightly rounder corners borderRadius: BorderRadius.circular(5), // Slightly rounder corners
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.08), // Softer, more subtle shadow color: Colors.black.withOpacity(0.08), // Softer, more subtle shadow
@ -420,14 +422,11 @@ class _SectionTitle extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Text( return MyText.bodySmall(
title, title,
style: TextStyle(
color: Colors.grey.shade700, color: Colors.grey.shade700,
fontSize: 14, fontWeight: 700,
fontWeight: FontWeight.bold,
letterSpacing: 0.5, letterSpacing: 0.5,
),
); );
} }
} }
@ -437,14 +436,21 @@ class _DashboardHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Text( return Row(mainAxisAlignment: MainAxisAlignment.start, children: [
'Purchase Invoice Dashboard ', Expanded(
style: TextStyle( child:
color: Colors.black, Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
fontSize: 20, MyText.bodyMedium(
fontWeight: FontWeight.w700, 'Purchase Invoice ',
fontWeight: 700,
), ),
); SizedBox(height: 2),
MyText.bodySmall(
'View your purchase invoice data.',
color: Colors.grey,
),
]))
]);
} }
} }
@ -464,7 +470,7 @@ class _TotalValueCard extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE3F2FD), // Lighter Blue color: const Color(0xFFE3F2FD), // Lighter Blue
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(5),
border: Border.all(color: const Color(0xFFBBDEFB), width: 1), border: Border.all(color: const Color(0xFFBBDEFB), width: 1),
), ),
child: Column( child: Column(
@ -473,15 +479,12 @@ class _TotalValueCard extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( MyText.bodySmall(
'TOTAL PROFORMA VALUE (₹)', 'TOTAL PROFORMA VALUE (₹)',
style: TextStyle(
color: Colors.blue.shade800, color: Colors.blue.shade800,
fontSize: 10, fontWeight: 700,
fontWeight: FontWeight.w700,
letterSpacing: 1.0, letterSpacing: 1.0,
), ),
),
Icon( Icon(
Icons.account_balance_wallet_outlined, Icons.account_balance_wallet_outlined,
color: Colors.blue.shade700, color: Colors.blue.shade700,
@ -489,24 +492,15 @@ class _TotalValueCard extends StatelessWidget {
), ),
], ],
), ),
const SizedBox(height: 8), MySpacing.height(8),
Text( MyText.bodyMedium(
// Format number with commas if needed for large values
totalProformaAmount.toStringAsFixed(0), totalProformaAmount.toStringAsFixed(0),
style: const TextStyle(
color: Colors.black,
fontSize: 32,
fontWeight: FontWeight.w900,
), ),
), MySpacing.height(4),
const SizedBox(height: 4), MyText.bodySmall(
Text(
'Over $totalCount Total Invoices', 'Over $totalCount Total Invoices',
style: TextStyle(
color: Colors.blueGrey.shade600, color: Colors.blueGrey.shade600,
fontSize: 12, fontWeight: 500,
fontWeight: FontWeight.w500,
),
), ),
], ],
), ),
@ -589,7 +583,7 @@ class _CondensedMetricCard extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withOpacity(0.05), color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(5),
border: Border.all(color: color.withOpacity(0.15), width: 1), border: Border.all(color: color.withOpacity(0.15), width: 1),
), ),
child: Column( child: Column(
@ -600,35 +594,25 @@ class _CondensedMetricCard extends StatelessWidget {
Icon(icon, color: color, size: 16), Icon(icon, color: color, size: 16),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: MyText.bodySmall(
title, title,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(
color: color, color: color,
fontSize: 10, fontWeight: 700,
fontWeight: FontWeight.w700,
),
), ),
), ),
], ],
), ),
const SizedBox(height: 6), MySpacing.height(6),
Text( MyText.bodyMedium(
value, value,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( fontWeight: 800,
color: Colors.black87,
fontSize: 16,
fontWeight: FontWeight.w800,
), ),
), MyText.bodySmall(
Text(
caption, caption,
style: TextStyle(
color: Colors.grey.shade500, color: Colors.grey.shade500,
fontSize: 9, fontWeight: 500,
fontWeight: FontWeight.w500,
),
), ),
], ],
), ),
@ -656,9 +640,9 @@ class _StatusDonutChart extends StatelessWidget {
if (activeBuckets.isEmpty) { if (activeBuckets.isEmpty) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: MyText.bodySmall(
'No active invoices to display status breakdown.', 'No active invoices to display status breakdown.',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500), color: Colors.grey.shade500,
), ),
); );
} }
@ -667,10 +651,6 @@ class _StatusDonutChart extends StatelessWidget {
final double mainPercentage = final double mainPercentage =
totalAmount > 0 ? activeBuckets.first.amount / totalAmount : 0.0; totalAmount > 0 ? activeBuckets.first.amount / totalAmount : 0.0;
// Placeholder for Donut Chart (requires external package like fl_chart for true pie/donut chart)
// We simulate the key visual hierarchy here:
//
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -688,21 +668,13 @@ class _StatusDonutChart extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( MyText.bodySmall(
'${activeBuckets.first.title}', // Top status name activeBuckets.first.title,
style: TextStyle(
fontSize: 10,
color: activeBuckets.first.color, color: activeBuckets.first.color,
fontWeight: FontWeight.bold, fontWeight: 700,
), ),
), MyText.bodyMedium(
Text(
'${(mainPercentage * 100).toStringAsFixed(0)}%', '${(mainPercentage * 100).toStringAsFixed(0)}%',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: Colors.black87,
),
), ),
], ],
), ),
@ -727,23 +699,17 @@ class _StatusDonutChart extends StatelessWidget {
), ),
), ),
Expanded( Expanded(
child: Text( child: MyText.bodySmall(
'${bucket.title} (${bucket.count})', '${bucket.title} (${bucket.count})',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade800, color: Colors.grey.shade800,
fontWeight: FontWeight.w500, fontWeight: 500,
), ),
), ),
), MyText.bodySmall(
Text(
'${bucket.amount.toStringAsFixed(0)}', '${bucket.amount.toStringAsFixed(0)}',
style: TextStyle( fontWeight: 700,
fontSize: 12,
fontWeight: FontWeight.bold,
color: bucket.color.withOpacity(0.9), color: bucket.color.withOpacity(0.9),
), ),
),
], ],
), ),
); );
@ -772,9 +738,9 @@ class _ProjectBreakdown extends StatelessWidget {
if (projects.isEmpty) { if (projects.isEmpty) {
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: MyText.bodySmall(
'No project data available.', 'No project data available.',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500), color: Colors.grey.shade500,
), ),
); );
} }
@ -804,18 +770,14 @@ class _ProjectBreakdown extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( MyText.bodyMedium(
project.name, project.name,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( fontWeight: 600,
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(5),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: percentage, value: percentage,
backgroundColor: Colors.grey.shade200, backgroundColor: Colors.grey.shade200,
@ -830,22 +792,16 @@ class _ProjectBreakdown extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( MyText.bodyMedium(
'${project.amount.toStringAsFixed(0)}', '${project.amount.toStringAsFixed(0)}',
style: TextStyle( fontWeight: 700,
fontSize: 13,
fontWeight: FontWeight.w700,
color: color.withOpacity(0.9), color: color.withOpacity(0.9),
), ),
), MyText.bodySmall(
Text(
'$percentageText%', '$percentageText%',
style: TextStyle( fontWeight: 500,
fontSize: 10,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600, color: Colors.grey.shade600,
), ),
),
], ],
), ),
], ],