2242 lines
80 KiB
Dart
2242 lines
80 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_card.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
|
import 'package:on_field_work/helpers/utils/my_shadow.dart';
|
|
|
|
class SkeletonLoaders {
|
|
static Widget buildLoadingSkeleton() {
|
|
return ShimmerEffect(
|
|
child: SizedBox(
|
|
height: 360,
|
|
child: Column(
|
|
children: List.generate(5, (index) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: List.generate(6, (i) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
|
width: 48,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget serviceProjectListSkeletonLoader() {
|
|
// --- Start: Configuration to match live UI ---
|
|
// Live UI uses ListView.separated with:
|
|
// - padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 120)
|
|
// - separatorBuilder: MySpacing.height(12)
|
|
// - _buildProjectCard uses Card(margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4))
|
|
|
|
// To combine:
|
|
// Horizontal padding: 8 (ListView) + 6 (Card margin) = 14 on each side.
|
|
// Top/Bottom separation: 4 (ListView padding) + 4 (Card margin) = 8
|
|
// Separator space: 4 (Card margin) + 12 (Separator) + 4 (Card margin) = 20 total space between cards.
|
|
|
|
// New ListView.separated padding to compensate for inner Card margins
|
|
const EdgeInsets listPadding =
|
|
const EdgeInsets.fromLTRB(14, 8, 14, 120 + 4); // 8(L/R) + 6(Card L/R Margin) = 14
|
|
// New separator to match the 12 + 4 * 2 = 20 gap.
|
|
const Widget cardSeparator = const SizedBox(height: 12);
|
|
const EdgeInsets cardMargin = EdgeInsets.zero; // Margin is now controlled by the ListView.separated padding
|
|
|
|
// Internal Card padding matches the live card
|
|
const EdgeInsets cardInnerPadding =
|
|
const EdgeInsets.symmetric(horizontal: 18, vertical: 14);
|
|
// --- End: Configuration to match live UI ---
|
|
|
|
return ListView.separated(
|
|
padding: listPadding, // Use calculated padding
|
|
physics:
|
|
const NeverScrollableScrollPhysics(),
|
|
itemCount: 4,
|
|
separatorBuilder: (_, __) => cardSeparator, // Use calculated separator
|
|
itemBuilder: (context, index) {
|
|
return Card(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
|
margin: cardMargin, // Set margin to zero, handled by ListView padding
|
|
shadowColor: Colors.indigo.withOpacity(0.10),
|
|
color: Colors.white,
|
|
child: ShimmerEffect(
|
|
child: Padding(
|
|
padding: cardInnerPadding, // Use live card's inner padding
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 1. Title and Status Row
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Project Name Placeholder
|
|
Container(
|
|
height: 18, // Matches MyText.titleMedium height approx
|
|
width: 150,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
// Status Chip Placeholder
|
|
Container(
|
|
height: 18, // Matches status chip height approx
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
// MySpacing.height(10) in live UI is the key spacing here
|
|
// Note: The live UI has MySpacing.height(4) after the title
|
|
// and then MySpacing.height(10) before the first detail row,
|
|
// so the total space is 4 + 10 = 14.
|
|
MySpacing.height(14),
|
|
|
|
// 2. Detail Rows (Date, Client, Contact)
|
|
// Assigned Date Row
|
|
_buildDetailRowSkeleton(
|
|
width: 200, iconColor: Colors.teal.shade300),
|
|
MySpacing.height(8),
|
|
|
|
// Client Row
|
|
_buildDetailRowSkeleton(
|
|
width: 240, iconColor: Colors.indigo.shade300),
|
|
MySpacing.height(8),
|
|
|
|
// Contact Row
|
|
_buildDetailRowSkeleton(
|
|
width: 220, iconColor: Colors.green.shade300),
|
|
MySpacing.height(12), // MySpacing.height(12) before Wrap
|
|
|
|
// 3. Service Chips Wrap
|
|
Wrap(
|
|
spacing: 6,
|
|
runSpacing: 4,
|
|
children: List.generate(
|
|
3,
|
|
(chipIndex) => Container(
|
|
height: 20,
|
|
width:
|
|
70 + (chipIndex * 10).toDouble(), // Varied widths
|
|
decoration: BoxDecoration(
|
|
color: Colors
|
|
.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Helper to build a skeleton row for details
|
|
static Widget _buildDetailRowSkeleton({
|
|
required double width,
|
|
required Color iconColor,
|
|
}) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// Icon Placeholder (size 18 matches live UI)
|
|
Icon(Icons.circle, size: 18, color: iconColor),
|
|
MySpacing.width(8),
|
|
// Text Placeholder (height 13 approx for font size 13)
|
|
Container(
|
|
height: 14,
|
|
width: width,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
static Widget attendanceQuickCardSkeleton() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(5),
|
|
// ... gradient color setup (using grey for shimmer)
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Row with avatar and texts
|
|
Row(
|
|
children: [
|
|
// Avatar (Size 30)
|
|
Container(
|
|
width: 30,
|
|
height: 30,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade400, shape: BoxShape.circle)),
|
|
MySpacing.width(10),
|
|
// Name + designation (Approximate heights for MyText.titleSmall and MyText.labelSmall)
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12, width: 100, color: Colors.grey.shade400),
|
|
MySpacing.height(
|
|
4), // Reduced from 6, guessing labelSmall is shorter
|
|
Container(
|
|
height: 10, width: 70, color: Colors.grey.shade400),
|
|
],
|
|
),
|
|
),
|
|
// Status (MyText.bodySmall, height approx 12-14)
|
|
Container(
|
|
height: 14,
|
|
width: 80,
|
|
color: Colors
|
|
.grey.shade400), // Adjusted width and height slightly
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Description (2 lines of Text, font size 13)
|
|
Container(
|
|
height: 14,
|
|
width: double.infinity,
|
|
color: Colors.grey
|
|
.shade400), // Height for one line of text size 13 + padding
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 14,
|
|
width: double.infinity * 0.7,
|
|
color: Colors.grey.shade400), // Shorter second line
|
|
const SizedBox(height: 12),
|
|
// Action buttons (Row at the end)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
// Check In/Out Button (Approx height 28)
|
|
Container(
|
|
height: 32,
|
|
width: 100,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade400,
|
|
borderRadius:
|
|
BorderRadius.circular(6))), // Larger button size
|
|
MySpacing.width(8),
|
|
// Log View Button (Icon Button, approx size 28-32)
|
|
Container(
|
|
height: 32,
|
|
width: 32,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade400, shape: BoxShape.circle)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget dashboardCardsSkeleton({double? maxWidth}) {
|
|
return LayoutBuilder(builder: (context, constraints) {
|
|
double width = maxWidth ?? constraints.maxWidth;
|
|
double crossAxisSpacing = 15;
|
|
int crossAxisCount = 3;
|
|
|
|
// Calculation remains the same: screen_width - (spacing * (count - 1)) / count
|
|
double totalHorizontalSpace =
|
|
width - (crossAxisSpacing * (crossAxisCount - 1));
|
|
double cardWidth = totalHorizontalSpace / crossAxisCount;
|
|
|
|
// Dynamic height calculation: width / 1.8 (e.g., 92.0 / 1.8 = 51.11, not 46.7)
|
|
// Rerunning the calculation based on the constraint h=46.7 given in the error:
|
|
// If cardWidth = 92.0, the aspect ratio must be different, or the parent widget
|
|
// is forcing a smaller height. To fix the overflow, we must assume the target
|
|
// height is fixed by the aspect ratio and reduce the inner content size.
|
|
double cardHeight = cardWidth / 1.8;
|
|
|
|
// Inner available vertical space (cardHeight - 2 * paddingAll):
|
|
// If cardHeight is 51.11, inner space is 51.11 - 8 = 43.11.
|
|
// If cardHeight is 46.7 (as per error constraint), inner space is 46.7 - 8 = 38.7.
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Skeleton for the "Modules" title (fontSize 16, fontWeight 700)
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 4, bottom: 8),
|
|
height: 18,
|
|
width: 80,
|
|
color: Colors.grey.shade300),
|
|
GridView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: crossAxisCount,
|
|
crossAxisSpacing: crossAxisSpacing,
|
|
mainAxisSpacing: 8,
|
|
childAspectRatio: 1.8,
|
|
),
|
|
itemCount: 6,
|
|
itemBuilder: (context, index) {
|
|
return MyCard.bordered(
|
|
width: cardWidth,
|
|
height: cardHeight,
|
|
paddingAll: 4,
|
|
borderRadiusAll: 10,
|
|
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Icon placeholder: Reduced size to 16
|
|
Container(
|
|
width: 16,
|
|
height: 16, // Reduced from 20
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
MySpacing.height(4), // Reduced spacing from 6
|
|
// Text placeholder 1: Reduced height to 8
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
child: Container(
|
|
width: cardWidth * 0.7,
|
|
height: 8, // Reduced from 10
|
|
color: Colors.grey.shade300,
|
|
),
|
|
),
|
|
MySpacing.height(2), // Reduced spacing from 4
|
|
// Text placeholder 2: Reduced height to 8
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
|
child: Container(
|
|
width: cardWidth * 0.5,
|
|
height: 8, // Reduced from 10
|
|
color: Colors.grey.shade300,
|
|
),
|
|
),
|
|
// Total inner height is now 16 + 4 + 8 + 2 + 8 = 38 pixels.
|
|
// This will fit safely within the calculated or constrained height.
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
static Widget projectSelectorSkeleton() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section Title Skeleton
|
|
Container(
|
|
margin: const EdgeInsets.only(left: 4, bottom: 8),
|
|
height: 18, // For _sectionTitle
|
|
width: 80,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
// Selector Card Skeleton
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
border:
|
|
Border.all(color: Colors.grey.shade300), // Placeholder border
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(.04),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Row(
|
|
children: [
|
|
// Icon placeholder
|
|
Container(width: 20, height: 20, color: Colors.grey.shade300),
|
|
const SizedBox(width: 12),
|
|
// Text placeholder
|
|
Expanded(
|
|
child: Container(
|
|
height: 16,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300),
|
|
),
|
|
// Arrow icon placeholder
|
|
Container(width: 26, height: 26, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
static Widget paymentRequestListSkeletonLoader() {
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
|
itemCount: 6,
|
|
separatorBuilder: (_, __) =>
|
|
Divider(color: Colors.grey.shade300, height: 20),
|
|
itemBuilder: (context, index) {
|
|
return ShimmerEffect(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Category name placeholder
|
|
Container(
|
|
height: 14,
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
|
|
// Payee placeholder
|
|
Row(
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 50,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Container(
|
|
height: 12,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
|
|
// Due date and status placeholders
|
|
Row(
|
|
children: [
|
|
// Due date label + value
|
|
Row(
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 50,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
height: 12,
|
|
width: 80,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
// Status chip placeholder
|
|
Container(
|
|
height: 20,
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
static Widget paymentRequestDetailSkeletonLoader() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 30),
|
|
child: Center(
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 520),
|
|
child: MyCard.bordered(
|
|
paddingAll: 16,
|
|
borderRadiusAll: 8,
|
|
shadow: MyShadow(elevation: 3),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header (Created At + Status)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
width: 140,
|
|
height: 16,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
Container(
|
|
width: 80,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(24),
|
|
|
|
// Parties Section
|
|
...List.generate(
|
|
4,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Container(
|
|
height: 14,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
)),
|
|
MySpacing.height(24),
|
|
|
|
// Details Table
|
|
...List.generate(
|
|
6,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Container(
|
|
height: 14,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
)),
|
|
MySpacing.height(24),
|
|
|
|
// Documents Section
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: List.generate(
|
|
3,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Container(
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
)),
|
|
),
|
|
MySpacing.height(24),
|
|
|
|
// Logs / Timeline
|
|
Column(
|
|
children: List.generate(
|
|
3,
|
|
(index) => Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 16,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 14,
|
|
width: 120,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 12,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 12,
|
|
width: 80,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(16),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget employeeDetailSkeletonLoader() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(12, 20, 12, 80),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// Header skeleton (avatar + name + role)
|
|
MyCard(
|
|
borderRadiusAll: 8,
|
|
paddingAll: 16,
|
|
margin: MySpacing.bottom(16),
|
|
shadow: MyShadow(elevation: 2),
|
|
child: ShimmerEffect(
|
|
child: Row(
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 45,
|
|
height: 45,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 14,
|
|
width: 120,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 12,
|
|
width: 80,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 24,
|
|
height: 24,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Sections skeleton
|
|
...List.generate(
|
|
4,
|
|
(_) => Column(
|
|
children: [
|
|
MyCard(
|
|
borderRadiusAll: 8,
|
|
paddingAll: 16,
|
|
margin: MySpacing.bottom(16),
|
|
shadow: MyShadow(elevation: 2),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section header
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.width(8),
|
|
Container(
|
|
width: 120,
|
|
height: 14,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
const Divider(color: Colors.grey),
|
|
|
|
// 2 rows placeholder
|
|
...List.generate(
|
|
2,
|
|
(_) => Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 12),
|
|
child: Row(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.width(16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 100,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(4),
|
|
Container(
|
|
height: 12,
|
|
width: 150,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget dailyProgressPlanningInfraSkeleton() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: List.generate(3, (floorIndex) {
|
|
return MyCard(
|
|
borderRadiusAll: 8,
|
|
paddingAll: 5,
|
|
margin: MySpacing.bottom(10),
|
|
shadow: MyShadow(elevation: 1.5),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Floor header placeholder
|
|
Container(
|
|
height: 14,
|
|
width: 160,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
MySpacing.height(10),
|
|
|
|
// Divider
|
|
Divider(color: Colors.grey.withOpacity(0.3)),
|
|
|
|
// Work areas skeleton
|
|
Column(
|
|
children: List.generate(2, (areaIndex) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 8, bottom: 6),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Area title
|
|
Container(
|
|
height: 12,
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
MySpacing.height(8),
|
|
|
|
// Work items skeleton rows
|
|
Column(
|
|
children: List.generate(2, (itemIndex) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 4),
|
|
child: Row(
|
|
children: [
|
|
// Bullet / icon
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(8),
|
|
// Item text placeholder
|
|
Expanded(
|
|
child: Container(
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius:
|
|
BorderRadius.circular(4),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
static Widget chartSkeletonLoader() {
|
|
return MyCard.bordered(
|
|
paddingAll: 16,
|
|
borderRadiusAll: 12,
|
|
shadow: MyShadow(
|
|
elevation: 1.5,
|
|
position: MyShadowPosition.bottom,
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Chart Header Placeholder
|
|
Container(
|
|
height: 16,
|
|
width: 180,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Donut Skeleton Placeholder
|
|
Center(
|
|
child: Container(
|
|
width: 180,
|
|
height: 180,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.grey.shade300.withOpacity(0.5),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Legend placeholders
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: List.generate(5, (index) {
|
|
return Container(
|
|
width: 100,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget dateSkeletonLoader() {
|
|
return ShimmerEffect(
|
|
child: Container(
|
|
height: 14,
|
|
width: 90,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget expenseByStatusSkeletonLoader() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.05),
|
|
blurRadius: 6,
|
|
spreadRadius: 1,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Title
|
|
Container(
|
|
height: 16,
|
|
width: 160,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// 4 Status Rows
|
|
...List.generate(4, (index) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Row(
|
|
children: [
|
|
// Icon placeholder
|
|
Container(
|
|
height: 44,
|
|
width: 44,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// Title + Amount
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 100,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
height: 12,
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Count + arrow placeholder
|
|
Container(
|
|
height: 12,
|
|
width: 30,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Icon(Icons.chevron_right,
|
|
color: Colors.grey.shade300, size: 24),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
|
|
const SizedBox(height: 16),
|
|
Divider(color: Colors.grey.shade300),
|
|
const SizedBox(height: 12),
|
|
|
|
// Bottom Row (Project Spendings)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
height: 10,
|
|
width: 140,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
height: 16,
|
|
width: 80,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget documentSkeletonLoader() {
|
|
return Column(
|
|
children: List.generate(5, (index) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Date placeholder
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
|
child: ShimmerEffect(
|
|
child: Container(
|
|
height: 12,
|
|
width: 80,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Document Card Skeleton
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Icon Placeholder
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.description,
|
|
color: Colors.transparent), // invisible icon
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// Text placeholders
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 80,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 14,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 12,
|
|
width: 100,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Action icon placeholder
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
static Widget documentDetailsSkeletonLoader() {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Details Card
|
|
Container(
|
|
constraints: const BoxConstraints(maxWidth: 460),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.06),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 16,
|
|
width: 180,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
height: 12,
|
|
width: 120,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Tags placeholder
|
|
Wrap(
|
|
spacing: 6,
|
|
runSpacing: 6,
|
|
children: List.generate(3, (index) {
|
|
return Container(
|
|
height: 20,
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Info rows placeholders
|
|
Column(
|
|
children: List.generate(10, (index) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 120,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Container(
|
|
height: 12,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Versions section skeleton
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: List.generate(3, (index) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 180,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
height: 10,
|
|
width: 120,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget employeeListSkeletonLoader() {
|
|
return Column(
|
|
children: List.generate(4, (index) {
|
|
return MyCard.bordered(
|
|
borderRadiusAll: 12,
|
|
paddingAll: 10,
|
|
margin: MySpacing.bottom(12),
|
|
shadow: MyShadow(elevation: 3),
|
|
child: ShimmerEffect(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 41,
|
|
height: 41,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
height: 14,
|
|
width: 100,
|
|
color: Colors.grey.shade300),
|
|
MySpacing.width(8),
|
|
Container(
|
|
height: 12,
|
|
width: 60,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.email,
|
|
size: 16, color: Colors.grey.shade300),
|
|
MySpacing.width(4),
|
|
Container(
|
|
height: 10,
|
|
width: 140,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.phone,
|
|
size: 16, color: Colors.grey.shade300),
|
|
MySpacing.width(4),
|
|
Container(
|
|
height: 10,
|
|
width: 100,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
static Widget employeeListCollapsedSkeletonLoader() {
|
|
return MyCard.bordered(
|
|
borderRadiusAll: 4,
|
|
paddingAll: 8,
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
children: List.generate(4, (index) {
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 31,
|
|
height: 31,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(16),
|
|
// Name, Designation & Buttons
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 100,
|
|
color: Colors.grey.shade300),
|
|
MySpacing.height(8),
|
|
Container(
|
|
height: 10,
|
|
width: 80,
|
|
color: Colors.grey.shade300),
|
|
MySpacing.height(12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Container(
|
|
height: 28,
|
|
width: 60,
|
|
color: Colors.grey.shade300),
|
|
MySpacing.width(8),
|
|
Container(
|
|
height: 28,
|
|
width: 60,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (index != 3)
|
|
Divider(
|
|
color: Colors.grey.withOpacity(0.3),
|
|
thickness: 1,
|
|
height: 1,
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget dailyProgressReportSkeletonLoader() {
|
|
return MyCard.bordered(
|
|
borderRadiusAll: 4,
|
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
paddingAll: 8,
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
children: List.generate(3, (index) {
|
|
return Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
height: 14, width: 120, color: Colors.grey.shade300),
|
|
Icon(Icons.add_circle, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
if (index != 2) ...[
|
|
MySpacing.height(12),
|
|
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1),
|
|
MySpacing.height(12),
|
|
],
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget dailyProgressPlanningSkeletonCollapsedOnly() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: List.generate(3, (index) {
|
|
return MyCard.bordered(
|
|
borderRadiusAll: 12,
|
|
paddingAll: 16,
|
|
margin: MySpacing.bottom(12),
|
|
shadow: MyShadow(elevation: 3),
|
|
child: ShimmerEffect(
|
|
child: Row(
|
|
children: [
|
|
// Icon placeholder
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(12),
|
|
// Text line
|
|
Expanded(
|
|
child: Container(height: 16, color: Colors.grey.shade300),
|
|
),
|
|
MySpacing.width(12),
|
|
// Expand button placeholder
|
|
Container(
|
|
width: 28,
|
|
height: 28,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
static Widget expenseListSkeletonLoader() {
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
|
itemCount: 6,
|
|
separatorBuilder: (_, __) =>
|
|
Divider(color: Colors.grey.shade300, height: 20),
|
|
itemBuilder: (context, index) {
|
|
return ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Title and Amount
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
height: 14,
|
|
width: 120,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
Container(
|
|
height: 14,
|
|
width: 80,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
// Date and Status
|
|
Row(
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 100,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
height: 12,
|
|
width: 50,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
static Widget employeeSkeletonCard() {
|
|
return MyCard.bordered(
|
|
margin: MySpacing.only(bottom: 12),
|
|
paddingAll: 12,
|
|
borderRadiusAll: 12,
|
|
shadow: MyShadow(
|
|
elevation: 1.5,
|
|
position: MyShadowPosition.bottom,
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
height: 35,
|
|
width: 35,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(12),
|
|
|
|
// Name, org, email, phone
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12, width: 120, color: Colors.grey.shade300),
|
|
MySpacing.height(6),
|
|
Container(height: 10, width: 80, color: Colors.grey.shade300),
|
|
MySpacing.height(8),
|
|
|
|
// Email placeholder
|
|
Row(
|
|
children: [
|
|
Icon(Icons.email_outlined,
|
|
size: 14, color: Colors.grey.shade300),
|
|
MySpacing.width(4),
|
|
Container(
|
|
height: 10, width: 140, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
|
|
// Phone placeholder
|
|
Row(
|
|
children: [
|
|
Icon(Icons.phone_outlined,
|
|
size: 14, color: Colors.grey.shade300),
|
|
MySpacing.width(4),
|
|
Container(
|
|
height: 10, width: 100, color: Colors.grey.shade300),
|
|
MySpacing.width(8),
|
|
Container(
|
|
height: 16,
|
|
width: 16,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
|
|
// Tags placeholder
|
|
Container(height: 8, width: 80, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Arrow
|
|
Icon(Icons.arrow_forward_ios,
|
|
size: 14, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static Widget contactSkeletonCard() {
|
|
return MyCard.bordered(
|
|
margin: MySpacing.only(bottom: 12),
|
|
paddingAll: 16,
|
|
borderRadiusAll: 16,
|
|
shadow: MyShadow(
|
|
elevation: 1.5,
|
|
position: MyShadowPosition.bottom,
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
height: 40,
|
|
width: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
MySpacing.width(12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: 100,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 10,
|
|
width: 60,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
MySpacing.height(16),
|
|
Container(height: 10, width: 150, color: Colors.grey.shade300),
|
|
MySpacing.height(8),
|
|
Container(height: 10, width: 100, color: Colors.grey.shade300),
|
|
MySpacing.height(8),
|
|
Container(height: 10, width: 120, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ====================================================================
|
|
// NEW SKELETON LOADER METHODS
|
|
// ====================================================================
|
|
|
|
/// Skeleton for the CollectionsHealthWidget
|
|
static Widget collectionHealthSkeleton() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
// Header Skeleton
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 14, width: 180, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(
|
|
height: 10, width: 140, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Main Content Row (Left Chart + Right Metrics)
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
// Left Chart Section
|
|
Expanded(
|
|
flex: 5,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Gauge Chart Placeholder
|
|
Container(
|
|
width: 120,
|
|
height: 80,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(60), bottom: Radius.zero),
|
|
),
|
|
alignment: Alignment.bottomCenter,
|
|
child: Container(
|
|
height: 12, width: 60, color: Colors.grey.shade400),
|
|
),
|
|
const SizedBox(height: 20),
|
|
// Summary Text Placeholders
|
|
Container(
|
|
height: 16, width: 150, color: Colors.grey.shade300),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 12, width: 200, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(
|
|
height: 12, width: 100, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
// Right Metrics Section
|
|
Expanded(
|
|
flex: 4,
|
|
child: Column(
|
|
children: [
|
|
// Metric Card 1
|
|
_buildMetricCardSkeleton(),
|
|
const SizedBox(height: 10),
|
|
// Metric Card 2
|
|
_buildMetricCardSkeleton(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Aging Analysis Section
|
|
Container(height: 14, width: 220, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(height: 10, width: 180, color: Colors.grey.shade300),
|
|
const SizedBox(height: 10),
|
|
|
|
// Aging Stacked Bar Placeholder
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Row(
|
|
children: List.generate(
|
|
4,
|
|
(index) => Expanded(
|
|
flex: 1,
|
|
child: Container(
|
|
height: 16, color: Colors.grey.shade400),
|
|
)),
|
|
),
|
|
),
|
|
const SizedBox(height: 15),
|
|
|
|
// Aging Legend Placeholders
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 8,
|
|
children: List.generate(
|
|
4,
|
|
(index) => Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle)),
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
height: 12,
|
|
width: 120,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Helper for Metric Card Skeleton
|
|
static Widget _buildMetricCardSkeleton() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200, // Background color for the card
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Container(height: 10, width: 90, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(height: 12, width: 100, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(height: 14, width: 80, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Skeleton for the CompactPurchaseInvoiceDashboard
|
|
static Widget purchaseInvoiceDashboardSkeleton() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 15,
|
|
offset: const Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ShimmerEffect(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Header Skeleton
|
|
Row(mainAxisAlignment: MainAxisAlignment.start, children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 14, width: 200, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(
|
|
height: 10, width: 150, color: Colors.grey.shade300),
|
|
]))
|
|
]),
|
|
const SizedBox(height: 16),
|
|
|
|
// Total Value Card Skeleton
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200, // Simulated light blue background
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
height: 12, width: 160, color: Colors.grey.shade300),
|
|
Icon(Icons.account_balance_wallet_outlined,
|
|
color: Colors.grey.shade300, size: 20),
|
|
],
|
|
),
|
|
MySpacing.height(8),
|
|
Container(
|
|
height: 16, width: 120, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(
|
|
height: 12, width: 180, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Condensed Metrics Row Skeleton
|
|
Row(
|
|
children: List.generate(
|
|
3,
|
|
(index) => Expanded(
|
|
child: Padding(
|
|
padding: index == 1
|
|
? MySpacing.symmetric(horizontal: 8)
|
|
: EdgeInsets.zero,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200, // Card background
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.circle,
|
|
color: Colors.grey.shade300, size: 16),
|
|
MySpacing.width(4),
|
|
Container(
|
|
height: 10,
|
|
width: 50,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
MySpacing.height(6),
|
|
Container(
|
|
height: 14,
|
|
width: 80,
|
|
color: Colors.grey.shade300),
|
|
Container(
|
|
height: 10,
|
|
width: 60,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Divider(
|
|
height: 1,
|
|
thickness: 0.5,
|
|
color: Colors.transparent), // Hidden divider for spacing
|
|
const SizedBox(height: 16),
|
|
|
|
// Status Breakdown Section Skeleton (Chart + Legend)
|
|
Container(height: 12, width: 180, color: Colors.grey.shade300),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Donut Chart Placeholder
|
|
Container(
|
|
width: 120,
|
|
height: 120,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.grey.shade300, width: 6),
|
|
color: Colors.grey.shade200,
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
height: 10, width: 60, color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
Container(
|
|
height: 14, width: 40, color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
// Legend/Details Placeholder
|
|
// Aging Legend Placeholders
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 8,
|
|
children: List.generate(
|
|
4,
|
|
(index) => Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle)),
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
height: 12,
|
|
width: 115, // Reduced from 120
|
|
color: Colors.grey.shade300),
|
|
],
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Divider(
|
|
height: 1,
|
|
thickness: 0.5,
|
|
color: Colors.transparent), // Hidden divider for spacing
|
|
const SizedBox(height: 16),
|
|
|
|
// Top Projects Section Skeleton
|
|
Container(height: 12, width: 200, color: Colors.grey.shade300),
|
|
const SizedBox(height: 8),
|
|
Column(
|
|
children: List.generate(
|
|
3,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: 6,
|
|
height: 6,
|
|
margin: const EdgeInsets.only(right: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade300,
|
|
shape: BoxShape.circle)),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
height: 12,
|
|
width: double.infinity,
|
|
color: Colors.grey.shade300),
|
|
MySpacing.height(4),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(5),
|
|
child: Container(
|
|
height: 4, color: Colors.grey.shade300),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Container(
|
|
height: 14,
|
|
width: 70,
|
|
color: Colors.grey.shade300),
|
|
MySpacing.height(2),
|
|
Container(
|
|
height: 10,
|
|
width: 40,
|
|
color: Colors.grey.shade300),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A custom reusable Shimmer Effect widget.
|
|
/// This creates a gradient wave animation over any child widget.
|
|
class ShimmerEffect extends StatefulWidget {
|
|
final Widget child;
|
|
final Color baseColor;
|
|
final Color highlightColor;
|
|
|
|
ShimmerEffect({
|
|
Key? key,
|
|
required this.child,
|
|
Color? baseColor,
|
|
Color? highlightColor,
|
|
}) : baseColor = baseColor ?? Colors.grey.shade300,
|
|
highlightColor = highlightColor ?? Colors.grey.shade100,
|
|
super(key: key);
|
|
|
|
@override
|
|
State<ShimmerEffect> createState() => _ShimmerEffectState();
|
|
}
|
|
|
|
class _ShimmerEffectState extends State<ShimmerEffect>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(seconds: 2), // Adjust speed here
|
|
)..repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _controller,
|
|
child: widget.child,
|
|
builder: (context, child) {
|
|
return ShaderMask(
|
|
blendMode: BlendMode.srcATop,
|
|
shaderCallback: (bounds) {
|
|
final gradient = LinearGradient(
|
|
colors: [
|
|
widget.baseColor,
|
|
widget.highlightColor,
|
|
widget.baseColor,
|
|
],
|
|
stops: const [0.1, 0.5, 0.9],
|
|
begin: const Alignment(-1.0, -0.3),
|
|
end: const Alignment(1.0, 0.3),
|
|
transform: _SlidingGradientTransform(_controller.value),
|
|
);
|
|
return gradient.createShader(bounds);
|
|
},
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SlidingGradientTransform extends GradientTransform {
|
|
final double percent;
|
|
|
|
const _SlidingGradientTransform(this.percent);
|
|
|
|
@override
|
|
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
|
|
// This moves the gradient across the width of the widget
|
|
return Matrix4.translationValues(
|
|
(bounds.width * 2) * percent - bounds.width, 0, 0);
|
|
}
|
|
}
|