enhacement of UI for mobile screen responsiveness
This commit is contained in:
parent
41112a3eea
commit
5bed5bd2f4
@ -95,7 +95,7 @@ class _SearchAndFilterState extends State<SearchAndFilter> with UIMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: MySpacing.fromLTRB(12, 10, 12, 0),
|
||||
padding: MySpacing.fromLTRB(12, 10, 12, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -179,13 +179,6 @@ class ToggleButtonsRow extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F0F0),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@ -286,82 +279,84 @@ class ExpenseList extends StatelessWidget {
|
||||
return Center(child: MyText.bodyMedium('No expenses found.'));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
||||
itemCount: expenseList.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
Divider(color: Colors.grey.shade300, height: 20),
|
||||
itemBuilder: (context, index) {
|
||||
final expense = expenseList[index];
|
||||
final formattedDate = DateTimeUtils.convertUtcToLocal(
|
||||
expense.transactionDate.toIso8601String(),
|
||||
format: 'dd MMM yyyy',
|
||||
);
|
||||
return SafeArea(
|
||||
bottom: true,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 100),
|
||||
itemCount: expenseList.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
Divider(color: Colors.grey.shade300, height: 20),
|
||||
itemBuilder: (context, index) {
|
||||
final expense = expenseList[index];
|
||||
final formattedDate = DateTimeUtils.convertUtcToLocal(
|
||||
expense.transactionDate.toIso8601String(),
|
||||
format: 'dd MMM yyyy',
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () async {
|
||||
await Get.to(
|
||||
() => ExpenseDetailScreen(expenseId: expense.id),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(expense.expenseCategory.name,
|
||||
fontWeight: 600),
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodyMedium('${expense.formattedAmount}',
|
||||
fontWeight: 600),
|
||||
if (expense.status.name.toLowerCase() == 'draft') ...[
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
_showDeleteConfirmation(context, expense),
|
||||
child: const Icon(Icons.delete,
|
||||
color: Colors.red, size: 20),
|
||||
),
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: () async {
|
||||
await Get.to(() => ExpenseDetailScreen(expenseId: expense.id));
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
MyText.bodyMedium(expense.expenseCategory.name,
|
||||
fontWeight: 600),
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodyMedium('${expense.formattedAmount}',
|
||||
fontWeight: 600),
|
||||
if (expense.status.name.toLowerCase() ==
|
||||
'draft') ...[
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
_showDeleteConfirmation(context, expense),
|
||||
child: const Icon(Icons.delete,
|
||||
color: Colors.red, size: 20),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodySmall(formattedDate, fontWeight: 500),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(
|
||||
'0xff${expense.status.color.substring(1)}'))
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: MyText.bodySmall(
|
||||
expense.status.name,
|
||||
color: Colors.white,
|
||||
fontWeight: 500,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodySmall(formattedDate, fontWeight: 500),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(
|
||||
'0xff${expense.status.color.substring(1)}'))
|
||||
.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: MyText.bodySmall(
|
||||
expense.status.name,
|
||||
color: Colors.white,
|
||||
fontWeight: 500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -354,38 +354,27 @@ class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
||||
final result = await showModalBottomSheet<List<EmployeeModel>>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
barrierColor: Colors.white,
|
||||
useSafeArea: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.85,
|
||||
minChildSize: 0.6,
|
||||
maxChildSize: 1.0,
|
||||
builder: (_, scrollController) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: MultipleSelectRoleBottomSheet(
|
||||
projectId: selectedProjectId!,
|
||||
organizationId: selectedOrganization?.id,
|
||||
serviceId: selectedService?.id,
|
||||
roleId: selectedRoleId,
|
||||
initiallySelected: controller.selectedEmployees.toList(),
|
||||
scrollController: scrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
builder: (_) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.90,
|
||||
child: MultipleSelectRoleBottomSheet(
|
||||
projectId: selectedProjectId!,
|
||||
organizationId: selectedOrganization?.id,
|
||||
serviceId: selectedService?.id,
|
||||
roleId: selectedRoleId,
|
||||
initiallySelected: controller.selectedEmployees.toList(),
|
||||
scrollController: ScrollController(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
controller.selectedEmployees
|
||||
.assignAll(result); // RxList updates UI automatically
|
||||
controller.selectedEmployees.assignAll(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -38,96 +38,111 @@ class _DirectoryMainScreenState extends State<DirectoryMainScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(72),
|
||||
child: AppBar(
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
final bool isLandscape = orientation == Orientation.landscape;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.width(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
appBar: PreferredSize(
|
||||
preferredSize: Size.fromHeight(
|
||||
isLandscape ? 55 : 72, // Responsive height
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: AppBar(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
elevation: 0.5,
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 0,
|
||||
title: Padding(
|
||||
padding: MySpacing.xy(16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Directory',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new,
|
||||
color: Colors.black, size: 20),
|
||||
onPressed: () => Get.offNamed('/dashboard'),
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
MySpacing.width(8),
|
||||
|
||||
/// FIX: Flexible to prevent overflow in landscape
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MyText.titleLarge(
|
||||
'Directory',
|
||||
fontWeight: 700,
|
||||
color: Colors.black,
|
||||
),
|
||||
MySpacing.height(2),
|
||||
GetBuilder<ProjectController>(
|
||||
builder: (projectController) {
|
||||
final projectName =
|
||||
projectController.selectedProject?.name ??
|
||||
'Select Project';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.work_outline,
|
||||
size: 14, color: Colors.grey),
|
||||
MySpacing.width(4),
|
||||
Expanded(
|
||||
child: MyText.bodySmall(
|
||||
projectName,
|
||||
fontWeight: 600,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// MAIN CONTENT
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Directory"),
|
||||
Tab(text: "Notes"),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
DirectoryView(),
|
||||
NotesView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// ---------------- TabBar ----------------
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Directory"),
|
||||
Tab(text: "Notes"),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ---------------- TabBarView ----------------
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
DirectoryView(),
|
||||
NotesView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,36 +49,42 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(
|
||||
0xFFF5F5F5),
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
appBar: _buildAppBar(),
|
||||
body: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final emp = controller.selectedEmployee.value;
|
||||
if (emp != null) {
|
||||
await controller.fetchAdvancePayments(emp.id.toString());
|
||||
}
|
||||
},
|
||||
color: Colors.white,
|
||||
backgroundColor: contentTheme.primary,
|
||||
strokeWidth: 2.5,
|
||||
displacement: 60,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Container(
|
||||
color:
|
||||
const Color(0xFFF5F5F5),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildEmployeeDropdown(context),
|
||||
_buildTopBalance(),
|
||||
_buildPaymentList(),
|
||||
],
|
||||
|
||||
// ✅ SafeArea added so nothing hides under system navigation buttons
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
final emp = controller.selectedEmployee.value;
|
||||
if (emp != null) {
|
||||
await controller.fetchAdvancePayments(emp.id.toString());
|
||||
}
|
||||
},
|
||||
color: Colors.white,
|
||||
backgroundColor: contentTheme.primary,
|
||||
strokeWidth: 2.5,
|
||||
displacement: 60,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
// ✅ Extra bottom padding so content does NOT go under 3-button navbar
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20,
|
||||
),
|
||||
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildEmployeeDropdown(context),
|
||||
_buildTopBalance(),
|
||||
_buildPaymentList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -322,7 +328,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ No employee selected yet
|
||||
if (controller.selectedEmployee.value == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 100),
|
||||
@ -330,7 +335,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Employee selected but no payments found
|
||||
if (controller.payments.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.only(top: 100),
|
||||
@ -340,7 +344,6 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Payments available
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@ -378,7 +381,7 @@ class _AdvancePaymentScreenState extends State<AdvancePaymentScreen>
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE0E0E0), width: 0.9),
|
||||
bottom: BorderSide(color: const Color(0xFFE0E0E0), width: 0.9),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
|
||||
@ -113,171 +113,190 @@ class _FinanceScreenState extends State<FinanceScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
body: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Obx(() {
|
||||
if (menuController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
body: SafeArea(
|
||||
top: false, // keep appbar area same
|
||||
bottom: true, // avoid system bottom buttons
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Obx(() {
|
||||
if (menuController.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (menuController.hasError.value || menuController.menuItems.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Failed to load menus. Please try again later.",
|
||||
style: TextStyle(color: Colors.red),
|
||||
if (menuController.hasError.value ||
|
||||
menuController.menuItems.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"Failed to load menus. Please try again later.",
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final financeMenuIds = [
|
||||
MenuItems.expenseReimbursement,
|
||||
MenuItems.paymentRequests,
|
||||
MenuItems.advancePaymentStatements,
|
||||
];
|
||||
|
||||
final financeMenus = menuController.menuItems
|
||||
.where((m) => financeMenuIds.contains(m.id) && m.available)
|
||||
.toList();
|
||||
|
||||
if (financeMenus.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"You don’t have access to the Finance section.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---- IMPORTANT FIX: Add bottom safe padding ----
|
||||
final double bottomInset =
|
||||
MediaQuery.of(context).viewPadding.bottom;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
bottomInset +
|
||||
24, // ensures charts never go under system buttons
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFinanceModulesCompact(financeMenus),
|
||||
MySpacing.height(24),
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
ExpenseTypeReportChart(),
|
||||
MySpacing.height(24),
|
||||
MonthlyExpenseDashboardChart(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter allowed Finance menus dynamically
|
||||
final financeMenuIds = [
|
||||
MenuItems.expenseReimbursement,
|
||||
MenuItems.paymentRequests,
|
||||
MenuItems.advancePaymentStatements,
|
||||
];
|
||||
|
||||
final financeMenus = menuController.menuItems
|
||||
.where((m) => financeMenuIds.contains(m.id) && m.available)
|
||||
.toList();
|
||||
|
||||
if (financeMenus.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
"You don’t have access to the Finance section.",
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildFinanceModulesCompact(financeMenus),
|
||||
MySpacing.height(24),
|
||||
ExpenseByStatusWidget(controller: dashboardController),
|
||||
MySpacing.height(24),
|
||||
ExpenseTypeReportChart(),
|
||||
MySpacing.height(24),
|
||||
MonthlyExpenseDashboardChart(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Finance Modules (Compact Dashboard-style) ---
|
||||
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
|
||||
// Map menu IDs to icon + color
|
||||
final Map<String, _FinanceCardMeta> financeCardMeta = {
|
||||
MenuItems.expenseReimbursement: _FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
|
||||
MenuItems.paymentRequests: _FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
|
||||
MenuItems.advancePaymentStatements: _FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
|
||||
};
|
||||
Widget _buildFinanceModulesCompact(List<MenuItem> financeMenus) {
|
||||
// Map menu IDs to icon + color
|
||||
final Map<String, _FinanceCardMeta> financeCardMeta = {
|
||||
MenuItems.expenseReimbursement:
|
||||
_FinanceCardMeta(LucideIcons.badge_dollar_sign, contentTheme.info),
|
||||
MenuItems.paymentRequests:
|
||||
_FinanceCardMeta(LucideIcons.receipt_text, contentTheme.primary),
|
||||
MenuItems.advancePaymentStatements:
|
||||
_FinanceCardMeta(LucideIcons.wallet, contentTheme.warning),
|
||||
};
|
||||
|
||||
// Build the stat items using API-provided mobileLink
|
||||
final stats = financeMenus.map((menu) {
|
||||
final meta = financeCardMeta[menu.id]!;
|
||||
// Build the stat items using API-provided mobileLink
|
||||
final stats = financeMenus.map((menu) {
|
||||
final meta = financeCardMeta[menu.id]!;
|
||||
|
||||
// --- Log the routing info ---
|
||||
debugPrint(
|
||||
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
|
||||
// --- Log the routing info ---
|
||||
debugPrint(
|
||||
"[Finance Card] ID: ${menu.id}, Title: ${menu.name}, Route: ${menu.mobileLink}");
|
||||
|
||||
return _FinanceStatItem(
|
||||
meta.icon,
|
||||
menu.name,
|
||||
meta.color,
|
||||
menu.mobileLink, // Each card navigates to its own route
|
||||
);
|
||||
}).toList();
|
||||
return _FinanceStatItem(
|
||||
meta.icon,
|
||||
menu.name,
|
||||
meta.color,
|
||||
menu.mobileLink, // Each card navigates to its own route
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final projectSelected = projectController.selectedProject != null;
|
||||
final projectSelected = projectController.selectedProject != null;
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
// Determine number of columns dynamically
|
||||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||||
double cardWidth = (constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
// Determine number of columns dynamically
|
||||
int crossAxisCount = (constraints.maxWidth ~/ 80).clamp(2, 4);
|
||||
double cardWidth =
|
||||
(constraints.maxWidth - (crossAxisCount - 1) * 6) / crossAxisCount;
|
||||
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
alignment: WrapAlignment.end,
|
||||
children: stats
|
||||
.map((stat) => _buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
alignment: WrapAlignment.end,
|
||||
children: stats
|
||||
.map((stat) =>
|
||||
_buildFinanceModuleCard(stat, projectSelected, cardWidth))
|
||||
.toList(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildFinanceModuleCard(
|
||||
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
||||
return Opacity(
|
||||
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
|
||||
child: IgnorePointer(
|
||||
ignoring: !isProjectSelected,
|
||||
child: InkWell(
|
||||
onTap: () => _onCardTap(stat, isProjectSelected),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: MyCard.bordered(
|
||||
width: width,
|
||||
height: 60,
|
||||
paddingAll: 4,
|
||||
borderRadiusAll: 5,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: stat.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
stat.icon,
|
||||
size: 16,
|
||||
color: stat.color,
|
||||
),
|
||||
),
|
||||
MySpacing.height(4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
stat.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Widget _buildFinanceModuleCard(
|
||||
_FinanceStatItem stat, bool isProjectSelected, double width) {
|
||||
return Opacity(
|
||||
opacity: isProjectSelected ? 1.0 : 0.4, // Dim if no project selected
|
||||
child: IgnorePointer(
|
||||
ignoring: !isProjectSelected,
|
||||
child: InkWell(
|
||||
onTap: () => _onCardTap(stat, isProjectSelected),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: MyCard.bordered(
|
||||
width: width,
|
||||
height: 60,
|
||||
paddingAll: 4,
|
||||
borderRadiusAll: 5,
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.15)),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: stat.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(
|
||||
stat.icon,
|
||||
size: 16,
|
||||
color: stat.color,
|
||||
),
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
MySpacing.height(4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
stat.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
title: "No Project Selected",
|
||||
middleText: "Please select a project before accessing this section.",
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("OK"),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Navigate to the card's specific route
|
||||
Get.toNamed(statItem.route);
|
||||
}
|
||||
|
||||
void _onCardTap(_FinanceStatItem statItem, bool isEnabled) {
|
||||
if (!isEnabled) {
|
||||
Get.defaultDialog(
|
||||
title: "No Project Selected",
|
||||
middleText: "Please select a project before accessing this section.",
|
||||
confirm: ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("OK"),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Navigate to the card's specific route
|
||||
Get.toNamed(statItem.route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _FinanceStatItem {
|
||||
final IconData icon;
|
||||
|
||||
@ -99,42 +99,50 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: _buildAppBar(),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Current Month"),
|
||||
Tab(text: "History"),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPaymentRequestList(isHistory: false),
|
||||
_buildPaymentRequestList(isHistory: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ------------------------
|
||||
// FIX: SafeArea prevents content from going under 3-button navbar
|
||||
// ------------------------
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: Colors.black,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
indicatorColor: Colors.red,
|
||||
tabs: const [
|
||||
Tab(text: "Current Month"),
|
||||
Tab(text: "History"),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.grey[100],
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSearchBar(),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildPaymentRequestList(isHistory: false),
|
||||
_buildPaymentRequestList(isHistory: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
floatingActionButton: Obx(() {
|
||||
if (permissionController.permissions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@ -294,7 +302,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
|
||||
final list = filteredList(isHistory: isHistory);
|
||||
|
||||
// ScrollController for infinite scroll
|
||||
final scrollController = ScrollController();
|
||||
scrollController.addListener(() {
|
||||
if (scrollController.position.pixels >=
|
||||
@ -309,6 +316,7 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
child: list.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
@ -325,7 +333,12 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
)
|
||||
: ListView.separated(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 80),
|
||||
|
||||
// ------------------------
|
||||
// FIX: ensure bottom list items stay visible above nav bar
|
||||
// ------------------------
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 120),
|
||||
|
||||
itemCount: list.length + 1,
|
||||
separatorBuilder: (_, __) =>
|
||||
Divider(color: Colors.grey.shade300, height: 20),
|
||||
@ -365,10 +378,6 @@ class _PaymentRequestMainScreenState extends State<PaymentRequestMainScreen>
|
||||
Row(
|
||||
children: [
|
||||
MyText.bodyMedium(item.expenseCategory.name, fontWeight: 600),
|
||||
|
||||
// -------------------------------
|
||||
// ADV CHIP (only if advance)
|
||||
// -------------------------------
|
||||
if (item.isAdvancePayment == true) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
|
||||
@ -51,6 +51,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
controller.fetchJobDetail(widget.jobId).then((_) {
|
||||
final job = controller.jobDetail.value?.data;
|
||||
if (job != null) {
|
||||
_selectedTags.value = job.tags ?? [];
|
||||
_titleController.text = job.title ?? '';
|
||||
_descriptionController.text = job.description ?? '';
|
||||
_startDateController.text = DateTimeUtils.convertUtcToLocal(
|
||||
@ -169,6 +170,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
|
||||
message: "Job updated successfully",
|
||||
type: SnackbarType.success);
|
||||
await controller.fetchJobDetail(widget.jobId);
|
||||
final updatedJob = controller.jobDetail.value?.data;
|
||||
if (updatedJob != null) {
|
||||
_selectedTags.value = updatedJob.tags ?? [];
|
||||
}
|
||||
|
||||
isEditing.value = false;
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
|
||||
@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final ServiceProjectController controller =
|
||||
Get.put(ServiceProjectController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Fetch projects safely after first frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.fetchProjects();
|
||||
});
|
||||
@ -49,10 +49,9 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () {
|
||||
// Navigate to ServiceProjectDetailsScreen
|
||||
Get.to(() => ServiceProjectDetailsScreen(
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
projectName: project.name,
|
||||
));
|
||||
},
|
||||
child: Padding(
|
||||
@ -60,7 +59,6 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
/// Project Header
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -92,20 +90,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
MySpacing.height(10),
|
||||
|
||||
/// Assigned Date
|
||||
_buildDetailRow(
|
||||
Icons.date_range_outlined,
|
||||
Colors.teal,
|
||||
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
|
||||
fontSize: 13,
|
||||
),
|
||||
|
||||
MySpacing.height(8),
|
||||
|
||||
/// Client Info
|
||||
if (project.client != null)
|
||||
_buildDetailRow(
|
||||
Icons.account_circle_outlined,
|
||||
@ -113,20 +105,14 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
"Client: ${project.client!.name} (${project.client!.contactPerson})",
|
||||
fontSize: 13,
|
||||
),
|
||||
|
||||
MySpacing.height(8),
|
||||
|
||||
/// Contact Info
|
||||
_buildDetailRow(
|
||||
Icons.phone,
|
||||
Colors.green,
|
||||
"Contact: ${project.contactName} (${project.contactPhone})",
|
||||
fontSize: 13,
|
||||
),
|
||||
|
||||
MySpacing.height(12),
|
||||
|
||||
/// Services List
|
||||
if (project.services.isNotEmpty)
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
@ -197,90 +183,97 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F5F5),
|
||||
|
||||
appBar: CustomAppBar(
|
||||
title: "Service Projects",
|
||||
projectName: 'All Service Projects',
|
||||
onBackPressed: () => Get.toNamed('/dashboard'),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
/// Search bar and actions
|
||||
Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
prefixIcon: const Icon(Icons.search,
|
||||
size: 20, color: Colors.grey),
|
||||
suffixIcon: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: searchController,
|
||||
builder: (context, value, _) {
|
||||
if (value.text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.clear,
|
||||
size: 20, color: Colors.grey),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.updateSearch('');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
hintText: 'Search projects...',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
|
||||
// FIX 1: Entire body wrapped in SafeArea
|
||||
body: SafeArea(
|
||||
bottom: true,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: MySpacing.xy(8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 12),
|
||||
prefixIcon: const Icon(Icons.search,
|
||||
size: 20, color: Colors.grey),
|
||||
suffixIcon: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: searchController,
|
||||
builder: (context, value, _) {
|
||||
if (value.text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.clear,
|
||||
size: 20, color: Colors.grey),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
controller.updateSearch('');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
hintText: 'Search projects...',
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
/// Project List
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final projects = controller.filteredProjects;
|
||||
|
||||
final projects = controller.filteredProjects;
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshProjects,
|
||||
backgroundColor: Colors.indigo,
|
||||
color: Colors.white,
|
||||
child: projects.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: MySpacing.only(
|
||||
left: 8, right: 8, top: 4, bottom: 80),
|
||||
itemCount: projects.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildProjectCard(projects[index]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
return MyRefreshIndicator(
|
||||
onRefresh: _refreshProjects,
|
||||
backgroundColor: Colors.indigo,
|
||||
color: Colors.white,
|
||||
child: projects.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
|
||||
// FIX 2: Increased bottom padding for landscape
|
||||
padding: MySpacing.only(
|
||||
left: 8, right: 8, top: 4, bottom: 120),
|
||||
|
||||
itemCount: projects.length,
|
||||
separatorBuilder: (_, __) => MySpacing.height(12),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildProjectCard(projects[index]),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user