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