From c94efac1de3b2dd4402ca7c58b67252799fff5c4 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 10:50:50 +0530 Subject: [PATCH 01/37] done landscape responsive of all screen --- .../expense/expense_main_components.dart | 183 ++++++---- .../attendance/attendence_filter_sheet.dart | 69 ++-- .../add_payment_request_bottom_sheet.dart | 176 +++++----- lib/view/directory/directory_main_screen.dart | 76 ++-- lib/view/finance/advance_payment_screen.dart | 93 +++-- lib/view/finance/finance_screen.dart | 328 ++++++++++-------- lib/view/finance/payment_request_screen.dart | 103 ++++-- 7 files changed, 606 insertions(+), 422 deletions(-) diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index ae04ff6..3741a0e 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -282,90 +282,129 @@ class ExpenseList extends StatelessWidget { @override Widget build(BuildContext context) { - if (expenseList.isEmpty && !Get.find().isLoading.value) { - return Center(child: MyText.bodyMedium('No expenses found.')); - } + final ExpenseController controller = Get.find(); - 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( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - return Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () async { - final result = await Get.to( - () => ExpenseDetailScreen(expenseId: expense.id), - arguments: {'expense': expense}, - ); - if (result == true && onViewDetail != null) { - await onViewDetail!(); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (controller.isLoading.value && expenseList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (expenseList.isEmpty) { + return const Center( + child: Text( + 'No expenses found.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + // PORTRAIT MODE + if (!isLandscape) { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ); + } + + // LANDSCAPE β†’ WRAP IN SCROLL FOR SAFETY + return SingleChildScrollView( + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ), + ), + ); + }, + ), + ); + } + + Widget _buildItem(BuildContext context, int 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 { + final result = await Get.to( + () => ExpenseDetailScreen(expenseId: expense.id), + arguments: {'expense': expense}, + ); + if (result == true && onViewDetail != null) { + await onViewDetail!(); + } + }, + 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( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - MyText.bodyMedium(expense.expenseCategory.name, + MyText.bodyMedium('${expense.formattedAmount}', 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), + 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), ), - 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, + ), + ), + ], + ), + ], ), - ); - }, + ), + ), ); } } diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index abeaaa7..885e7bb 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -123,7 +123,6 @@ class _AttendanceFilterBottomSheetState }).toList(); final List widgets = [ - // πŸ”Ή View Section Padding( padding: const EdgeInsets.only(bottom: 4), child: Align( @@ -146,7 +145,6 @@ class _AttendanceFilterBottomSheetState }), ]; - // πŸ”Ή Organization filter widgets.addAll([ const Divider(), Padding( @@ -165,24 +163,6 @@ class _AttendanceFilterBottomSheetState color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 100, - height: 14, - color: Colors.grey.shade400, - ), - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Colors.grey.shade400, - shape: BoxShape.circle, - ), - ), - ], - ), ); } else if (widget.controller.organizations.isEmpty) { return Center( @@ -200,7 +180,6 @@ class _AttendanceFilterBottomSheetState }), ]); - // πŸ”Ή Date Range (only for Attendance Logs) if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), @@ -211,16 +190,12 @@ class _AttendanceFilterBottomSheetState child: MyText.titleSmall("Date Range", fontWeight: 600), ), ), - // βœ… Reusable DateRangePickerWidget DateRangePickerWidget( startDate: widget.controller.startDateAttendance, endDate: widget.controller.endDateAttendance, startLabel: "Start Date", endLabel: "End Date", - onDateRangeSelected: (start, end) { - // Optional: trigger UI updates if needed - setState(() {}); - }, + onDateRangeSelected: (_, __) => setState(() {}), ), ]); } @@ -232,18 +207,36 @@ class _AttendanceFilterBottomSheetState Widget build(BuildContext context) { return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: BaseBottomSheet( - title: "Attendance Filter", - submitText: "Apply", - onCancel: () => Navigator.pop(context), - onSubmit: () => Navigator.pop(context, { - 'selectedTab': tempSelectedTab, - 'selectedOrganization': widget.controller.selectedOrganization?.id, - }), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: buildMainFilters(), - ), + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; + + return BaseBottomSheet( + title: "Attendance Filter", + submitText: "Apply", + onCancel: () => Navigator.pop(context), + onSubmit: () => Navigator.pop(context, { + 'selectedTab': tempSelectedTab, + 'selectedOrganization': + widget.controller.selectedOrganization?.id, + }), + + // ---------------- UPDATED RESPONSIVE CHILD ---------------- + child: SizedBox( + height: isLandscape + ? constraints.maxHeight // πŸ”₯ Full screen in landscape + : constraints.maxHeight * 0.78, // normal in portrait + + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: buildMainFilters(), + ), + ), + ), + ); + }, ), ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index 0fe6e8d..abe1527 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -58,12 +58,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (widget.isEdit && widget.existingData != null) { final data = widget.existingData!; - // Prefill text fields controller.titleController.text = data["title"] ?? ""; controller.amountController.text = data["amount"]?.toString() ?? ""; controller.descriptionController.text = data["description"] ?? ""; - // Prefill due date if (data["dueDate"] != null && data["dueDate"].toString().isNotEmpty) { DateTime? dueDate = DateTime.tryParse(data["dueDate"].toString()); if (dueDate != null) { @@ -73,15 +71,14 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> } } - // Prefill dropdowns & toggles controller.selectedProject.value = { 'id': data["projectId"], 'name': data["projectName"], }; + controller.selectedPayee.value = data["payee"] ?? ""; controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; - // Categories & currencies everAll([controller.categories, controller.currencies], (_) { controller.selectedCategory.value = controller.categories .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); @@ -89,7 +86,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> .firstWhereOrNull((c) => c.id == data["currencyId"]); }); - // Attachments final attachmentsData = data["attachments"]; if (attachmentsData != null && attachmentsData is List && @@ -116,51 +112,56 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> @override Widget build(BuildContext context) { - return Obx(() => Form( - key: _formKey, - child: BaseBottomSheet( - title: widget.isEdit - ? "Edit Payment Request" - : "Create Payment Request", - isSubmitting: controller.isSubmitting.value, - onCancel: Get.back, - submitText: "Save as Draft", - onSubmit: () async { - if (_formKey.currentState!.validate() && _validateSelections()) { - bool success = false; - if (widget.isEdit && widget.existingData != null) { - final requestId = - widget.existingData!['id']?.toString() ?? ''; - if (requestId.isNotEmpty) { - success = await controller.submitEditedPaymentRequest( - requestId: requestId); + return Obx(() => SafeArea( + child: Form( + key: _formKey, + child: BaseBottomSheet( + title: widget.isEdit + ? "Edit Payment Request" + : "Create Payment Request", + isSubmitting: controller.isSubmitting.value, + onCancel: Get.back, + submitText: "Save as Draft", + onSubmit: () async { + if (_formKey.currentState!.validate() && + _validateSelections()) { + bool success = false; + + if (widget.isEdit && widget.existingData != null) { + final requestId = + widget.existingData!['id']?.toString() ?? ''; + if (requestId.isNotEmpty) { + success = await controller.submitEditedPaymentRequest( + requestId: requestId); + } else { + _showError("Invalid Payment Request ID"); + return; + } } else { - _showError("Invalid Payment Request ID"); - return; + success = await controller.submitPaymentRequest(); } - } else { - success = await controller.submitPaymentRequest(); - } - if (success) { - Get.back(); - if (widget.onUpdated != null) widget.onUpdated!(); + if (success) { + Get.back(); + widget.onUpdated?.call(); - showAppSnackbar( - title: "Success", - message: widget.isEdit - ? "Payment request updated successfully!" - : "Payment request created successfully!", - type: SnackbarType.success, - ); + showAppSnackbar( + title: "Success", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", + type: SnackbarType.success, + ); + } } - } - }, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDropdown( + }, + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDropdown( "Select Project", Icons.work_outline, controller.selectedProject.value?['name'] ?? @@ -168,9 +169,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.globalProjects, (p) => p['name'], controller.selectProject, - key: _projectDropdownKey), - _gap(), - _buildDropdown( + key: _projectDropdownKey, + ), + _gap(), + _buildDropdown( "Expense Category", Icons.category_outlined, controller.selectedCategory.value?.name ?? @@ -178,30 +180,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.categories, (c) => c.name, controller.selectCategory, - key: _categoryDropdownKey), - _gap(), - _buildTextField( - "Title", Icons.title_outlined, controller.titleController, - hint: "Enter title", validator: Validators.requiredField), - _gap(), - _buildRadio("Is Advance Payment", Icons.attach_money_outlined, - controller.isAdvancePayment, ["Yes", "No"]), - _gap(), - _buildDueDateField(), - _gap(), - _buildTextField("Amount", Icons.currency_rupee, - controller.amountController, - hint: "Enter Amount", - keyboardType: TextInputType.number, - validator: (v) => (v != null && - v.isNotEmpty && - double.tryParse(v) != null) - ? null - : "Enter valid amount"), - _gap(), - _buildPayeeAutocompleteField(), - _gap(), - _buildDropdown( + key: _categoryDropdownKey, + ), + _gap(), + _buildTextField("Title", Icons.title_outlined, + controller.titleController, + hint: "Enter title", + validator: Validators.requiredField), + _gap(), + _buildRadio( + "Is Advance Payment", + Icons.attach_money_outlined, + controller.isAdvancePayment, + ["Yes", "No"]), + _gap(), + _buildDueDateField(), + _gap(), + _buildTextField("Amount", Icons.currency_rupee, + controller.amountController, + hint: "Enter Amount", + keyboardType: TextInputType.number, + validator: (v) => (v != null && + v.isNotEmpty && + double.tryParse(v) != null) + ? null + : "Enter valid amount"), + _gap(), + _buildPayeeAutocompleteField(), + _gap(), + _buildDropdown( "Currency", Icons.monetization_on_outlined, controller.selectedCurrency.value?.currencyName ?? @@ -209,16 +216,19 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.currencies, (c) => c.currencyName, controller.selectCurrency, - key: _currencyDropdownKey), - _gap(), - _buildTextField("Description", Icons.description_outlined, - controller.descriptionController, - hint: "Enter description", - maxLines: 3, - validator: Validators.requiredField), - _gap(), - _buildAttachmentsSection(), - ], + key: _currencyDropdownKey, + ), + _gap(), + _buildTextField("Description", Icons.description_outlined, + controller.descriptionController, + hint: "Enter description", + maxLines: 3, + validator: Validators.requiredField), + _gap(), + _buildAttachmentsSection(), + MySpacing.height(30), + ], + ), ), ), ), @@ -284,6 +294,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final i = entry.key; final label = entry.value; final value = i == 0; + return Expanded( child: RadioListTile( contentPadding: EdgeInsets.zero, @@ -354,7 +365,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> displayStringForOption: (option) => option, fieldViewBuilder: (context, fieldController, focusNode, onFieldSubmitted) { - // Avoid updating during build WidgetsBinding.instance.addPostFrameCallback((_) { if (fieldController.text != controller.selectedPayee.value) { fieldController.text = controller.selectedPayee.value; diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index c5d55a1..5c06a6a 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -99,34 +99,60 @@ class _DirectoryMainScreenState extends State ), ), ), - 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"), - ], + body: SafeArea( + child: 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(), - ], + // ---------------- TabBarView + Scroll / Landscape Support ---------------- + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + if (isLandscape) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ), + ), + ); + } + + // Portrait + return TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ); + }, + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 2c1fe40..5cf3edc 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -49,39 +49,72 @@ class _AdvancePaymentScreenState extends State @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(), - ], + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return 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, + + // ---------------- PORTRAIT (UNCHANGED) ---------------- + child: !isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + color: const Color(0xFFF5F5F5), + child: Column( + children: [ + _buildSearchBar(), + _buildEmployeeDropdown(context), + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ) + + // ---------------- LANDSCAPE (FIXED) ---------------- + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + width: double.infinity, + color: const Color(0xFFF5F5F5), + + // ❗ Removed IntrinsicHeight + // ❗ Removed ConstrainedBox + // Dropdown can now open freely + + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSearchBar(), + _buildEmployeeDropdown( + context), // now overlay works + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ), ), - ), - ), + ); + }, ), ), ); diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index eaa1508..08df85d 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -113,171 +113,219 @@ class _FinanceScreenState extends State ), ), ), - body: FadeTransition( - opacity: _fadeAnimation, - child: Obx(() { - if (menuController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } + body: SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; - 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), - ), - ); - } + return Obx(() { + if (menuController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - // Filter allowed Finance menus dynamically - final financeMenuIds = [ - MenuItems.expenseReimbursement, - MenuItems.paymentRequests, - MenuItems.advancePaymentStatements, - ]; + 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 financeMenus = menuController.menuItems - .where((m) => financeMenuIds.contains(m.id) && m.available) - .toList(); + // Filter allowed Finance menus dynamically + final financeMenuIds = [ + MenuItems.expenseReimbursement, + MenuItems.paymentRequests, + MenuItems.advancePaymentStatements, + ]; - if (financeMenus.isEmpty) { - return const Center( - child: Text( - "You don’t have access to the Finance section.", - style: TextStyle(color: Colors.grey), - ), - ); - } + final financeMenus = menuController.menuItems + .where((m) => financeMenuIds.contains(m.id) && m.available) + .toList(); - 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(), - ], - ), - ); - }), + if (financeMenus.isEmpty) { + return const Center( + child: Text( + "You don’t have access to the Finance section.", + style: TextStyle(color: Colors.grey), + ), + ); + } + + // ---------------------- PORTRAIT MODE ---------------------- + if (!isLandscape) { + 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(), + ], + ), + ); + } + + // ---------------------- LANDSCAPE MODE ---------------------- + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildFinanceModulesCompact(financeMenus), + MySpacing.height(24), + + // Wider charts behave better side-by-side or full width + SizedBox( + width: constraints.maxWidth, + child: ExpenseByStatusWidget( + controller: dashboardController), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: ExpenseTypeReportChart(), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: MonthlyExpenseDashboardChart(), + ), + ], + ), + ); + }); + }, + ), + ), ), ); } // --- Finance Modules (Compact Dashboard-style) --- -Widget _buildFinanceModulesCompact(List financeMenus) { - // Map menu IDs to icon + color - final Map 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 financeMenus) { + // Map menu IDs to icon + color + final Map 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; diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index 417bfd4..ae7078e 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -88,41 +88,76 @@ class _PaymentRequestMainScreenState extends State 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), - ], - ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return Column( + children: [ + // ---------------- TabBar ---------------- + 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"), + ], ), - ], - ), - ), - ), - ], + ), + + // ---------------- Content Area ---------------- + Expanded( + child: Container( + color: Colors.grey[100], + child: isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList( + isHistory: false), + _buildPaymentRequestList( + isHistory: true), + ], + ), + ), + ], + ), + ), + ) + : Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList(isHistory: false), + _buildPaymentRequestList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), ), floatingActionButton: Obx(() { if (permissionController.permissions.isEmpty) { From 0b4f429f54407125b065091c211b6e31f55cbb49 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 16:25:25 +0530 Subject: [PATCH 02/37] Done all screen landscape responsive for mobile and tablet --- .../directory_filter_bottom_sheet.dart | 173 +++++---- .../assign_employee_bottom_sheet.dart | 229 ++++++------ lib/view/layouts/user_profile_right_bar.dart | 123 ++++--- .../service_project_screen.dart | 330 +++++++++--------- 4 files changed, 449 insertions(+), 406 deletions(-) diff --git a/lib/model/directory/directory_filter_bottom_sheet.dart b/lib/model/directory/directory_filter_bottom_sheet.dart index 4443f61..850b1c8 100644 --- a/lib/model/directory/directory_filter_bottom_sheet.dart +++ b/lib/model/directory/directory_filter_bottom_sheet.dart @@ -15,6 +15,7 @@ class DirectoryFilterBottomSheet extends StatefulWidget { class _DirectoryFilterBottomSheetState extends State { final DirectoryController controller = Get.find(); + final _categorySearchQuery = ''.obs; final _bucketSearchQuery = ''.obs; @@ -59,84 +60,100 @@ class _DirectoryFilterBottomSheetState Get.back(); }, onCancel: Get.back, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() { - final hasSelections = _tempSelectedCategories.isNotEmpty || - _tempSelectedBuckets.isNotEmpty; - if (!hasSelections) return const SizedBox.shrink(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 16, left: 4, right: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final hasSelections = _tempSelectedCategories.isNotEmpty || + _tempSelectedBuckets.isNotEmpty; + + if (!hasSelections) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText("Selected Filters:", fontWeight: 600), + const SizedBox(height: 4), + _buildChips(_tempSelectedCategories, + controller.contactCategories, _toggleCategory), + _buildChips(_tempSelectedBuckets, + controller.contactBuckets, _toggleBucket), + const SizedBox(height: 10), + ], + ); + }), + + // RESET BUTTON + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - MyText("Selected Filters:", fontWeight: 600), - const SizedBox(height: 4), - _buildChips(_tempSelectedCategories, - controller.contactCategories, _toggleCategory), - _buildChips(_tempSelectedBuckets, controller.contactBuckets, - _toggleBucket), - ], - ); - }), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( - onPressed: _resetFilters, - icon: const Icon(Icons.restart_alt, size: 18), - label: MyText("Reset All", color: Colors.red), - style: TextButton.styleFrom( - foregroundColor: Colors.red.shade400, + TextButton.icon( + onPressed: _resetFilters, + icon: const Icon(Icons.restart_alt, size: 18), + label: MyText("Reset All", color: Colors.red), ), - ), - ], - ), - if (controller.contactCategories.isNotEmpty) - Obx(() => _buildExpandableFilterSection( - title: "Categories", - expanded: _categoryExpanded, - searchQuery: _categorySearchQuery, - allItems: controller.contactCategories, - selectedItems: _tempSelectedCategories, - onToggle: _toggleCategory, - )), - if (controller.contactBuckets.isNotEmpty) - Obx(() => _buildExpandableFilterSection( - title: "Buckets", - expanded: _bucketExpanded, - searchQuery: _bucketSearchQuery, - allItems: controller.contactBuckets, - selectedItems: _tempSelectedBuckets, - onToggle: _toggleBucket, - )), - ], - ), - ), + ], + ), + + // CATEGORIES + if (controller.contactCategories.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Categories", + expanded: _categoryExpanded, + searchQuery: _categorySearchQuery, + allItems: controller.contactCategories, + selectedItems: _tempSelectedCategories, + onToggle: _toggleCategory, + )), + + // BUCKETS + if (controller.contactBuckets.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Buckets", + expanded: _bucketExpanded, + searchQuery: _bucketSearchQuery, + allItems: controller.contactBuckets, + selectedItems: _tempSelectedBuckets, + onToggle: _toggleBucket, + )), + + const SizedBox(height: 20), + ], + ), + ); + }, ), ); } + // ------------------------------ + // CHIP UI FOR SELECTED FILTERS + // ------------------------------ Widget _buildChips(RxList selectedIds, List allItems, Function(String) onRemoved) { final idToName = {for (var item in allItems) item.id: item.name}; + return Wrap( spacing: 4, runSpacing: 4, - children: selectedIds - .map((id) => Chip( - label: MyText(idToName[id] ?? "", color: Colors.black87), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => onRemoved(id), - backgroundColor: Colors.blue.shade50, - )) - .toList(), + children: selectedIds.map((id) { + return Chip( + label: MyText(idToName[id] ?? "", color: Colors.black87), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => onRemoved(id), + backgroundColor: Colors.blue.shade50, + ); + }).toList(), ); } + // ------------------------------ + // EXPANDABLE FILTER UI + // ------------------------------ Widget _buildExpandableFilterSection({ required String title, required RxBool expanded, @@ -146,7 +163,7 @@ class _DirectoryFilterBottomSheetState required Function(String) onToggle, }) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ GestureDetector( @@ -159,28 +176,27 @@ class _DirectoryFilterBottomSheetState : Icons.keyboard_arrow_right, size: 20, ), - const SizedBox(width: 4), - MyText( - "$title", - fontWeight: 600, - fontSize: 16, - ), + const SizedBox(width: 6), + MyText(title, fontWeight: 600, fontSize: 16), ], ), ), if (expanded.value) _buildFilterSection( + title: title, searchQuery: searchQuery, allItems: allItems, selectedItems: selectedItems, onToggle: onToggle, - title: title, ), ], ), ); } + // ------------------------------ + // FILTER LIST + SEARCH + // ------------------------------ Widget _buildFilterSection({ required String title, required RxString searchQuery, @@ -189,14 +205,16 @@ class _DirectoryFilterBottomSheetState required Function(String) onToggle, }) { final filteredList = allItems.where((item) { - if (searchQuery.isEmpty) return true; + if (searchQuery.value.isEmpty) return true; return item.name.toLowerCase().contains(searchQuery.value.toLowerCase()); }).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 6), + const SizedBox(height: 8), + + // SEARCH BOX TextField( onChanged: (value) => searchQuery.value = value, style: const TextStyle(fontSize: 13), @@ -215,7 +233,10 @@ class _DirectoryFilterBottomSheetState fillColor: Colors.grey.shade100, ), ), + const SizedBox(height: 8), + + // NO RESULTS if (filteredList.isEmpty) Row( children: [ @@ -227,7 +248,7 @@ class _DirectoryFilterBottomSheetState ) else ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 230), + constraints: const BoxConstraints(maxHeight: 260), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, @@ -238,7 +259,7 @@ class _DirectoryFilterBottomSheetState return Obx(() { final isSelected = selectedItems.contains(item.id); - return GestureDetector( + return InkWell( onTap: () => onToggle(item.id), child: Container( padding: const EdgeInsets.symmetric( @@ -271,7 +292,7 @@ class _DirectoryFilterBottomSheetState }); }, ), - ) + ), ], ); } diff --git a/lib/view/employees/assign_employee_bottom_sheet.dart b/lib/view/employees/assign_employee_bottom_sheet.dart index 61440ea..9f5e05d 100644 --- a/lib/view/employees/assign_employee_bottom_sheet.dart +++ b/lib/view/employees/assign_employee_bottom_sheet.dart @@ -48,116 +48,133 @@ class _AssignProjectBottomSheetState extends State { onCancel: () => Navigator.pop(context), onSubmit: _handleAssign, submitText: "Assign", - child: Obx(() { - if (assignController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - final projects = assignController.allProjects; - if (projects.isEmpty) { - return const Center(child: Text('No projects available.')); - } + /// πŸ”₯ MAKE BODY SCROLLABLE (fix for landscape) + child: LayoutBuilder( + builder: (context, constraints) { + return Obx(() { + if (assignController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall( - 'Select the projects to assign this employee.', - color: Colors.grey[600], - ), - MySpacing.height(8), + final projects = assignController.allProjects; + if (projects.isEmpty) { + return const Center(child: Text('No projects available.')); + } - // Select All - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Projects (${projects.length})', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - TextButton( - onPressed: () { - assignController.toggleSelectAll(); - }, - child: Obx(() { - return Text( - assignController.areAllSelected() - ? 'Deselect All' - : 'Select All', - style: const TextStyle( - color: Colors.blueAccent, - fontWeight: FontWeight.w600, - ), - ); - }), - ), - ], - ), - - // List of Projects - SizedBox( - height: 300, - child: ListView.builder( - controller: _scrollController, - itemCount: projects.length, - itemBuilder: (context, index) { - final GlobalProjectModel project = projects[index]; - return Obx(() { - final bool isSelected = - assignController.isProjectSelected( - project.id.toString(), - ); - return Theme( - data: Theme.of(context).copyWith( - checkboxTheme: CheckboxThemeData( - fillColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.selected) - ? Colors.blueAccent - : Colors.white, - ), - side: const BorderSide( - color: Colors.black, - width: 2, - ), - checkColor: - WidgetStateProperty.all(Colors.white), - ), - ), - child: CheckboxListTile( - dense: true, - value: isSelected, - title: Text( - project.name, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - ), - onChanged: (checked) { - assignController.toggleProjectSelection( - project.id.toString(), - checked ?? false, - ); - }, - activeColor: Colors.blueAccent, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), - ), - ); - }); - }, + return ConstrainedBox( + constraints: BoxConstraints( + /// πŸ”₯ Always allow enough height for scroll + maxHeight: constraints.maxHeight, ), - ), - ], - ); - }), + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodySmall( + 'Select the projects to assign this employee.', + color: Colors.grey[600], + ), + MySpacing.height(8), + + // Header Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Projects (${projects.length})', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + TextButton( + onPressed: () { + assignController.toggleSelectAll(); + }, + child: Obx(() { + return Text( + assignController.areAllSelected() + ? 'Deselect All' + : 'Select All', + style: const TextStyle( + color: Colors.blueAccent, + fontWeight: FontWeight.w600, + ), + ); + }), + ), + ], + ), + + /// πŸ”₯ List auto grows and scrolls β€” no fixed height + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: projects.length, + itemBuilder: (context, index) { + final GlobalProjectModel project = projects[index]; + return Obx(() { + final bool isSelected = + assignController.isProjectSelected( + project.id.toString(), + ); + return Theme( + data: Theme.of(context).copyWith( + checkboxTheme: CheckboxThemeData( + fillColor: + WidgetStateProperty.resolveWith( + (states) => + states.contains(WidgetState.selected) + ? Colors.blueAccent + : Colors.white, + ), + side: const BorderSide( + color: Colors.black, + width: 2, + ), + checkColor: + WidgetStateProperty.all(Colors.white), + ), + ), + child: CheckboxListTile( + dense: true, + value: isSelected, + title: Text( + project.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + onChanged: (checked) { + assignController.toggleProjectSelection( + project.id.toString(), + checked ?? false, + ); + }, + activeColor: Colors.blueAccent, + controlAffinity: + ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + ); + }); + }, + ), + ], + ), + ), + ); + }); + }, + ), ); }, ); diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 2ef33ba..62212ef 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -15,7 +15,6 @@ import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/controller/tenant/tenant_switch_controller.dart'; import 'package:marco/helpers/theme/theme_editor_widget.dart'; - class UserProfileBar extends StatefulWidget { final bool isCondensed; const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key); @@ -46,6 +45,7 @@ class _UserProfileBarState extends State @override Widget build(BuildContext context) { final bool isCondensed = widget.isCondensed; + return Padding( padding: const EdgeInsets.only(left: 14), child: ClipRRect( @@ -79,55 +79,74 @@ class _UserProfileBarState extends State ), ), child: SafeArea( - bottom: true, - child: Stack( - children: [ - Offstage( - offstage: _isThemeEditorVisible, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _isLoading - ? const _LoadingSection() - : _userProfileSection(isCondensed), - if (!_isLoading && !isCondensed) _switchTenantRow(), - MySpacing.height(12), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.7, - color: Colors.grey.withOpacity(0.25), + bottom: true, + child: Stack( + children: [ + // ======================= MAIN PROFILE SIDEBAR ======================= + Offstage( + offstage: _isThemeEditorVisible, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _isLoading + ? const _LoadingSection() + : _userProfileSection(isCondensed), + if (!_isLoading && !isCondensed) + _switchTenantRow(), + MySpacing.height(12), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.7, + color: Colors.grey.withOpacity(0.25), + ), + MySpacing.height(12), + _supportAndSettingsMenu(isCondensed), + const Spacer(), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.35, + color: Colors.grey.withOpacity(0.18), + ), + _logoutButton(isCondensed), + ], + ), + ), ), - MySpacing.height(12), - _supportAndSettingsMenu(isCondensed), - const Spacer(), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.35, - color: Colors.grey.withOpacity(0.18), - ), - _logoutButton(isCondensed), - ], - ), + ); + }, ), - Offstage( - offstage: !_isThemeEditorVisible, - child: ThemeEditorWidget( - onClose: () { - setState(() => _isThemeEditorVisible = false); - }, - ), + ), + + // ======================= THEME EDITOR VIEW ======================= + Offstage( + offstage: !_isThemeEditorVisible, + child: ThemeEditorWidget( + onClose: () { + setState(() => _isThemeEditorVisible = false); + }, ), - ], - )), + ), + ], + ), + ), ), ), ), ); } - // ==================== CONTINUE EXISTING CODE ===================== + // ==================== EXISTING CODE (UNCHANGED) ===================== + Widget _switchTenantRow() { final TenantSwitchController tenantSwitchController = Get.put(TenantSwitchController()); @@ -231,17 +250,25 @@ class _UserProfileBarState extends State child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ); + // ⭐ FIXED β€” YOUR ORIGINAL INTENT, COMPLETED PROPERLY Widget _noTenantContainer() => Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue.shade200, width: 1), + border: Border.all( + color: Colors.blue.shade200, + width: 1, + ), ), - child: MyText.bodyMedium( - "No tenants available", - color: Colors.blueAccent, - fontWeight: 600, + child: const Center( + child: Text( + "No organizations available", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), ), ); @@ -443,24 +470,20 @@ class _UserProfileBarState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Top icon Icon(LucideIcons.log_out, size: 56, color: primaryColor), MySpacing.height(18), - // Title MyText.titleLarge( "Logout Confirmation", fontWeight: 700, textAlign: TextAlign.center, ), MySpacing.height(14), - // Subtitle MyText.bodyMedium( "Are you sure you want to logout?\nYou will need to login again to continue.", color: Colors.grey[700], textAlign: TextAlign.center, ), MySpacing.height(30), - // Buttons Row( children: [ Expanded( diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index 50bdee7..3245f1e 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State 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(); }); @@ -42,35 +42,29 @@ class _ServiceProjectScreenState extends State Widget _buildProjectCard(ProjectItem project) { return Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shadowColor: Colors.indigo.withOpacity(0.10), color: Colors.white, child: InkWell( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(5), onTap: () { - // Navigate to ServiceProjectDetailsScreen Get.to(() => ServiceProjectDetailsScreen(projectId: project.id)); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// Project Header Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleMedium( - project.name, - fontWeight: 700, - ), - MySpacing.height(4), - ], + child: MyText.titleMedium( + project.name, + fontWeight: 700, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), if (project.status?.status.isNotEmpty ?? false) @@ -89,47 +83,32 @@ class _ServiceProjectScreenState extends State ), ], ), - - MySpacing.height(10), - - /// Assigned Date + MySpacing.height(8), _buildDetailRow( Icons.date_range_outlined, Colors.teal, "Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}", - fontSize: 13, ), - - MySpacing.height(8), - - /// Client Info + MySpacing.height(6), if (project.client != null) _buildDetailRow( Icons.account_circle_outlined, Colors.indigo, "Client: ${project.client!.name} (${project.client!.contactPerson})", - fontSize: 13, ), - - MySpacing.height(8), - - /// Contact Info + MySpacing.height(6), _buildDetailRow( Icons.phone, Colors.green, "Contact: ${project.contactName} (${project.contactPhone})", - fontSize: 13, ), - - MySpacing.height(12), - - /// Services List + MySpacing.height(10), if (project.services.isNotEmpty) Wrap( spacing: 6, runSpacing: 4, children: project.services - .map((service) => _buildServiceChip(service.name)) + .map((e) => _buildServiceChip(e.name)) .toList(), ), ], @@ -145,7 +124,7 @@ class _ServiceProjectScreenState extends State color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: MyText.labelSmall( name, color: Colors.orange[800], @@ -154,19 +133,18 @@ class _ServiceProjectScreenState extends State ); } - Widget _buildDetailRow(IconData icon, Color iconColor, String value, - {double fontSize = 12}) { + Widget _buildDetailRow(IconData icon, Color color, String value) { return Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(icon, size: 18, color: iconColor), + Icon(icon, size: 18, color: color), MySpacing.width(8), - Flexible( + Expanded( child: MyText.bodySmall( value, - color: Colors.grey[900], + maxLines: 2, + overflow: TextOverflow.ellipsis, fontWeight: 500, - fontSize: fontSize, + color: Colors.grey[900], ), ), ], @@ -181,7 +159,7 @@ class _ServiceProjectScreenState extends State const Icon(Icons.work_outline, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium('No matching projects found.', - fontWeight: 600, color: Colors.grey), + color: Colors.grey, fontWeight: 600), MySpacing.height(10), MyText.bodySmall('Try adjusting your filters or refresh.', color: Colors.grey), @@ -192,146 +170,150 @@ class _ServiceProjectScreenState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: CustomAppBar( - title: "Service Projects", - onBackPressed: () => Get.toNamed('/dashboard'), - ), - body: Column( - children: [ - /// Search bar and actions - Padding( - padding: MySpacing.xy(8, 8), - child: Row( + return SafeArea( + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: "Service Projects", + onBackPressed: () => Get.toNamed('/dashboard'), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Column( 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( - valueListenable: searchController, - builder: (context, value, _) { - if (value.text.isEmpty) { - return const SizedBox.shrink(); - } - return IconButton( - icon: const Icon(Icons.clear, + /// SEARCH BAR AREA + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 38, + child: TextField( + controller: searchController, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: const Icon(Icons.search, 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), - ), - ), - ), - ), - ), - MySpacing.width(8), - Container( - height: 35, - width: 35, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(5), - ), - child: IconButton( - icon: - const Icon(Icons.tune, size: 20, color: Colors.black87), - onPressed: () { - // TODO: Open filter bottom sheet - }, - ), - ), - MySpacing.width(10), - Container( - height: 35, - width: 35, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(5), - ), - child: PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.more_vert, - size: 20, color: Colors.black87), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5)), - itemBuilder: (context) => [ - const PopupMenuItem( - enabled: false, - height: 30, - child: Text( - "Actions", - style: TextStyle( - fontWeight: FontWeight.bold, color: Colors.grey), - ), - ), - const PopupMenuItem( - value: 1, - child: Row( - children: [ - SizedBox(width: 10), - Expanded(child: Text("Manage Projects")), - Icon(Icons.chevron_right, - size: 20, color: Colors.indigo), - ], + suffixIcon: + ValueListenableBuilder( + valueListenable: searchController, + builder: (context, value, _) { + return value.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, + size: 20, color: Colors.grey), + onPressed: () { + searchController.clear(); + controller.updateSearch(''); + }, + ) + : const SizedBox.shrink(); + }, + ), + hintText: 'Search projects...', + fillColor: Colors.white, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: + BorderSide(color: Colors.grey.shade300), + ), + ), + ), ), ), + MySpacing.width(8), + + /// FILTER BUTTON + _roundIconButton(Icons.tune), + + MySpacing.width(8), + + /// ACTION MENU + _roundMenuButton(), ], ), ), + + /// LIST AREA + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final projects = controller.filteredProjects; + + return MyRefreshIndicator( + onRefresh: _refreshProjects, + color: Colors.white, + backgroundColor: Colors.indigo, + child: projects.isEmpty + ? _buildEmptyState() + : ListView.separated( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 4, bottom: 20), + itemCount: projects.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, index) => + _buildProjectCard(projects[index]), + ), + ); + }), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _roundIconButton(IconData icon) { + return Container( + height: 38, + width: 38, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.grey.shade300), + ), + child: Icon(icon, size: 20, color: Colors.black87), + ); + } + + Widget _roundMenuButton() { + return Container( + height: 38, + width: 38, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + itemBuilder: (context) => [ + const PopupMenuItem( + enabled: false, + height: 30, + child: Text("Actions", + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)), + ), + const PopupMenuItem( + value: 1, + child: Row( + children: [ + Expanded(child: Text("Manage Projects")), + Icon(Icons.chevron_right, size: 20, color: Colors.indigo), ], ), ), - - /// Project List - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - - 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]), - ), - ); - }), - ), ], ), ); From abbe3f3896b68cfedb3ec912fb5e47b6399d72a8 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:13:44 +0530 Subject: [PATCH 03/37] added multiselect bottom sheet in assign task bottom sheet form --- .../assign_task_bottom_sheet .dart | 197 +++++++++++------- lib/model/employees/employee_model.dart | 2 +- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 4db2228..fdfcced 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -13,6 +13,10 @@ import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; +// Added imports for employee selection +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; final String activityName; @@ -43,7 +47,8 @@ class _AssignTaskBottomSheetState extends State { final DailyTaskPlanningController controller = Get.find(); final ProjectController projectController = Get.find(); - final OrganizationController orgController = Get.put(OrganizationController()); + final OrganizationController orgController = + Get.put(OrganizationController()); final ServiceController serviceController = Get.put(ServiceController()); final TextEditingController targetController = TextEditingController(); @@ -79,7 +84,8 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, serviceId: selectedService?.id); + await controller.fetchTaskData(selectedProjectId, + serviceId: selectedService?.id); } @override @@ -142,10 +148,11 @@ class _AssignTaskBottomSheetState extends State { const Divider(), // Pending Task Info - _infoRow(Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), + _infoRow( + Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector + // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, child: Row( @@ -158,18 +165,52 @@ class _AssignTaskBottomSheetState extends State { ), MySpacing.height(8), - // Employee List - Container( - constraints: const BoxConstraints(maxHeight: 180), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), + // ------------------------------- + // Employee selector (REPLACED) + // ------------------------------- + // We show a button-like container (with border) that opens the reusable + // EmployeeSelectionBottomSheet. Selected employees are reflected using + // existing controller.uploadingStates & controller.selectedEmployees. + GestureDetector( + onTap: _openEmployeeSelectionSheet, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText("Select team members", + color: Colors.grey.shade700); + } + // show summary text when there are selected employees + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), ), - child: _buildEmployeeList(), ), MySpacing.height(8), - // Selected Employees Chips + // Selected Employees Chips (keeps existing behavior) _buildSelectedEmployees(), MySpacing.height(8), @@ -198,7 +239,8 @@ class _AssignTaskBottomSheetState extends State { } void _onRoleMenuPressed() { - final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; final Size screenSize = overlay.size; showMenu( @@ -219,63 +261,12 @@ class _AssignTaskBottomSheetState extends State { }), ], ).then((value) { - if (value != null) controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) + controller.onRoleSelected(value == 'all' ? null : value); }); } - Widget _buildEmployeeList() { - return Obx(() { - if (controller.isFetchingEmployees.value) { - return Center(child: CircularProgressIndicator()); - } - - final filteredEmployees = controller.selectedRoleId.value == null - ? controller.employees - : controller.employees - .where((e) => e.jobRoleID.toString() == controller.selectedRoleId.value) - .toList(); - - if (filteredEmployees.isEmpty) { - return Center(child: Text("No employees available for selected role.")); - } - - return Scrollbar( - controller: _employeeListScrollController, - thumbVisibility: true, - child: ListView.builder( - controller: _employeeListScrollController, - itemCount: filteredEmployees.length, - itemBuilder: (context, index) { - final employee = filteredEmployees[index]; - final rxBool = controller.uploadingStates[employee.id]; - - return Obx(() => ListTile( - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - leading: Checkbox( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - value: rxBool?.value ?? false, - onChanged: (selected) { - if (rxBool != null) { - rxBool.value = selected ?? false; - controller.updateSelectedEmployees(); - } - }, - fillColor: MaterialStateProperty.resolveWith((states) => - states.contains(MaterialState.selected) - ? const Color.fromARGB(255, 95, 132, 255) - : Colors.transparent), - checkColor: Colors.white, - side: const BorderSide(color: Colors.black), - ), - title: Text(employee.name, style: const TextStyle(fontSize: 14)), - visualDensity: VisualDensity.compact, - )); - }, - ), - ); - }); - } + // Removed old inline employee list; selection handled by bottom sheet. Widget _buildSelectedEmployees() { return Obx(() { @@ -329,9 +320,12 @@ class _AssignTaskBottomSheetState extends State { decoration: InputDecoration( hintText: hintText, border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), - validator: (value) => this.controller.formFieldValidator(value, fieldType: validatorType), + validator: (value) => this + .controller + .formFieldValidator(value, fieldType: validatorType), ), ], ); @@ -350,9 +344,11 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", fontWeight: 600, color: Colors.black), + child: MyText.titleMedium("$title: ", + fontWeight: 600, color: Colors.black), ), - TextSpan(text: value, style: const TextStyle(color: Colors.black)), + TextSpan( + text: value, style: const TextStyle(color: Colors.black)), ], ), ), @@ -362,6 +358,50 @@ class _AssignTaskBottomSheetState extends State { ); } + Future _openEmployeeSelectionSheet() async { + // Open the existing EmployeeSelectionBottomSheet + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedEmployees.toList(), + multipleSelection: true, + title: 'Select Team Members', + ), + ); + + if (result == null) return; + + // Merge returned employees into controller.uploadingStates & controller.selectedEmployees + // 1) Reset all uploadingStates to false, then set true for selected + controller.uploadingStates.forEach((key, rx) { + rx.value = false; + }); + + for (final emp in result) { + final idStr = emp.id.toString(); + if (controller.uploadingStates.containsKey(idStr)) { + controller.uploadingStates[idStr]?.value = true; + } else { + // if uploadingStates doesn't have the id yet, add it (safe fallback) + controller.uploadingStates[idStr] = RxBool(true); + } + } + + // 2) Update selectedEmployees list in controller + controller.selectedEmployees.assignAll(result); + + // 3) Call controller helper (keeps existing behavior) + try { + controller.updateSelectedEmployees(); + } catch (_) { + // If controller does not implement updateSelectedEmployees, ignore. + } + } + void _onAssignTaskPressed() { final selectedTeam = controller.uploadingStates.entries .where((e) => e.value.value) @@ -369,13 +409,19 @@ class _AssignTaskBottomSheetState extends State { .toList(); if (selectedTeam.isEmpty) { - showAppSnackbar(title: "Team Required", message: "Please select at least one team member", type: SnackbarType.error); + showAppSnackbar( + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { - showAppSnackbar(title: "Invalid Input", message: "Please enter a valid target number", type: SnackbarType.error); + showAppSnackbar( + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error); return; } @@ -390,7 +436,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { - showAppSnackbar(title: "Description Required", message: "Please enter a description", type: SnackbarType.error); + showAppSnackbar( + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error); return; } diff --git a/lib/model/employees/employee_model.dart b/lib/model/employees/employee_model.dart index d774f23..0abc764 100644 --- a/lib/model/employees/employee_model.dart +++ b/lib/model/employees/employee_model.dart @@ -72,7 +72,7 @@ class EmployeeModel { }; } - /// βœ… Add equality based on unique `id` + ///Equality based on unique `id` β€” required for multi-selection to work @override bool operator ==(Object other) { if (identical(this, other)) return true; From 6515a511ee04d191d3ba0555d94aceec1585a14e Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:49:26 +0530 Subject: [PATCH 04/37] done with employee list dropdown updation --- .../multiple_select_bottomsheet.dart | 139 +++++++++++------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 336bb1a..0e11530 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; @@ -24,33 +25,67 @@ class EmployeeSelectionBottomSheet extends StatefulWidget { class _EmployeeSelectionBottomSheetState extends State { final TextEditingController _searchController = TextEditingController(); + final RxBool _isSearching = false.obs; - final RxList _searchResults = [].obs; + final RxList _allResults = [].obs; + late RxList _selectedEmployees; + Timer? _debounce; + @override void initState() { super.initState(); _selectedEmployees = RxList.from(widget.initiallySelected); - _searchEmployees(''); + + _performSearch(''); } @override void dispose() { + _debounce?.cancel(); _searchController.dispose(); super.dispose(); } - Future _searchEmployees(String query) async { + // ------------------------------------------------------ + // πŸ”₯ Optimized debounce-based search + // ------------------------------------------------------ + void _onSearchChanged(String query) { + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 300), () { + _performSearch(query.trim()); + }); + } + + Future _performSearch(String query) async { _isSearching.value = true; + final data = await ApiService.searchEmployeesBasic(searchString: query); + final results = (data as List) .map((e) => EmployeeModel.fromJson(e as Map)) .toList(); - _searchResults.assignAll(results); + + // ------------------------------------------------------ + // πŸ”₯ Auto-move selected employees to top + // ------------------------------------------------------ + results.sort((a, b) { + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; + + if (aSel != bSel) return aSel.compareTo(bSel); + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _allResults.assignAll(results); + _isSearching.value = false; } + // ------------------------------------------------------ + // Handle tap & checkbox + // ------------------------------------------------------ void _toggleEmployee(EmployeeModel emp) { if (widget.multipleSelection) { if (_selectedEmployees.contains(emp)) { @@ -61,9 +96,14 @@ class _EmployeeSelectionBottomSheetState } else { _selectedEmployees.assignAll([emp]); } - _selectedEmployees.refresh(); // important for Obx rebuild + + // Re-sort list after each toggle + _performSearch(_searchController.text.trim()); } + // ------------------------------------------------------ + // Submit selection + // ------------------------------------------------------ void _handleSubmit() { if (widget.multipleSelection) { Navigator.of(context).pop(_selectedEmployees.toList()); @@ -73,11 +113,14 @@ class _EmployeeSelectionBottomSheetState } } + // ------------------------------------------------------ + // Search bar widget + // ------------------------------------------------------ Widget _searchBar() => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: TextField( controller: _searchController, - onChanged: _searchEmployees, + onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Search employees...', filled: true, @@ -88,7 +131,7 @@ class _EmployeeSelectionBottomSheetState icon: const Icon(Icons.close, color: Colors.grey), onPressed: () { _searchController.clear(); - _searchEmployees(''); + _performSearch(''); }, ) : null, @@ -102,60 +145,52 @@ class _EmployeeSelectionBottomSheetState ), ); + // ------------------------------------------------------ + // Employee list (optimized) + // ------------------------------------------------------ Widget _employeeList() => Expanded( child: Obx(() { - if (_isSearching.value) { - return const Center(child: CircularProgressIndicator()); - } - - if (_searchResults.isEmpty) { - return const Center(child: Text("No employees found")); - } + final results = _allResults; return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: _searchResults.length, + itemCount: results.length, itemBuilder: (context, index) { - final emp = _searchResults[index]; + final emp = results[index]; + final isSelected = _selectedEmployees.contains(emp); - return Obx(() { - final isSelected = _selectedEmployees.contains(emp); - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blueAccent, - child: Text( - (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') - .toUpperCase(), - style: const TextStyle(color: Colors.white), - ), + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') + .toUpperCase(), + style: const TextStyle(color: Colors.white), ), - title: Text('${emp.firstName} ${emp.lastName}'), - subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) { - FocusScope.of(context).unfocus(); // hide keyboard - _toggleEmployee(emp); - }, - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), + ), + title: Text('${emp.firstName} ${emp.lastName}'), + subtitle: Text(emp.email), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, ), - onTap: () { - FocusScope.of(context).unfocus(); - _toggleEmployee(emp); - }, - contentPadding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - ); - }); + ), + onTap: () => _toggleEmployee(emp), + contentPadding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); }, ); }), ); + // ------------------------------------------------------ + // Build bottom sheet + // ------------------------------------------------------ @override Widget build(BuildContext context) { return BaseBottomSheet( @@ -164,10 +199,12 @@ class _EmployeeSelectionBottomSheetState onSubmit: _handleSubmit, child: SizedBox( height: MediaQuery.of(context).size.height * 0.7, - child: Column(children: [ - _searchBar(), - _employeeList(), - ]), + child: Column( + children: [ + _searchBar(), + _employeeList(), + ], + ), ), ); } From aa846f1a55ffc9a5a3770d310d9a8a2ca4f1fabe Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 11:57:13 +0530 Subject: [PATCH 05/37] advance paymen --- lib/view/finance/advance_payment_screen.dart | 72 ++++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 5cf3edc..2ca1799 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -35,8 +35,8 @@ class _AdvancePaymentScreenState extends State } }); - controller.searchQuery.listen((q) { - if (_searchCtrl.text != q) _searchCtrl.text = q; + _searchCtrl.addListener(() { + controller.searchQuery.value = _searchCtrl.text.trim(); }); } @@ -58,19 +58,25 @@ class _AdvancePaymentScreenState extends State final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: RefreshIndicator( - onRefresh: () async { - final emp = controller.selectedEmployee.value; - if (emp != null) { - await controller.fetchAdvancePayments(emp.id.toString()); + return 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: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); } }, - color: Colors.white, - backgroundColor: contentTheme.primary, - strokeWidth: 2.5, - displacement: 60, // ---------------- PORTRAIT (UNCHANGED) ---------------- child: !isLandscape @@ -195,7 +201,13 @@ class _AdvancePaymentScreenState extends State child: TextField( controller: _searchCtrl, focusNode: _searchFocus, - onChanged: (v) => controller.searchQuery.value = v.trim(), + onTap: () { + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted) { + FocusScope.of(context).requestFocus(_searchFocus); + } + }); + }, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), @@ -269,8 +281,12 @@ class _AdvancePaymentScreenState extends State return InkWell( onTap: () { controller.selectEmployee(e); - _searchCtrl.text = e.name; - controller.searchQuery.value = e.name; + _searchCtrl + ..text = e.name + ..selection = TextSelection.fromPosition( + TextPosition(offset: e.name.length), + ); + FocusScope.of(context).unfocus(); SystemChannels.textInput.invokeMethod('TextInput.hide'); controller.employees.clear(); @@ -397,8 +413,8 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - final formattedTime = - parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; + // final formattedTime = + // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; final project = item.name ?? ''; final desc = item.title ?? ''; @@ -429,16 +445,16 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - if (formattedTime.isNotEmpty) ...[ - const SizedBox(width: 6), - Text( - formattedTime, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade500, - fontStyle: FontStyle.italic), - ), - ] + // if (formattedTime.isNotEmpty) ...[ + // const SizedBox(width: 6), + // Text( + // formattedTime, + // style: TextStyle( + // fontSize: 12, + // color: Colors.grey.shade500, + // fontStyle: FontStyle.italic), + // ), + // ] ], ), const SizedBox(height: 4), From a03876bdb742c53479551945bbc7898822f955cc Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 12:00:28 +0530 Subject: [PATCH 06/37] unwanted code removed --- lib/view/finance/advance_payment_screen.dart | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 2ca1799..3b12431 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -413,9 +413,7 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - // final formattedTime = - // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; - + final project = item.name ?? ''; final desc = item.title ?? ''; final amount = (item.amount ?? 0).toDouble(); @@ -445,16 +443,7 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - // if (formattedTime.isNotEmpty) ...[ - // const SizedBox(width: 6), - // Text( - // formattedTime, - // style: TextStyle( - // fontSize: 12, - // color: Colors.grey.shade500, - // fontStyle: FontStyle.italic), - // ), - // ] + ], ), const SizedBox(height: 4), From 542f27635a0f5293d35411c5de902004b39adf01 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:04:40 +0530 Subject: [PATCH 07/37] upadted with select enployee sheet --- .../expense/add_expense_controller.dart | 15 +- .../add_payment_request_controller.dart | 15 +- .../assign_task_bottom_sheet .dart | 246 +++++++++--------- .../directory/edit_bucket_bottom_sheet.dart | 181 +++++-------- .../multiple_select_bottomsheet.dart | 30 ++- .../multiple_select_role_bottomsheet.dart | 244 +++++++++++++++++ .../expense/add_expense_bottom_sheet.dart | 61 +++-- .../add_payment_request_bottom_sheet.dart | 108 ++++---- .../payment_request_filter_bottom_sheet.dart | 8 +- .../expense/expense_filter_bottom_sheet.dart | 10 +- 10 files changed, 577 insertions(+), 341 deletions(-) create mode 100644 lib/model/employees/multiple_select_role_bottomsheet.dart diff --git a/lib/controller/expense/add_expense_controller.dart b/lib/controller/expense/add_expense_controller.dart index 11ce63d..cfeed4d 100644 --- a/lib/controller/expense/add_expense_controller.dart +++ b/lib/controller/expense/add_expense_controller.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -50,10 +51,22 @@ class AddExpenseController extends GetxController { final isEditMode = false.obs; final isSearchingEmployees = false.obs; +// --- Paid By (Single + Multi Selection Support) --- + +// single selection + final selectedPaidBy = Rxn(); + + + +// helper setters + void setSelectedPaidBy(EmployeeModel? emp) { + selectedPaidBy.value = emp; + } + // --- Dropdown Selections & Data --- final selectedPaymentMode = Rxn(); final selectedExpenseType = Rxn(); - final selectedPaidBy = Rxn(); + // final selectedPaidBy = Rxn(); final selectedProject = ''.obs; final selectedTransactionDate = Rxn(); diff --git a/lib/controller/finance/add_payment_request_controller.dart b/lib/controller/finance/add_payment_request_controller.dart index 1ff0a4e..f05ff45 100644 --- a/lib/controller/finance/add_payment_request_controller.dart +++ b/lib/controller/finance/add_payment_request_controller.dart @@ -14,6 +14,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; import 'package:marco/model/finance/expense_category_model.dart'; import 'package:marco/model/finance/currency_list_model.dart'; +import 'package:marco/model/employees/employee_model.dart'; class AddPaymentRequestController extends GetxController { // Loading States @@ -32,7 +33,7 @@ class AddPaymentRequestController extends GetxController { // Selected Values final selectedProject = Rx?>(null); final selectedCategory = Rx(null); - final selectedPayee = ''.obs; + final selectedPayee = Rx(null); final selectedCurrency = Rx(null); final isAdvancePayment = false.obs; final selectedDueDate = Rx(null); @@ -161,7 +162,7 @@ class AddPaymentRequestController extends GetxController { try { final pickedFile = await _picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { - isProcessingAttachment.value = true; + isProcessingAttachment.value = true; File imageFile = File(pickedFile.path); // Add timestamp to the captured image @@ -184,7 +185,7 @@ class AddPaymentRequestController extends GetxController { selectedProject.value = project; void selectCategory(ExpenseCategory category) => selectedCategory.value = category; - void selectPayee(String payee) => selectedPayee.value = payee; + void selectPayee(EmployeeModel payee) => selectedPayee.value = payee; void selectCurrency(Currency currency) => selectedCurrency.value = currency; void addAttachment(File file) => attachments.add(file); @@ -268,7 +269,7 @@ class AddPaymentRequestController extends GetxController { "amount": double.tryParse(amountController.text.trim()) ?? 0, "currencyId": selectedCurrency.value?.id ?? '', "description": descriptionController.text.trim(), - "payee": selectedPayee.value, + "payee": selectedPayee.value?.id ?? "", "dueDate": selectedDueDate.value?.toIso8601String(), "isAdvancePayment": isAdvancePayment.value, "billAttachments": billAttachments.map((a) { @@ -337,7 +338,7 @@ class AddPaymentRequestController extends GetxController { "amount": double.tryParse(amountController.text.trim()) ?? 0, "currencyId": selectedCurrency.value?.id ?? '', "description": descriptionController.text.trim(), - "payee": selectedPayee.value, + "payee": selectedPayee.value?.id ?? "", "dueDate": selectedDueDate.value?.toIso8601String(), "isAdvancePayment": isAdvancePayment.value, "billAttachments": billAttachments.map((a) { @@ -388,7 +389,7 @@ class AddPaymentRequestController extends GetxController { return _errorSnackbar("Please select a project"); if (selectedCategory.value == null) return _errorSnackbar("Please select a category"); - if (selectedPayee.value.isEmpty) + if (selectedPayee.value == null) return _errorSnackbar("Please select a payee"); if (selectedCurrency.value == null) return _errorSnackbar("Please select currency"); @@ -408,7 +409,7 @@ class AddPaymentRequestController extends GetxController { descriptionController.clear(); selectedProject.value = null; selectedCategory.value = null; - selectedPayee.value = ''; + selectedPayee.value = null; selectedCurrency.value = null; isAdvancePayment.value = false; attachments.clear(); diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index fdfcced..5191079 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -1,3 +1,6 @@ +// Updated AssignTaskBottomSheet with bottom sheet height fix +// Only modified layout for employee selection area to prevent overflow. + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; @@ -12,10 +15,8 @@ import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; - -// Added imports for employee selection import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/multiple_select_role_bottomsheet.dart'; class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; @@ -53,9 +54,9 @@ class _AssignTaskBottomSheetState extends State { final TextEditingController targetController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); - final ScrollController _employeeListScrollController = ScrollController(); String? selectedProjectId; + String? selectedRoleId; Organization? selectedOrganization; Service? selectedService; @@ -84,13 +85,14 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, - serviceId: selectedService?.id); + await controller.fetchTaskData( + selectedProjectId, + serviceId: selectedService?.id, + ); } @override void dispose() { - _employeeListScrollController.dispose(); targetController.dispose(); descriptionController.dispose(); super.dispose(); @@ -98,20 +100,21 @@ class _AssignTaskBottomSheetState extends State { @override Widget build(BuildContext context) { - return Obx(() => BaseBottomSheet( - title: "Assign Task", - child: _buildAssignTaskForm(), - onCancel: () => Get.back(), - onSubmit: _onAssignTaskPressed, - isSubmitting: controller.isAssigningTask.value, - )); + return Obx( + () => BaseBottomSheet( + title: "Assign Task", + child: _buildAssignTaskForm(), + onCancel: () => Get.back(), + onSubmit: _onAssignTaskPressed, + isSubmitting: controller.isAssigningTask.value, + ), + ); } Widget _buildAssignTaskForm() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Organization Selector SizedBox( height: 50, child: OrganizationSelector( @@ -123,9 +126,9 @@ class _AssignTaskBottomSheetState extends State { }, ), ), + MySpacing.height(12), - // Service Selector SizedBox( height: 50, child: ServiceSelector( @@ -137,40 +140,27 @@ class _AssignTaskBottomSheetState extends State { }, ), ), + MySpacing.height(16), - - // Work Location Info - _infoRow( - Icons.location_on, - "Work Location", - "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}", - ), + _infoRow(Icons.location_on, "Work Location", + "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), const Divider(), - - // Pending Task Info _infoRow( Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, - child: Row( - children: [ - MyText.titleMedium("Select Team :", fontWeight: 600), - const SizedBox(width: 4), - const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)), - ], - ), + child: Row(children: [ + MyText.titleMedium("Select Team :", fontWeight: 600), + const SizedBox(width: 4), + const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)), + ]), ), + MySpacing.height(8), - // ------------------------------- - // Employee selector (REPLACED) - // ------------------------------- - // We show a button-like container (with border) that opens the reusable - // EmployeeSelectionBottomSheet. Selected employees are reflected using - // existing controller.uploadingStates & controller.selectedEmployees. + /// TEAM SELECT BOX GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( @@ -180,41 +170,47 @@ class _AssignTaskBottomSheetState extends State { borderRadius: BorderRadius.circular(6), ), child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText("Select team members", - color: Colors.grey.shade700); - } - // show summary text when there are selected employees - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - ), + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], +) + ), ), - MySpacing.height(8), - // Selected Employees Chips (keeps existing behavior) + MySpacing.height(8), _buildSelectedEmployees(), MySpacing.height(8), - // Target Input _buildTextField( icon: Icons.track_changes, label: "Target for Today :", @@ -223,9 +219,9 @@ class _AssignTaskBottomSheetState extends State { keyboardType: const TextInputType.numberWithOptions(decimal: true), validatorType: "target", ), + MySpacing.height(16), - // Description Input _buildTextField( icon: Icons.description, label: "Description :", @@ -253,21 +249,21 @@ class _AssignTaskBottomSheetState extends State { ), items: [ const PopupMenuItem(value: 'all', child: Text("All Roles")), - ...controller.roles.map((role) { - return PopupMenuItem( + ...controller.roles.map( + (role) => PopupMenuItem( value: role['id'].toString(), child: Text(role['name'] ?? 'Unknown Role'), - ); - }), + ), + ), ], ).then((value) { - if (value != null) - controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) { + selectedRoleId = value == 'all' ? null : value; + controller.onRoleSelected(selectedRoleId); + } }); } - // Removed old inline employee list; selection handled by bottom sheet. - Widget _buildSelectedEmployees() { return Obx(() { if (controller.selectedEmployees.isEmpty) return Container(); @@ -319,7 +315,9 @@ class _AssignTaskBottomSheetState extends State { maxLines: maxLines, decoration: InputDecoration( hintText: hintText, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), @@ -344,61 +342,62 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", - fontWeight: 600, color: Colors.black), + child: MyText.titleMedium( + "$title: ", + fontWeight: 600, + color: Colors.black, + ), ), TextSpan( - text: value, style: const TextStyle(color: Colors.black)), + text: value, + style: const TextStyle(color: Colors.black), + ), ], ), ), - ), + ) ], ), ); } Future _openEmployeeSelectionSheet() async { - // Open the existing EmployeeSelectionBottomSheet final result = await showModalBottomSheet>( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectionBottomSheet( - initiallySelected: controller.selectedEmployees.toList(), - multipleSelection: true, - title: 'Select Team Members', - ), + 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, + ), + ); + }, + ); + }, ); - if (result == null) return; - - // Merge returned employees into controller.uploadingStates & controller.selectedEmployees - // 1) Reset all uploadingStates to false, then set true for selected - controller.uploadingStates.forEach((key, rx) { - rx.value = false; - }); - - for (final emp in result) { - final idStr = emp.id.toString(); - if (controller.uploadingStates.containsKey(idStr)) { - controller.uploadingStates[idStr]?.value = true; - } else { - // if uploadingStates doesn't have the id yet, add it (safe fallback) - controller.uploadingStates[idStr] = RxBool(true); - } - } - - // 2) Update selectedEmployees list in controller - controller.selectedEmployees.assignAll(result); - - // 3) Call controller helper (keeps existing behavior) - try { + if (result != null) { + controller.selectedEmployees.assignAll(result); controller.updateSelectedEmployees(); - } catch (_) { - // If controller does not implement updateSelectedEmployees, ignore. } } @@ -410,18 +409,20 @@ class _AssignTaskBottomSheetState extends State { if (selectedTeam.isEmpty) { showAppSnackbar( - title: "Team Required", - message: "Please select at least one team member", - type: SnackbarType.error); + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error, + ); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { showAppSnackbar( - title: "Invalid Input", - message: "Please enter a valid target number", - type: SnackbarType.error); + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error, + ); return; } @@ -437,9 +438,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { showAppSnackbar( - title: "Description Required", - message: "Please enter a description", - type: SnackbarType.error); + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error, + ); return; } diff --git a/lib/model/directory/edit_bucket_bottom_sheet.dart b/lib/model/directory/edit_bucket_bottom_sheet.dart index 7347b63..d115377 100644 --- a/lib/model/directory/edit_bucket_bottom_sheet.dart +++ b/lib/model/directory/edit_bucket_bottom_sheet.dart @@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/directory/contact_bucket_list_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class EditBucketBottomSheet { static void show( @@ -21,10 +22,8 @@ class EditBucketBottomSheet { final nameController = TextEditingController(text: bucket.name); final descController = TextEditingController(text: bucket.description); - final searchController = TextEditingController(); final selectedIds = RxSet({...bucket.employeeIds}); - final searchText = ''.obs; InputDecoration _inputDecoration(String label) { return InputDecoration( @@ -84,6 +83,15 @@ class EditBucketBottomSheet { } } + Future _handleSubmitBottomSheet(BuildContext sheetContext) async { + await _handleSubmit(); + + // close bottom sheet safely + if (Navigator.of(sheetContext).canPop()) { + Navigator.of(sheetContext).pop(); + } + } + Widget _formContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -101,117 +109,72 @@ class EditBucketBottomSheet { MySpacing.height(20), MyText.labelLarge('Shared With', fontWeight: 600), MySpacing.height(8), - Obx(() => TextField( - controller: searchController, - onChanged: (value) => searchText.value = value.toLowerCase(), - decoration: InputDecoration( - hintText: 'Search employee...', - prefixIcon: const Icon(Icons.search, size: 20), - suffixIcon: searchText.value.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, size: 18), - onPressed: () { - searchController.clear(); - searchText.value = ''; - }, - ) - : null, - isDense: true, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - filled: true, - fillColor: Colors.grey.shade100, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), - )), - MySpacing.height(8), Obx(() { - final filtered = allEmployees.where((emp) { - final fullName = '${emp.firstName} ${emp.lastName}'.toLowerCase(); - return fullName.contains(searchText.value); - }).toList(); + if (selectedIds.isEmpty) return const SizedBox.shrink(); - return SizedBox( - height: 180, - child: ListView.separated( - itemCount: filtered.length, - separatorBuilder: (_, __) => const SizedBox(height: 2), - itemBuilder: (context, index) { - final emp = filtered[index]; - final fullName = '${emp.firstName} ${emp.lastName}'.trim(); + final selectedEmployees = + allEmployees.where((e) => selectedIds.contains(e.id)).toList(); - return Obx(() => Theme( - data: Theme.of(context).copyWith( - unselectedWidgetColor: Colors.grey.shade500, - checkboxTheme: CheckboxThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4)), - side: const BorderSide(color: Colors.grey), - fillColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; - } - return Colors.white; - }), - checkColor: MaterialStateProperty.all(Colors.white), - ), - ), - child: CheckboxListTile( - dense: true, - contentPadding: EdgeInsets.zero, - visualDensity: const VisualDensity(vertical: -4), - controlAffinity: ListTileControlAffinity.leading, - value: selectedIds.contains(emp.id), - onChanged: emp.id == ownerId - ? null - : (val) { - if (val == true) { - selectedIds.add(emp.id); - } else { - selectedIds.remove(emp.id); - } - }, - title: Row( - children: [ - Expanded( - child: MyText.bodyMedium( - fullName.isNotEmpty ? fullName : 'Unnamed', - fontWeight: 600, - ), - ), - if (emp.id == ownerId) - Container( - margin: const EdgeInsets.only(left: 6), - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(4), - ), - child: MyText.labelSmall( - "Owner", - fontWeight: 600, - color: Colors.red, - ), - ), - ], - ), - subtitle: emp.jobRole.isNotEmpty - ? MyText.bodySmall( - emp.jobRole, - color: Colors.grey.shade600, - ) - : null, - ), - )); - }, - ), + return Wrap( + spacing: 8, + children: selectedEmployees.map((emp) { + final fullName = '${emp.firstName} ${emp.lastName}'.trim(); + + return Chip( + label: Text(fullName), + onDeleted: emp.id == ownerId + ? null + : () => selectedIds.remove(emp.id), + ); + }).toList(), ); }), + MySpacing.height(8), + +// --- Open new EmployeeSelectionBottomSheet --- + GestureDetector( + onTap: () async { + final initiallySelected = allEmployees + .where((e) => selectedIds.contains(e.id)) + .toList(); + + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(22)), + ), + builder: (_) => EmployeeSelectionBottomSheet( + initiallySelected: initiallySelected, + multipleSelection: true, + title: "Shared With", + ), + ); + + if (result != null) { + selectedIds + ..clear() + ..addAll(result.map((e) => e.id)); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: const [ + Icon(Icons.search, color: Colors.grey), + SizedBox(width: 8), + Expanded(child: Text("Search & Select Employees")), + ], + ), + ), + ), + MySpacing.height(8), + const SizedBox.shrink(), ], ); } @@ -224,7 +187,7 @@ class EditBucketBottomSheet { return BaseBottomSheet( title: "Edit Bucket", onCancel: () => Navigator.pop(context), - onSubmit: _handleSubmit, + onSubmit: () => _handleSubmitBottomSheet(context), child: _formContent(), ); }, diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 0e11530..f96e361 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -159,6 +159,26 @@ class _EmployeeSelectionBottomSheetState final emp = results[index]; final isSelected = _selectedEmployees.contains(emp); + Widget trailingWidget; + + if (widget.multipleSelection) { + // Multiple selection β†’ normal checkbox + trailingWidget = Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, + ), + ); + } else { + // Single selection β†’ check circle + trailingWidget = isSelected + ? const Icon(Icons.check_circle, color: Colors.blueAccent) + : const Icon(Icons.circle_outlined, color: Colors.grey); + } + return ListTile( leading: CircleAvatar( backgroundColor: Colors.blueAccent, @@ -170,15 +190,7 @@ class _EmployeeSelectionBottomSheetState ), title: Text('${emp.firstName} ${emp.lastName}'), subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) => _toggleEmployee(emp), - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), - ), + trailing: trailingWidget, onTap: () => _toggleEmployee(emp), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart new file mode 100644 index 0000000..1ffd1e0 --- /dev/null +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; + +class MultipleSelectRoleBottomSheet extends StatefulWidget { + final String title; + final bool multipleSelection; + + final String projectId; + final String? serviceId; + final String? organizationId; + final String? roleId; + final ScrollController? scrollController; + + final List initiallySelected; + + const MultipleSelectRoleBottomSheet({ + super.key, + this.title = "Select Employees", + this.multipleSelection = true, + required this.projectId, + this.serviceId, + this.organizationId, + this.roleId, + this.initiallySelected = const [], + this.scrollController, + }); + + @override + State createState() => + _MultipleSelectRoleBottomSheetState(); +} + +class _MultipleSelectRoleBottomSheetState + extends State { + final RxList _employees = [].obs; + final RxList _filtered = [].obs; + final RxBool _isLoading = true.obs; + + late RxList _selected; + final TextEditingController _searchController = TextEditingController(); + + late DailyTaskPlanningController controller; + + @override + void initState() { + super.initState(); + _selected = widget.initiallySelected.obs; + controller = Get.find(); + _fetchEmployeesFiltered(); + } + + Future _fetchEmployeesFiltered() async { + _isLoading.value = true; + try { + List employees = controller.employees.toList(); + + if (widget.roleId != null && widget.roleId!.isNotEmpty) { + employees = employees + .where((emp) => emp.jobRoleID == widget.roleId) + .toList(); + } + + // Selected first + employees.sort((a, b) { + final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; + final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; + return aSel != bSel + ? aSel.compareTo(bSel) + : a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _employees.assignAll(employees); + _filtered.assignAll(employees); + } catch (e) { + print("Error fetching employees: $e"); + } finally { + _isLoading.value = false; + } + } + + void _onSearch(String text) { + if (text.isEmpty) { + _filtered.assignAll(_employees); + } else { + _filtered.assignAll( + _employees.where((e) => + e.name.toLowerCase().contains(text.toLowerCase()) || + e.designation.toLowerCase().contains(text.toLowerCase())), + ); + } + + // Selected on top + _filtered.sort((a, b) { + final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; + final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; + return aSel != bSel + ? aSel.compareTo(bSel) + : a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + } + + void _onTap(EmployeeModel emp) { + if (widget.multipleSelection) { + if (_selected.any((e) => e.id == emp.id)) { + _selected.removeWhere((e) => e.id == emp.id); + } else { + _selected.add(emp); + } + } else { + _selected.assignAll([emp]); + Get.back(result: _selected); + } + + _onSearch(_searchController.text.trim()); + } + + bool _isSelected(EmployeeModel emp) { + return _selected.any((e) => e.id == emp.id); + } + + Widget _searchBar() => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextField( + controller: _searchController, + onChanged: _onSearch, + decoration: InputDecoration( + hintText: 'Search employees...', + filled: true, + fillColor: Colors.grey.shade100, + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: () { + _searchController.clear(); + _onSearch(''); + }, + ) + : null, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + ), + ); + + /// ⭐ NEW β€” Chips showing selected employees + Widget _selectedChips() { + return Obx(() { + if (_selected.isEmpty) return const SizedBox(); + + return Wrap( + spacing: 8, + runSpacing: 8, + children: _selected.map((emp) { + return Chip( + label: Text(emp.name), + deleteIcon: const Icon(Icons.close), + onDeleted: () { + _selected.remove(emp); + _onSearch(_searchController.text.trim()); + }, + backgroundColor: Colors.blue.shade50, + ); + }).toList(), + ); + }); + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: widget.title, + onCancel: () => Get.back(), + onSubmit: () => Get.back(result: _selected), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.55, + child: Column( + children: [ + _searchBar(), + + /// ⭐ Chips shown right below search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _selectedChips(), + ), + + const SizedBox(height: 6), + + Expanded( + child: Obx(() { + if (_isLoading.value) { + return SkeletonLoaders.employeeSkeletonCard(); + } + + if (_filtered.isEmpty) { + return const Center(child: Text("No employees found")); + } + + return ListView.builder( + controller: widget.scrollController, + padding: const EdgeInsets.only(bottom: 20), + itemCount: _filtered.length, + itemBuilder: (_, index) { + final emp = _filtered[index]; + final isSelected = _isSelected(emp); + + return ListTile( + onTap: () => _onTap(emp), + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + style: const TextStyle(color: Colors.white), + ), + ), + title: Text(emp.name), + subtitle: Text(emp.designation), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _onTap(emp), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + ); + }, + ); + }), + ), + ], + ), + ), + ); + } +} diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 317bf9b..ca714ba 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -5,13 +5,15 @@ import 'package:marco/controller/expense/add_expense_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; -import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; +// import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/validators.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; /// Show bottom sheet wrapper Future showAddExpenseBottomSheet({ @@ -52,24 +54,36 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> /// Show employee list Future _showEmployeeList() async { - await showModalBottomSheet( + final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (_) => ReusableEmployeeSelectorBottomSheet( - searchController: controller.employeeSearchController, - searchResults: controller.employeeSearchResults, - isSearching: controller.isSearchingEmployees, - onSearch: controller.searchEmployees, - onSelect: (emp) => controller.selectedPaidBy.value = emp, + builder: (_) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedPaidBy.value != null + ? [controller.selectedPaidBy.value!] + : [], + multipleSelection: false, + title: "Select Paid By", ), ); - controller.employeeSearchController.clear(); - controller.employeeSearchResults.clear(); + if (result == null) return; + + // result will be EmployeeModel or [EmployeeModel] + if (result is EmployeeModel) { + controller.setSelectedPaidBy(result); + } else if (result is List && result.isNotEmpty) { + controller.setSelectedPaidBy(result.first as EmployeeModel); + } + + // cleanup + try { + controller.employeeSearchController.clear(); + controller.employeeSearchResults.clear(); + } catch (_) {} } /// Generic option list @@ -343,23 +357,26 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> const SectionTitle( icon: Icons.person_outline, title: "Paid By", requiredField: true), MySpacing.height(6), + // Main tile: tap to choose mode + selection sheet GestureDetector( onTap: _showEmployeeList, child: TileContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - controller.selectedPaidBy.value == null - ? "Select Paid By" - : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', - style: const TextStyle(fontSize: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + controller.selectedPaidBy.value?.name ?? "Select Paid By", + style: TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, ), - const Icon(Icons.arrow_drop_down, size: 22), - ], - ), - ), + ), + Icon(Icons.arrow_drop_down, size: 22), + ], + )), ), + // small helper: long-press to quickly open multi-select directly (optional) + const SizedBox(height: 6), ], ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index abe1527..a23bfcf 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -9,6 +9,8 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; Future showPaymentRequestBottomSheet({ bool isEdit = false, @@ -206,7 +208,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ? null : "Enter valid amount"), _gap(), - _buildPayeeAutocompleteField(), + _buildPayeeField(), _gap(), _buildDropdown( "Currency", @@ -347,74 +349,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ); } - Widget _buildPayeeAutocompleteField() { + Widget _buildPayeeField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SectionTitle( - icon: Icons.person_outline, title: "Payee", requiredField: true), - const SizedBox(height: 6), - Autocomplete( - optionsBuilder: (textEditingValue) { - final query = textEditingValue.text.toLowerCase(); - return query.isEmpty - ? const Iterable.empty() - : controller.payees - .where((p) => p.toLowerCase().contains(query)); - }, - displayStringForOption: (option) => option, - fieldViewBuilder: - (context, fieldController, focusNode, onFieldSubmitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (fieldController.text != controller.selectedPayee.value) { - fieldController.text = controller.selectedPayee.value; - fieldController.selection = TextSelection.fromPosition( - TextPosition(offset: fieldController.text.length)); - } - }); - - return TextFormField( - controller: fieldController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: "Type or select payee", - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), + const SectionTitle( + icon: Icons.person_outline, + title: "Payee", + requiredField: true, + ), + MySpacing.height(6), + GestureDetector( + onTap: _showPayeeSelector, + child: TileContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Obx(() => Text( + controller.selectedPayee.value?.name ?? "Select Payee", + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + )), ), - ), - validator: (v) => - v == null || v.trim().isEmpty ? "Please enter payee" : null, - onChanged: (val) => controller.selectedPayee.value = val, - ); - }, - onSelected: (selection) => controller.selectedPayee.value = selection, - optionsViewBuilder: (context, onSelected, options) => Material( - color: Colors.white, - elevation: 4, - borderRadius: BorderRadius.circular(8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (_, index) => InkWell( - onTap: () => onSelected(options.elementAt(index)), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 12), - child: Text(options.elementAt(index), - style: const TextStyle(fontSize: 14)), - ), - ), - ), + const Icon(Icons.arrow_drop_down, size: 22), + ], ), ), ), + const SizedBox(height: 6), ], ); } @@ -533,7 +496,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (controller.selectedCategory.value == null) { return _showError("Please select a category"); } - if (controller.selectedPayee.value.isEmpty) { + if (controller.selectedPayee.value == null) { return _showError("Please select a payee"); } if (controller.selectedCurrency.value == null) { @@ -542,6 +505,25 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> return true; } + Future _showPayeeSelector() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EmployeeSelectionBottomSheet( + title: "Select Payee", + multipleSelection: false, + initiallySelected: controller.selectedPayee.value != null + ? [controller.selectedPayee.value!] + : [], + ), + ); + + if (result is EmployeeModel) { + controller.selectedPayee.value = result; + } + } + bool _showError(String msg) { showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error); return false; diff --git a/lib/model/finance/payment_request_filter_bottom_sheet.dart b/lib/model/finance/payment_request_filter_bottom_sheet.dart index 8d680b2..737495d 100644 --- a/lib/model/finance/payment_request_filter_bottom_sheet.dart +++ b/lib/model/finance/payment_request_filter_bottom_sheet.dart @@ -8,7 +8,7 @@ import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/date_range_picker.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class PaymentRequestFilterBottomSheet extends StatefulWidget { final PaymentRequestController controller; @@ -441,9 +441,9 @@ class _PaymentRequestFilterBottomSheetState shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: (query) => searchEmployees(query, items), + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: selectedEmployees.toList(), + multipleSelection: true, title: title, ), ); diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index 0c6d565..380aed2 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -8,9 +8,10 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/date_range_picker.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + class ExpenseFilterBottomSheet extends StatefulWidget { final ExpenseController expenseController; @@ -303,12 +304,13 @@ class _ExpenseFilterBottomSheetState extends State shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: searchEmployees, + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: selectedEmployees.toList(), + multipleSelection: true, title: title, ), ); + if (result != null) selectedEmployees.assignAll(result); }, child: Container( From 3e317328b2a35147c821896119d7c99744779b93 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:17:00 +0530 Subject: [PATCH 08/37] check box color change to white --- .../multiple_select_role_bottomsheet.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index 1ffd1e0..f7e5dff 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -59,9 +59,8 @@ class _MultipleSelectRoleBottomSheetState List employees = controller.employees.toList(); if (widget.roleId != null && widget.roleId!.isNotEmpty) { - employees = employees - .where((emp) => emp.jobRoleID == widget.roleId) - .toList(); + employees = + employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } // Selected first @@ -226,6 +225,16 @@ class _MultipleSelectRoleBottomSheetState trailing: Checkbox( value: isSelected, onChanged: (_) => _onTap(emp), + fillColor: + MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.blueAccent; // Selected color + } + return Colors.white; // Unselected square color + }), + checkColor: Colors.white, // Check mark color + side: const BorderSide( + color: Colors.grey), // Outline for unselected ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From 8cbefbb9e8093cbcc7301866f4a5fd43fac81735 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 17:36:56 +0530 Subject: [PATCH 09/37] UI enhancement --- .../assign_task_bottom_sheet .dart | 106 ++++++++---------- .../multiple_select_bottomsheet.dart | 13 ++- .../multiple_select_role_bottomsheet.dart | 49 ++------ 3 files changed, 63 insertions(+), 105 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 5191079..ce19774 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -164,47 +164,45 @@ class _AssignTaskBottomSheetState extends State { GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), - // Expanded name area - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText( - "Select team members", - color: Colors.grey.shade700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], -) - - ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + )), ), MySpacing.height(8), @@ -272,20 +270,14 @@ class _AssignTaskBottomSheetState extends State { spacing: 4, runSpacing: 4, children: controller.selectedEmployees.map((e) { - return Obx(() { - final isSelected = controller.uploadingStates[e.id]?.value ?? false; - if (!isSelected) return Container(); - - return Chip( - label: Text(e.name, style: const TextStyle(color: Colors.white)), - backgroundColor: const Color.fromARGB(255, 95, 132, 255), - deleteIcon: const Icon(Icons.close, color: Colors.white), - onDeleted: () { - controller.uploadingStates[e.id]?.value = false; - controller.updateSelectedEmployees(); - }, - ); - }); + return Chip( + label: Text(e.name, style: const TextStyle(color: Colors.white)), + backgroundColor: const Color.fromARGB(255, 95, 132, 255), + deleteIcon: const Icon(Icons.close, color: Colors.white), + onDeleted: () { + controller.selectedEmployees.remove(e); + }, + ); }).toList(), ); }); @@ -380,7 +372,6 @@ class _AssignTaskBottomSheetState extends State { color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - child: MultipleSelectRoleBottomSheet( projectId: selectedProjectId!, organizationId: selectedOrganization?.id, @@ -396,16 +387,13 @@ class _AssignTaskBottomSheetState extends State { ); if (result != null) { - controller.selectedEmployees.assignAll(result); - controller.updateSelectedEmployees(); + controller.selectedEmployees + .assignAll(result); // RxList updates UI automatically } } void _onAssignTaskPressed() { - final selectedTeam = controller.uploadingStates.entries - .where((e) => e.value.value) - .map((e) => e.key) - .toList(); + final selectedTeam = controller.selectedEmployees; if (selectedTeam.isEmpty) { showAppSnackbar( @@ -449,7 +437,7 @@ class _AssignTaskBottomSheetState extends State { workItemId: widget.workItemId, plannedTask: target.toInt(), description: description, - taskTeam: selectedTeam, + taskTeam: selectedTeam.map((e) => e.id).toList(), // pass IDs assignmentDate: widget.assignmentDate, organizationId: selectedOrganization?.id, serviceId: selectedService?.id, diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index f96e361..1509cf9 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -68,13 +68,18 @@ class _EmployeeSelectionBottomSheetState .toList(); // ------------------------------------------------------ - // πŸ”₯ Auto-move selected employees to top + // Auto-move selected employees to top // ------------------------------------------------------ results.sort((a, b) { - final aSel = _selectedEmployees.contains(a) ? 0 : 1; - final bSel = _selectedEmployees.contains(b) ? 0 : 1; + if (widget.multipleSelection) { + // Only move selected employees to top in multi-select + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; - if (aSel != bSel) return aSel.compareTo(bSel); + if (aSel != bSel) return aSel.compareTo(bSel); + } + + // Otherwise, keep original order (or alphabetically if needed) return a.name.toLowerCase().compareTo(b.name.toLowerCase()); }); diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index f7e5dff..a1f4951 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -63,7 +63,6 @@ class _MultipleSelectRoleBottomSheetState employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } - // Selected first employees.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -92,7 +91,6 @@ class _MultipleSelectRoleBottomSheetState ); } - // Selected on top _filtered.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -110,8 +108,8 @@ class _MultipleSelectRoleBottomSheetState _selected.add(emp); } } else { - _selected.assignAll([emp]); - Get.back(result: _selected); + // Single selection β†’ return immediately + Get.back(result: [emp]); } _onSearch(_searchController.text.trim()); @@ -150,49 +148,17 @@ class _MultipleSelectRoleBottomSheetState ), ); - /// ⭐ NEW β€” Chips showing selected employees - Widget _selectedChips() { - return Obx(() { - if (_selected.isEmpty) return const SizedBox(); - - return Wrap( - spacing: 8, - runSpacing: 8, - children: _selected.map((emp) { - return Chip( - label: Text(emp.name), - deleteIcon: const Icon(Icons.close), - onDeleted: () { - _selected.remove(emp); - _onSearch(_searchController.text.trim()); - }, - backgroundColor: Colors.blue.shade50, - ); - }).toList(), - ); - }); - } - @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Get.back(), - onSubmit: () => Get.back(result: _selected), + onSubmit: () => Get.back(result: _selected.toList()), // Return plain list child: SizedBox( height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ _searchBar(), - - /// ⭐ Chips shown right below search bar - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: _selectedChips(), - ), - - const SizedBox(height: 6), - Expanded( child: Obx(() { if (_isLoading.value) { @@ -228,13 +194,12 @@ class _MultipleSelectRoleBottomSheetState fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; // Selected color + return Colors.blueAccent; } - return Colors.white; // Unselected square color + return Colors.white; }), - checkColor: Colors.white, // Check mark color - side: const BorderSide( - color: Colors.grey), // Outline for unselected + checkColor: Colors.white, + side: const BorderSide(color: Colors.grey), ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From 261cba9dcf5754b6003f87baffc2d39713bdcd0b Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 10:50:50 +0530 Subject: [PATCH 10/37] done landscape responsive of all screen --- .../expense/expense_main_components.dart | 179 ++++++---- .../attendance/attendence_filter_sheet.dart | 69 ++-- .../add_payment_request_bottom_sheet.dart | 176 +++++----- lib/view/directory/directory_main_screen.dart | 76 ++-- lib/view/finance/advance_payment_screen.dart | 93 +++-- lib/view/finance/finance_screen.dart | 328 ++++++++++-------- lib/view/finance/payment_request_screen.dart | 103 ++++-- 7 files changed, 606 insertions(+), 418 deletions(-) diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index 79a1b75..3741a0e 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -282,86 +282,129 @@ class ExpenseList extends StatelessWidget { @override Widget build(BuildContext context) { - if (expenseList.isEmpty && !Get.find().isLoading.value) { - return Center(child: MyText.bodyMedium('No expenses found.')); - } + final ExpenseController controller = Get.find(); - 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( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - 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, + if (controller.isLoading.value && expenseList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (expenseList.isEmpty) { + return const Center( + child: Text( + 'No expenses found.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + // PORTRAIT MODE + if (!isLandscape) { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ); + } + + // LANDSCAPE β†’ WRAP IN SCROLL FOR SAFETY + return SingleChildScrollView( + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ), + ), + ); + }, + ), + ); + } + + Widget _buildItem(BuildContext context, int 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 { + final result = await Get.to( + () => ExpenseDetailScreen(expenseId: expense.id), + arguments: {'expense': expense}, + ); + if (result == true && onViewDetail != null) { + await onViewDetail!(); + } + }, + 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( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - MyText.bodyMedium(expense.expenseCategory.name, + MyText.bodyMedium('${expense.formattedAmount}', 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), + 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), ), - 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, + ), + ), + ], + ), + ], ), - ); - }, + ), + ), ); } } diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index abeaaa7..885e7bb 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -123,7 +123,6 @@ class _AttendanceFilterBottomSheetState }).toList(); final List widgets = [ - // πŸ”Ή View Section Padding( padding: const EdgeInsets.only(bottom: 4), child: Align( @@ -146,7 +145,6 @@ class _AttendanceFilterBottomSheetState }), ]; - // πŸ”Ή Organization filter widgets.addAll([ const Divider(), Padding( @@ -165,24 +163,6 @@ class _AttendanceFilterBottomSheetState color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 100, - height: 14, - color: Colors.grey.shade400, - ), - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Colors.grey.shade400, - shape: BoxShape.circle, - ), - ), - ], - ), ); } else if (widget.controller.organizations.isEmpty) { return Center( @@ -200,7 +180,6 @@ class _AttendanceFilterBottomSheetState }), ]); - // πŸ”Ή Date Range (only for Attendance Logs) if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), @@ -211,16 +190,12 @@ class _AttendanceFilterBottomSheetState child: MyText.titleSmall("Date Range", fontWeight: 600), ), ), - // βœ… Reusable DateRangePickerWidget DateRangePickerWidget( startDate: widget.controller.startDateAttendance, endDate: widget.controller.endDateAttendance, startLabel: "Start Date", endLabel: "End Date", - onDateRangeSelected: (start, end) { - // Optional: trigger UI updates if needed - setState(() {}); - }, + onDateRangeSelected: (_, __) => setState(() {}), ), ]); } @@ -232,18 +207,36 @@ class _AttendanceFilterBottomSheetState Widget build(BuildContext context) { return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: BaseBottomSheet( - title: "Attendance Filter", - submitText: "Apply", - onCancel: () => Navigator.pop(context), - onSubmit: () => Navigator.pop(context, { - 'selectedTab': tempSelectedTab, - 'selectedOrganization': widget.controller.selectedOrganization?.id, - }), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: buildMainFilters(), - ), + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; + + return BaseBottomSheet( + title: "Attendance Filter", + submitText: "Apply", + onCancel: () => Navigator.pop(context), + onSubmit: () => Navigator.pop(context, { + 'selectedTab': tempSelectedTab, + 'selectedOrganization': + widget.controller.selectedOrganization?.id, + }), + + // ---------------- UPDATED RESPONSIVE CHILD ---------------- + child: SizedBox( + height: isLandscape + ? constraints.maxHeight // πŸ”₯ Full screen in landscape + : constraints.maxHeight * 0.78, // normal in portrait + + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: buildMainFilters(), + ), + ), + ), + ); + }, ), ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index 0fe6e8d..abe1527 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -58,12 +58,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (widget.isEdit && widget.existingData != null) { final data = widget.existingData!; - // Prefill text fields controller.titleController.text = data["title"] ?? ""; controller.amountController.text = data["amount"]?.toString() ?? ""; controller.descriptionController.text = data["description"] ?? ""; - // Prefill due date if (data["dueDate"] != null && data["dueDate"].toString().isNotEmpty) { DateTime? dueDate = DateTime.tryParse(data["dueDate"].toString()); if (dueDate != null) { @@ -73,15 +71,14 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> } } - // Prefill dropdowns & toggles controller.selectedProject.value = { 'id': data["projectId"], 'name': data["projectName"], }; + controller.selectedPayee.value = data["payee"] ?? ""; controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; - // Categories & currencies everAll([controller.categories, controller.currencies], (_) { controller.selectedCategory.value = controller.categories .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); @@ -89,7 +86,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> .firstWhereOrNull((c) => c.id == data["currencyId"]); }); - // Attachments final attachmentsData = data["attachments"]; if (attachmentsData != null && attachmentsData is List && @@ -116,51 +112,56 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> @override Widget build(BuildContext context) { - return Obx(() => Form( - key: _formKey, - child: BaseBottomSheet( - title: widget.isEdit - ? "Edit Payment Request" - : "Create Payment Request", - isSubmitting: controller.isSubmitting.value, - onCancel: Get.back, - submitText: "Save as Draft", - onSubmit: () async { - if (_formKey.currentState!.validate() && _validateSelections()) { - bool success = false; - if (widget.isEdit && widget.existingData != null) { - final requestId = - widget.existingData!['id']?.toString() ?? ''; - if (requestId.isNotEmpty) { - success = await controller.submitEditedPaymentRequest( - requestId: requestId); + return Obx(() => SafeArea( + child: Form( + key: _formKey, + child: BaseBottomSheet( + title: widget.isEdit + ? "Edit Payment Request" + : "Create Payment Request", + isSubmitting: controller.isSubmitting.value, + onCancel: Get.back, + submitText: "Save as Draft", + onSubmit: () async { + if (_formKey.currentState!.validate() && + _validateSelections()) { + bool success = false; + + if (widget.isEdit && widget.existingData != null) { + final requestId = + widget.existingData!['id']?.toString() ?? ''; + if (requestId.isNotEmpty) { + success = await controller.submitEditedPaymentRequest( + requestId: requestId); + } else { + _showError("Invalid Payment Request ID"); + return; + } } else { - _showError("Invalid Payment Request ID"); - return; + success = await controller.submitPaymentRequest(); } - } else { - success = await controller.submitPaymentRequest(); - } - if (success) { - Get.back(); - if (widget.onUpdated != null) widget.onUpdated!(); + if (success) { + Get.back(); + widget.onUpdated?.call(); - showAppSnackbar( - title: "Success", - message: widget.isEdit - ? "Payment request updated successfully!" - : "Payment request created successfully!", - type: SnackbarType.success, - ); + showAppSnackbar( + title: "Success", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", + type: SnackbarType.success, + ); + } } - } - }, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDropdown( + }, + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDropdown( "Select Project", Icons.work_outline, controller.selectedProject.value?['name'] ?? @@ -168,9 +169,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.globalProjects, (p) => p['name'], controller.selectProject, - key: _projectDropdownKey), - _gap(), - _buildDropdown( + key: _projectDropdownKey, + ), + _gap(), + _buildDropdown( "Expense Category", Icons.category_outlined, controller.selectedCategory.value?.name ?? @@ -178,30 +180,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.categories, (c) => c.name, controller.selectCategory, - key: _categoryDropdownKey), - _gap(), - _buildTextField( - "Title", Icons.title_outlined, controller.titleController, - hint: "Enter title", validator: Validators.requiredField), - _gap(), - _buildRadio("Is Advance Payment", Icons.attach_money_outlined, - controller.isAdvancePayment, ["Yes", "No"]), - _gap(), - _buildDueDateField(), - _gap(), - _buildTextField("Amount", Icons.currency_rupee, - controller.amountController, - hint: "Enter Amount", - keyboardType: TextInputType.number, - validator: (v) => (v != null && - v.isNotEmpty && - double.tryParse(v) != null) - ? null - : "Enter valid amount"), - _gap(), - _buildPayeeAutocompleteField(), - _gap(), - _buildDropdown( + key: _categoryDropdownKey, + ), + _gap(), + _buildTextField("Title", Icons.title_outlined, + controller.titleController, + hint: "Enter title", + validator: Validators.requiredField), + _gap(), + _buildRadio( + "Is Advance Payment", + Icons.attach_money_outlined, + controller.isAdvancePayment, + ["Yes", "No"]), + _gap(), + _buildDueDateField(), + _gap(), + _buildTextField("Amount", Icons.currency_rupee, + controller.amountController, + hint: "Enter Amount", + keyboardType: TextInputType.number, + validator: (v) => (v != null && + v.isNotEmpty && + double.tryParse(v) != null) + ? null + : "Enter valid amount"), + _gap(), + _buildPayeeAutocompleteField(), + _gap(), + _buildDropdown( "Currency", Icons.monetization_on_outlined, controller.selectedCurrency.value?.currencyName ?? @@ -209,16 +216,19 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.currencies, (c) => c.currencyName, controller.selectCurrency, - key: _currencyDropdownKey), - _gap(), - _buildTextField("Description", Icons.description_outlined, - controller.descriptionController, - hint: "Enter description", - maxLines: 3, - validator: Validators.requiredField), - _gap(), - _buildAttachmentsSection(), - ], + key: _currencyDropdownKey, + ), + _gap(), + _buildTextField("Description", Icons.description_outlined, + controller.descriptionController, + hint: "Enter description", + maxLines: 3, + validator: Validators.requiredField), + _gap(), + _buildAttachmentsSection(), + MySpacing.height(30), + ], + ), ), ), ), @@ -284,6 +294,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final i = entry.key; final label = entry.value; final value = i == 0; + return Expanded( child: RadioListTile( contentPadding: EdgeInsets.zero, @@ -354,7 +365,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> displayStringForOption: (option) => option, fieldViewBuilder: (context, fieldController, focusNode, onFieldSubmitted) { - // Avoid updating during build WidgetsBinding.instance.addPostFrameCallback((_) { if (fieldController.text != controller.selectedPayee.value) { fieldController.text = controller.selectedPayee.value; diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index c5d55a1..5c06a6a 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -99,34 +99,60 @@ class _DirectoryMainScreenState extends State ), ), ), - 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"), - ], + body: SafeArea( + child: 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(), - ], + // ---------------- TabBarView + Scroll / Landscape Support ---------------- + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + if (isLandscape) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ), + ), + ); + } + + // Portrait + return TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ); + }, + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 2c1fe40..5cf3edc 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -49,39 +49,72 @@ class _AdvancePaymentScreenState extends State @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(), - ], + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return 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, + + // ---------------- PORTRAIT (UNCHANGED) ---------------- + child: !isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + color: const Color(0xFFF5F5F5), + child: Column( + children: [ + _buildSearchBar(), + _buildEmployeeDropdown(context), + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ) + + // ---------------- LANDSCAPE (FIXED) ---------------- + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + width: double.infinity, + color: const Color(0xFFF5F5F5), + + // ❗ Removed IntrinsicHeight + // ❗ Removed ConstrainedBox + // Dropdown can now open freely + + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSearchBar(), + _buildEmployeeDropdown( + context), // now overlay works + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ), ), - ), - ), + ); + }, ), ), ); diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index eaa1508..08df85d 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -113,171 +113,219 @@ class _FinanceScreenState extends State ), ), ), - body: FadeTransition( - opacity: _fadeAnimation, - child: Obx(() { - if (menuController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } + body: SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; - 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), - ), - ); - } + return Obx(() { + if (menuController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - // Filter allowed Finance menus dynamically - final financeMenuIds = [ - MenuItems.expenseReimbursement, - MenuItems.paymentRequests, - MenuItems.advancePaymentStatements, - ]; + 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 financeMenus = menuController.menuItems - .where((m) => financeMenuIds.contains(m.id) && m.available) - .toList(); + // Filter allowed Finance menus dynamically + final financeMenuIds = [ + MenuItems.expenseReimbursement, + MenuItems.paymentRequests, + MenuItems.advancePaymentStatements, + ]; - if (financeMenus.isEmpty) { - return const Center( - child: Text( - "You don’t have access to the Finance section.", - style: TextStyle(color: Colors.grey), - ), - ); - } + final financeMenus = menuController.menuItems + .where((m) => financeMenuIds.contains(m.id) && m.available) + .toList(); - 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(), - ], - ), - ); - }), + if (financeMenus.isEmpty) { + return const Center( + child: Text( + "You don’t have access to the Finance section.", + style: TextStyle(color: Colors.grey), + ), + ); + } + + // ---------------------- PORTRAIT MODE ---------------------- + if (!isLandscape) { + 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(), + ], + ), + ); + } + + // ---------------------- LANDSCAPE MODE ---------------------- + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildFinanceModulesCompact(financeMenus), + MySpacing.height(24), + + // Wider charts behave better side-by-side or full width + SizedBox( + width: constraints.maxWidth, + child: ExpenseByStatusWidget( + controller: dashboardController), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: ExpenseTypeReportChart(), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: MonthlyExpenseDashboardChart(), + ), + ], + ), + ); + }); + }, + ), + ), ), ); } // --- Finance Modules (Compact Dashboard-style) --- -Widget _buildFinanceModulesCompact(List financeMenus) { - // Map menu IDs to icon + color - final Map 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 financeMenus) { + // Map menu IDs to icon + color + final Map 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; diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index f826785..df94cb6 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -99,41 +99,76 @@ class _PaymentRequestMainScreenState extends State 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), - ], - ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return Column( + children: [ + // ---------------- TabBar ---------------- + 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"), + ], ), - ], - ), - ), - ), - ], + ), + + // ---------------- Content Area ---------------- + Expanded( + child: Container( + color: Colors.grey[100], + child: isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList( + isHistory: false), + _buildPaymentRequestList( + isHistory: true), + ], + ), + ), + ], + ), + ), + ) + : Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList(isHistory: false), + _buildPaymentRequestList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), ), floatingActionButton: Obx(() { if (permissionController.permissions.isEmpty) { From 125a09c7729b9ac2be300f5d3e837f13b8dec1f0 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 16:25:25 +0530 Subject: [PATCH 11/37] Done all screen landscape responsive for mobile and tablet --- .../directory_filter_bottom_sheet.dart | 173 ++++++----- .../assign_employee_bottom_sheet.dart | 229 ++++++++------- lib/view/layouts/user_profile_right_bar.dart | 186 ++++-------- .../service_project_screen.dart | 276 ++++++++++-------- 4 files changed, 434 insertions(+), 430 deletions(-) diff --git a/lib/model/directory/directory_filter_bottom_sheet.dart b/lib/model/directory/directory_filter_bottom_sheet.dart index 4443f61..850b1c8 100644 --- a/lib/model/directory/directory_filter_bottom_sheet.dart +++ b/lib/model/directory/directory_filter_bottom_sheet.dart @@ -15,6 +15,7 @@ class DirectoryFilterBottomSheet extends StatefulWidget { class _DirectoryFilterBottomSheetState extends State { final DirectoryController controller = Get.find(); + final _categorySearchQuery = ''.obs; final _bucketSearchQuery = ''.obs; @@ -59,84 +60,100 @@ class _DirectoryFilterBottomSheetState Get.back(); }, onCancel: Get.back, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() { - final hasSelections = _tempSelectedCategories.isNotEmpty || - _tempSelectedBuckets.isNotEmpty; - if (!hasSelections) return const SizedBox.shrink(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 16, left: 4, right: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final hasSelections = _tempSelectedCategories.isNotEmpty || + _tempSelectedBuckets.isNotEmpty; + + if (!hasSelections) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText("Selected Filters:", fontWeight: 600), + const SizedBox(height: 4), + _buildChips(_tempSelectedCategories, + controller.contactCategories, _toggleCategory), + _buildChips(_tempSelectedBuckets, + controller.contactBuckets, _toggleBucket), + const SizedBox(height: 10), + ], + ); + }), + + // RESET BUTTON + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - MyText("Selected Filters:", fontWeight: 600), - const SizedBox(height: 4), - _buildChips(_tempSelectedCategories, - controller.contactCategories, _toggleCategory), - _buildChips(_tempSelectedBuckets, controller.contactBuckets, - _toggleBucket), - ], - ); - }), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( - onPressed: _resetFilters, - icon: const Icon(Icons.restart_alt, size: 18), - label: MyText("Reset All", color: Colors.red), - style: TextButton.styleFrom( - foregroundColor: Colors.red.shade400, + TextButton.icon( + onPressed: _resetFilters, + icon: const Icon(Icons.restart_alt, size: 18), + label: MyText("Reset All", color: Colors.red), ), - ), - ], - ), - if (controller.contactCategories.isNotEmpty) - Obx(() => _buildExpandableFilterSection( - title: "Categories", - expanded: _categoryExpanded, - searchQuery: _categorySearchQuery, - allItems: controller.contactCategories, - selectedItems: _tempSelectedCategories, - onToggle: _toggleCategory, - )), - if (controller.contactBuckets.isNotEmpty) - Obx(() => _buildExpandableFilterSection( - title: "Buckets", - expanded: _bucketExpanded, - searchQuery: _bucketSearchQuery, - allItems: controller.contactBuckets, - selectedItems: _tempSelectedBuckets, - onToggle: _toggleBucket, - )), - ], - ), - ), + ], + ), + + // CATEGORIES + if (controller.contactCategories.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Categories", + expanded: _categoryExpanded, + searchQuery: _categorySearchQuery, + allItems: controller.contactCategories, + selectedItems: _tempSelectedCategories, + onToggle: _toggleCategory, + )), + + // BUCKETS + if (controller.contactBuckets.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Buckets", + expanded: _bucketExpanded, + searchQuery: _bucketSearchQuery, + allItems: controller.contactBuckets, + selectedItems: _tempSelectedBuckets, + onToggle: _toggleBucket, + )), + + const SizedBox(height: 20), + ], + ), + ); + }, ), ); } + // ------------------------------ + // CHIP UI FOR SELECTED FILTERS + // ------------------------------ Widget _buildChips(RxList selectedIds, List allItems, Function(String) onRemoved) { final idToName = {for (var item in allItems) item.id: item.name}; + return Wrap( spacing: 4, runSpacing: 4, - children: selectedIds - .map((id) => Chip( - label: MyText(idToName[id] ?? "", color: Colors.black87), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => onRemoved(id), - backgroundColor: Colors.blue.shade50, - )) - .toList(), + children: selectedIds.map((id) { + return Chip( + label: MyText(idToName[id] ?? "", color: Colors.black87), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => onRemoved(id), + backgroundColor: Colors.blue.shade50, + ); + }).toList(), ); } + // ------------------------------ + // EXPANDABLE FILTER UI + // ------------------------------ Widget _buildExpandableFilterSection({ required String title, required RxBool expanded, @@ -146,7 +163,7 @@ class _DirectoryFilterBottomSheetState required Function(String) onToggle, }) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ GestureDetector( @@ -159,28 +176,27 @@ class _DirectoryFilterBottomSheetState : Icons.keyboard_arrow_right, size: 20, ), - const SizedBox(width: 4), - MyText( - "$title", - fontWeight: 600, - fontSize: 16, - ), + const SizedBox(width: 6), + MyText(title, fontWeight: 600, fontSize: 16), ], ), ), if (expanded.value) _buildFilterSection( + title: title, searchQuery: searchQuery, allItems: allItems, selectedItems: selectedItems, onToggle: onToggle, - title: title, ), ], ), ); } + // ------------------------------ + // FILTER LIST + SEARCH + // ------------------------------ Widget _buildFilterSection({ required String title, required RxString searchQuery, @@ -189,14 +205,16 @@ class _DirectoryFilterBottomSheetState required Function(String) onToggle, }) { final filteredList = allItems.where((item) { - if (searchQuery.isEmpty) return true; + if (searchQuery.value.isEmpty) return true; return item.name.toLowerCase().contains(searchQuery.value.toLowerCase()); }).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 6), + const SizedBox(height: 8), + + // SEARCH BOX TextField( onChanged: (value) => searchQuery.value = value, style: const TextStyle(fontSize: 13), @@ -215,7 +233,10 @@ class _DirectoryFilterBottomSheetState fillColor: Colors.grey.shade100, ), ), + const SizedBox(height: 8), + + // NO RESULTS if (filteredList.isEmpty) Row( children: [ @@ -227,7 +248,7 @@ class _DirectoryFilterBottomSheetState ) else ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 230), + constraints: const BoxConstraints(maxHeight: 260), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, @@ -238,7 +259,7 @@ class _DirectoryFilterBottomSheetState return Obx(() { final isSelected = selectedItems.contains(item.id); - return GestureDetector( + return InkWell( onTap: () => onToggle(item.id), child: Container( padding: const EdgeInsets.symmetric( @@ -271,7 +292,7 @@ class _DirectoryFilterBottomSheetState }); }, ), - ) + ), ], ); } diff --git a/lib/view/employees/assign_employee_bottom_sheet.dart b/lib/view/employees/assign_employee_bottom_sheet.dart index 61440ea..9f5e05d 100644 --- a/lib/view/employees/assign_employee_bottom_sheet.dart +++ b/lib/view/employees/assign_employee_bottom_sheet.dart @@ -48,116 +48,133 @@ class _AssignProjectBottomSheetState extends State { onCancel: () => Navigator.pop(context), onSubmit: _handleAssign, submitText: "Assign", - child: Obx(() { - if (assignController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - final projects = assignController.allProjects; - if (projects.isEmpty) { - return const Center(child: Text('No projects available.')); - } + /// πŸ”₯ MAKE BODY SCROLLABLE (fix for landscape) + child: LayoutBuilder( + builder: (context, constraints) { + return Obx(() { + if (assignController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall( - 'Select the projects to assign this employee.', - color: Colors.grey[600], - ), - MySpacing.height(8), + final projects = assignController.allProjects; + if (projects.isEmpty) { + return const Center(child: Text('No projects available.')); + } - // Select All - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Projects (${projects.length})', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - TextButton( - onPressed: () { - assignController.toggleSelectAll(); - }, - child: Obx(() { - return Text( - assignController.areAllSelected() - ? 'Deselect All' - : 'Select All', - style: const TextStyle( - color: Colors.blueAccent, - fontWeight: FontWeight.w600, - ), - ); - }), - ), - ], - ), - - // List of Projects - SizedBox( - height: 300, - child: ListView.builder( - controller: _scrollController, - itemCount: projects.length, - itemBuilder: (context, index) { - final GlobalProjectModel project = projects[index]; - return Obx(() { - final bool isSelected = - assignController.isProjectSelected( - project.id.toString(), - ); - return Theme( - data: Theme.of(context).copyWith( - checkboxTheme: CheckboxThemeData( - fillColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.selected) - ? Colors.blueAccent - : Colors.white, - ), - side: const BorderSide( - color: Colors.black, - width: 2, - ), - checkColor: - WidgetStateProperty.all(Colors.white), - ), - ), - child: CheckboxListTile( - dense: true, - value: isSelected, - title: Text( - project.name, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - ), - onChanged: (checked) { - assignController.toggleProjectSelection( - project.id.toString(), - checked ?? false, - ); - }, - activeColor: Colors.blueAccent, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), - ), - ); - }); - }, + return ConstrainedBox( + constraints: BoxConstraints( + /// πŸ”₯ Always allow enough height for scroll + maxHeight: constraints.maxHeight, ), - ), - ], - ); - }), + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodySmall( + 'Select the projects to assign this employee.', + color: Colors.grey[600], + ), + MySpacing.height(8), + + // Header Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Projects (${projects.length})', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + TextButton( + onPressed: () { + assignController.toggleSelectAll(); + }, + child: Obx(() { + return Text( + assignController.areAllSelected() + ? 'Deselect All' + : 'Select All', + style: const TextStyle( + color: Colors.blueAccent, + fontWeight: FontWeight.w600, + ), + ); + }), + ), + ], + ), + + /// πŸ”₯ List auto grows and scrolls β€” no fixed height + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: projects.length, + itemBuilder: (context, index) { + final GlobalProjectModel project = projects[index]; + return Obx(() { + final bool isSelected = + assignController.isProjectSelected( + project.id.toString(), + ); + return Theme( + data: Theme.of(context).copyWith( + checkboxTheme: CheckboxThemeData( + fillColor: + WidgetStateProperty.resolveWith( + (states) => + states.contains(WidgetState.selected) + ? Colors.blueAccent + : Colors.white, + ), + side: const BorderSide( + color: Colors.black, + width: 2, + ), + checkColor: + WidgetStateProperty.all(Colors.white), + ), + ), + child: CheckboxListTile( + dense: true, + value: isSelected, + title: Text( + project.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + onChanged: (checked) { + assignController.toggleProjectSelection( + project.id.toString(), + checked ?? false, + ); + }, + activeColor: Colors.blueAccent, + controlAffinity: + ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + ); + }); + }, + ), + ], + ), + ), + ); + }); + }, + ), ); }, ); diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index a36a555..5573f3b 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -52,6 +52,7 @@ class _UserProfileBarState extends State @override Widget build(BuildContext context) { final bool isCondensed = widget.isCondensed; + return Padding( padding: const EdgeInsets.only(left: 14), child: ClipRRect( @@ -88,41 +89,52 @@ class _UserProfileBarState extends State bottom: true, child: Stack( children: [ + // ======================= MAIN PROFILE SIDEBAR ======================= Offstage( offstage: _isThemeEditorVisible, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _isLoading - ? const _LoadingSection() - : _userProfileSection(isCondensed), - if (!_isLoading && !isCondensed) _switchTenantRow(), - MySpacing.height(12), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.7, - color: Colors.grey.withOpacity(0.25), - ), - MySpacing.height(12), - _supportAndSettingsMenu(isCondensed), - MySpacing.height(12), - - // Subtle version text for expanded mode - if (!isCondensed && _appVersion.isNotEmpty) - _versionText(), - - const Spacer(), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.35, - color: Colors.grey.withOpacity(0.18), - ), - _logoutButton(isCondensed), - ], + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _isLoading + ? const _LoadingSection() + : _userProfileSection(isCondensed), + if (!_isLoading && !isCondensed) + _switchTenantRow(), + MySpacing.height(12), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.7, + color: Colors.grey.withOpacity(0.25), + ), + MySpacing.height(12), + _supportAndSettingsMenu(isCondensed), + const Spacer(), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.35, + color: Colors.grey.withOpacity(0.18), + ), + _logoutButton(isCondensed), + ], + ), + ), + ), + ); + }, ), ), + + // ======================= THEME EDITOR VIEW ======================= Offstage( offstage: !_isThemeEditorVisible, child: ThemeEditorWidget( @@ -131,9 +143,6 @@ class _UserProfileBarState extends State }, ), ), - - // Floating badge for condensed mode - if (isCondensed && _appVersion.isNotEmpty) _versionBadge(), ], ), ), @@ -143,96 +152,7 @@ class _UserProfileBarState extends State ); } - // =================== Version Widgets =================== - - Widget _versionText() { - return Padding( - padding: const EdgeInsets.only(top: 4, bottom: 12), - child: Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - decoration: BoxDecoration( - color: Colors.grey.shade100.withOpacity(0.85), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.grey.shade200, - width: 0.7, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.12), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.info_outline, size: 14, color: Colors.grey[700]), - const SizedBox(width: 4), - Text( - 'Version: $_appVersion', - style: TextStyle( - fontSize: 12, - color: Colors.grey[800], - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget _versionBadge() { - return Positioned( - bottom: 10, - right: 14, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - decoration: BoxDecoration( - color: Colors.grey.shade100.withOpacity(0.85), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.17), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - _appVersion, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.black87, - letterSpacing: 0.4, - ), - ), - ), - ), - ), - ); - } - - // =================== Existing methods =================== + // ==================== EXISTING CODE (UNCHANGED) ===================== Widget _switchTenantRow() { final TenantSwitchController tenantSwitchController = @@ -337,17 +257,25 @@ class _UserProfileBarState extends State child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ); + // ⭐ FIXED β€” YOUR ORIGINAL INTENT, COMPLETED PROPERLY Widget _noTenantContainer() => Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue.shade200, width: 1), + border: Border.all( + color: Colors.blue.shade200, + width: 1, + ), ), - child: MyText.bodyMedium( - "No tenants available", - color: Colors.blueAccent, - fontWeight: 600, + child: const Center( + child: Text( + "No organizations available", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), ), ); diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index e26d7a3..3245f1e 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State 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(); }); @@ -42,35 +42,29 @@ class _ServiceProjectScreenState extends State Widget _buildProjectCard(ProjectItem project) { return Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shadowColor: Colors.indigo.withOpacity(0.10), color: Colors.white, child: InkWell( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(5), onTap: () { - // Navigate to ServiceProjectDetailsScreen Get.to(() => ServiceProjectDetailsScreen(projectId: project.id)); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// Project Header Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleMedium( - project.name, - fontWeight: 700, - ), - MySpacing.height(4), - ], + child: MyText.titleMedium( + project.name, + fontWeight: 700, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), if (project.status?.status.isNotEmpty ?? false) @@ -89,47 +83,32 @@ class _ServiceProjectScreenState extends State ), ], ), - - MySpacing.height(10), - - /// Assigned Date + MySpacing.height(8), _buildDetailRow( Icons.date_range_outlined, Colors.teal, "Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}", - fontSize: 13, ), - - MySpacing.height(8), - - /// Client Info + MySpacing.height(6), if (project.client != null) _buildDetailRow( Icons.account_circle_outlined, Colors.indigo, "Client: ${project.client!.name} (${project.client!.contactPerson})", - fontSize: 13, ), - - MySpacing.height(8), - - /// Contact Info + MySpacing.height(6), _buildDetailRow( Icons.phone, Colors.green, "Contact: ${project.contactName} (${project.contactPhone})", - fontSize: 13, ), - - MySpacing.height(12), - - /// Services List + MySpacing.height(10), if (project.services.isNotEmpty) Wrap( spacing: 6, runSpacing: 4, children: project.services - .map((service) => _buildServiceChip(service.name)) + .map((e) => _buildServiceChip(e.name)) .toList(), ), ], @@ -145,7 +124,7 @@ class _ServiceProjectScreenState extends State color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: MyText.labelSmall( name, color: Colors.orange[800], @@ -154,19 +133,18 @@ class _ServiceProjectScreenState extends State ); } - Widget _buildDetailRow(IconData icon, Color iconColor, String value, - {double fontSize = 12}) { + Widget _buildDetailRow(IconData icon, Color color, String value) { return Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(icon, size: 18, color: iconColor), + Icon(icon, size: 18, color: color), MySpacing.width(8), - Flexible( + Expanded( child: MyText.bodySmall( value, - color: Colors.grey[900], + maxLines: 2, + overflow: TextOverflow.ellipsis, fontWeight: 500, - fontSize: fontSize, + color: Colors.grey[900], ), ), ], @@ -181,7 +159,7 @@ class _ServiceProjectScreenState extends State const Icon(Icons.work_outline, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium('No matching projects found.', - fontWeight: 600, color: Colors.grey), + color: Colors.grey, fontWeight: 600), MySpacing.height(10), MyText.bodySmall('Try adjusting your filters or refresh.', color: Colors.grey), @@ -192,90 +170,150 @@ class _ServiceProjectScreenState extends State @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF5F5F5), - appBar: CustomAppBar( - title: "Service Projects", - onBackPressed: () => Get.toNamed('/dashboard'), - ), - body: Column( - children: [ - /// Search bar and actions - Padding( - padding: MySpacing.xy(8, 8), - child: Row( + return SafeArea( + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: "Service Projects", + onBackPressed: () => Get.toNamed('/dashboard'), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Column( 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( - valueListenable: searchController, - builder: (context, value, _) { - if (value.text.isEmpty) { - return const SizedBox.shrink(); - } - return IconButton( - icon: const Icon(Icons.clear, + /// SEARCH BAR AREA + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 38, + child: TextField( + controller: searchController, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: const Icon(Icons.search, 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), + suffixIcon: + ValueListenableBuilder( + valueListenable: searchController, + builder: (context, value, _) { + return value.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, + size: 20, color: Colors.grey), + onPressed: () { + searchController.clear(); + controller.updateSearch(''); + }, + ) + : const SizedBox.shrink(); + }, + ), + hintText: 'Search projects...', + fillColor: Colors.white, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: + BorderSide(color: Colors.grey.shade300), + ), + ), + ), ), ), - ), + MySpacing.width(8), + + /// FILTER BUTTON + _roundIconButton(Icons.tune), + + MySpacing.width(8), + + /// ACTION MENU + _roundMenuButton(), + ], ), ), + + /// LIST AREA + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final projects = controller.filteredProjects; + + return MyRefreshIndicator( + onRefresh: _refreshProjects, + color: Colors.white, + backgroundColor: Colors.indigo, + child: projects.isEmpty + ? _buildEmptyState() + : ListView.separated( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 4, bottom: 20), + itemCount: projects.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, index) => + _buildProjectCard(projects[index]), + ), + ); + }), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _roundIconButton(IconData icon) { + return Container( + height: 38, + width: 38, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.grey.shade300), + ), + child: Icon(icon, size: 20, color: Colors.black87), + ); + } + + Widget _roundMenuButton() { + return Container( + height: 38, + width: 38, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + itemBuilder: (context) => [ + const PopupMenuItem( + enabled: false, + height: 30, + child: Text("Actions", + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)), + ), + const PopupMenuItem( + value: 1, + child: Row( + children: [ + Expanded(child: Text("Manage Projects")), + Icon(Icons.chevron_right, size: 20, color: Colors.indigo), ], ), ), - - /// Project List - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - - 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]), - ), - ); - }), - ), ], ), ); From 7a9673e2c21cfda456f9e29abc87f97f2bc0723c Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:13:44 +0530 Subject: [PATCH 12/37] added multiselect bottom sheet in assign task bottom sheet form --- .../assign_task_bottom_sheet .dart | 197 +++++++++++------- lib/model/employees/employee_model.dart | 2 +- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 4db2228..fdfcced 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -13,6 +13,10 @@ import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; +// Added imports for employee selection +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; final String activityName; @@ -43,7 +47,8 @@ class _AssignTaskBottomSheetState extends State { final DailyTaskPlanningController controller = Get.find(); final ProjectController projectController = Get.find(); - final OrganizationController orgController = Get.put(OrganizationController()); + final OrganizationController orgController = + Get.put(OrganizationController()); final ServiceController serviceController = Get.put(ServiceController()); final TextEditingController targetController = TextEditingController(); @@ -79,7 +84,8 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, serviceId: selectedService?.id); + await controller.fetchTaskData(selectedProjectId, + serviceId: selectedService?.id); } @override @@ -142,10 +148,11 @@ class _AssignTaskBottomSheetState extends State { const Divider(), // Pending Task Info - _infoRow(Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), + _infoRow( + Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector + // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, child: Row( @@ -158,18 +165,52 @@ class _AssignTaskBottomSheetState extends State { ), MySpacing.height(8), - // Employee List - Container( - constraints: const BoxConstraints(maxHeight: 180), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), + // ------------------------------- + // Employee selector (REPLACED) + // ------------------------------- + // We show a button-like container (with border) that opens the reusable + // EmployeeSelectionBottomSheet. Selected employees are reflected using + // existing controller.uploadingStates & controller.selectedEmployees. + GestureDetector( + onTap: _openEmployeeSelectionSheet, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText("Select team members", + color: Colors.grey.shade700); + } + // show summary text when there are selected employees + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), ), - child: _buildEmployeeList(), ), MySpacing.height(8), - // Selected Employees Chips + // Selected Employees Chips (keeps existing behavior) _buildSelectedEmployees(), MySpacing.height(8), @@ -198,7 +239,8 @@ class _AssignTaskBottomSheetState extends State { } void _onRoleMenuPressed() { - final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; final Size screenSize = overlay.size; showMenu( @@ -219,63 +261,12 @@ class _AssignTaskBottomSheetState extends State { }), ], ).then((value) { - if (value != null) controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) + controller.onRoleSelected(value == 'all' ? null : value); }); } - Widget _buildEmployeeList() { - return Obx(() { - if (controller.isFetchingEmployees.value) { - return Center(child: CircularProgressIndicator()); - } - - final filteredEmployees = controller.selectedRoleId.value == null - ? controller.employees - : controller.employees - .where((e) => e.jobRoleID.toString() == controller.selectedRoleId.value) - .toList(); - - if (filteredEmployees.isEmpty) { - return Center(child: Text("No employees available for selected role.")); - } - - return Scrollbar( - controller: _employeeListScrollController, - thumbVisibility: true, - child: ListView.builder( - controller: _employeeListScrollController, - itemCount: filteredEmployees.length, - itemBuilder: (context, index) { - final employee = filteredEmployees[index]; - final rxBool = controller.uploadingStates[employee.id]; - - return Obx(() => ListTile( - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - leading: Checkbox( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - value: rxBool?.value ?? false, - onChanged: (selected) { - if (rxBool != null) { - rxBool.value = selected ?? false; - controller.updateSelectedEmployees(); - } - }, - fillColor: MaterialStateProperty.resolveWith((states) => - states.contains(MaterialState.selected) - ? const Color.fromARGB(255, 95, 132, 255) - : Colors.transparent), - checkColor: Colors.white, - side: const BorderSide(color: Colors.black), - ), - title: Text(employee.name, style: const TextStyle(fontSize: 14)), - visualDensity: VisualDensity.compact, - )); - }, - ), - ); - }); - } + // Removed old inline employee list; selection handled by bottom sheet. Widget _buildSelectedEmployees() { return Obx(() { @@ -329,9 +320,12 @@ class _AssignTaskBottomSheetState extends State { decoration: InputDecoration( hintText: hintText, border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), - validator: (value) => this.controller.formFieldValidator(value, fieldType: validatorType), + validator: (value) => this + .controller + .formFieldValidator(value, fieldType: validatorType), ), ], ); @@ -350,9 +344,11 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", fontWeight: 600, color: Colors.black), + child: MyText.titleMedium("$title: ", + fontWeight: 600, color: Colors.black), ), - TextSpan(text: value, style: const TextStyle(color: Colors.black)), + TextSpan( + text: value, style: const TextStyle(color: Colors.black)), ], ), ), @@ -362,6 +358,50 @@ class _AssignTaskBottomSheetState extends State { ); } + Future _openEmployeeSelectionSheet() async { + // Open the existing EmployeeSelectionBottomSheet + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedEmployees.toList(), + multipleSelection: true, + title: 'Select Team Members', + ), + ); + + if (result == null) return; + + // Merge returned employees into controller.uploadingStates & controller.selectedEmployees + // 1) Reset all uploadingStates to false, then set true for selected + controller.uploadingStates.forEach((key, rx) { + rx.value = false; + }); + + for (final emp in result) { + final idStr = emp.id.toString(); + if (controller.uploadingStates.containsKey(idStr)) { + controller.uploadingStates[idStr]?.value = true; + } else { + // if uploadingStates doesn't have the id yet, add it (safe fallback) + controller.uploadingStates[idStr] = RxBool(true); + } + } + + // 2) Update selectedEmployees list in controller + controller.selectedEmployees.assignAll(result); + + // 3) Call controller helper (keeps existing behavior) + try { + controller.updateSelectedEmployees(); + } catch (_) { + // If controller does not implement updateSelectedEmployees, ignore. + } + } + void _onAssignTaskPressed() { final selectedTeam = controller.uploadingStates.entries .where((e) => e.value.value) @@ -369,13 +409,19 @@ class _AssignTaskBottomSheetState extends State { .toList(); if (selectedTeam.isEmpty) { - showAppSnackbar(title: "Team Required", message: "Please select at least one team member", type: SnackbarType.error); + showAppSnackbar( + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { - showAppSnackbar(title: "Invalid Input", message: "Please enter a valid target number", type: SnackbarType.error); + showAppSnackbar( + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error); return; } @@ -390,7 +436,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { - showAppSnackbar(title: "Description Required", message: "Please enter a description", type: SnackbarType.error); + showAppSnackbar( + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error); return; } diff --git a/lib/model/employees/employee_model.dart b/lib/model/employees/employee_model.dart index d774f23..0abc764 100644 --- a/lib/model/employees/employee_model.dart +++ b/lib/model/employees/employee_model.dart @@ -72,7 +72,7 @@ class EmployeeModel { }; } - /// βœ… Add equality based on unique `id` + ///Equality based on unique `id` β€” required for multi-selection to work @override bool operator ==(Object other) { if (identical(this, other)) return true; From 170ff050c4c65abbdd7c7f06806dc7fd076a4015 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:49:26 +0530 Subject: [PATCH 13/37] done with employee list dropdown updation --- .../multiple_select_bottomsheet.dart | 139 +++++++++++------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 336bb1a..0e11530 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; @@ -24,33 +25,67 @@ class EmployeeSelectionBottomSheet extends StatefulWidget { class _EmployeeSelectionBottomSheetState extends State { final TextEditingController _searchController = TextEditingController(); + final RxBool _isSearching = false.obs; - final RxList _searchResults = [].obs; + final RxList _allResults = [].obs; + late RxList _selectedEmployees; + Timer? _debounce; + @override void initState() { super.initState(); _selectedEmployees = RxList.from(widget.initiallySelected); - _searchEmployees(''); + + _performSearch(''); } @override void dispose() { + _debounce?.cancel(); _searchController.dispose(); super.dispose(); } - Future _searchEmployees(String query) async { + // ------------------------------------------------------ + // πŸ”₯ Optimized debounce-based search + // ------------------------------------------------------ + void _onSearchChanged(String query) { + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 300), () { + _performSearch(query.trim()); + }); + } + + Future _performSearch(String query) async { _isSearching.value = true; + final data = await ApiService.searchEmployeesBasic(searchString: query); + final results = (data as List) .map((e) => EmployeeModel.fromJson(e as Map)) .toList(); - _searchResults.assignAll(results); + + // ------------------------------------------------------ + // πŸ”₯ Auto-move selected employees to top + // ------------------------------------------------------ + results.sort((a, b) { + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; + + if (aSel != bSel) return aSel.compareTo(bSel); + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _allResults.assignAll(results); + _isSearching.value = false; } + // ------------------------------------------------------ + // Handle tap & checkbox + // ------------------------------------------------------ void _toggleEmployee(EmployeeModel emp) { if (widget.multipleSelection) { if (_selectedEmployees.contains(emp)) { @@ -61,9 +96,14 @@ class _EmployeeSelectionBottomSheetState } else { _selectedEmployees.assignAll([emp]); } - _selectedEmployees.refresh(); // important for Obx rebuild + + // Re-sort list after each toggle + _performSearch(_searchController.text.trim()); } + // ------------------------------------------------------ + // Submit selection + // ------------------------------------------------------ void _handleSubmit() { if (widget.multipleSelection) { Navigator.of(context).pop(_selectedEmployees.toList()); @@ -73,11 +113,14 @@ class _EmployeeSelectionBottomSheetState } } + // ------------------------------------------------------ + // Search bar widget + // ------------------------------------------------------ Widget _searchBar() => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: TextField( controller: _searchController, - onChanged: _searchEmployees, + onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Search employees...', filled: true, @@ -88,7 +131,7 @@ class _EmployeeSelectionBottomSheetState icon: const Icon(Icons.close, color: Colors.grey), onPressed: () { _searchController.clear(); - _searchEmployees(''); + _performSearch(''); }, ) : null, @@ -102,60 +145,52 @@ class _EmployeeSelectionBottomSheetState ), ); + // ------------------------------------------------------ + // Employee list (optimized) + // ------------------------------------------------------ Widget _employeeList() => Expanded( child: Obx(() { - if (_isSearching.value) { - return const Center(child: CircularProgressIndicator()); - } - - if (_searchResults.isEmpty) { - return const Center(child: Text("No employees found")); - } + final results = _allResults; return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: _searchResults.length, + itemCount: results.length, itemBuilder: (context, index) { - final emp = _searchResults[index]; + final emp = results[index]; + final isSelected = _selectedEmployees.contains(emp); - return Obx(() { - final isSelected = _selectedEmployees.contains(emp); - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blueAccent, - child: Text( - (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') - .toUpperCase(), - style: const TextStyle(color: Colors.white), - ), + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') + .toUpperCase(), + style: const TextStyle(color: Colors.white), ), - title: Text('${emp.firstName} ${emp.lastName}'), - subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) { - FocusScope.of(context).unfocus(); // hide keyboard - _toggleEmployee(emp); - }, - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), + ), + title: Text('${emp.firstName} ${emp.lastName}'), + subtitle: Text(emp.email), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, ), - onTap: () { - FocusScope.of(context).unfocus(); - _toggleEmployee(emp); - }, - contentPadding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - ); - }); + ), + onTap: () => _toggleEmployee(emp), + contentPadding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); }, ); }), ); + // ------------------------------------------------------ + // Build bottom sheet + // ------------------------------------------------------ @override Widget build(BuildContext context) { return BaseBottomSheet( @@ -164,10 +199,12 @@ class _EmployeeSelectionBottomSheetState onSubmit: _handleSubmit, child: SizedBox( height: MediaQuery.of(context).size.height * 0.7, - child: Column(children: [ - _searchBar(), - _employeeList(), - ]), + child: Column( + children: [ + _searchBar(), + _employeeList(), + ], + ), ), ); } From 270d24ed8ce341cb84cd608c578ecf2a54065424 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 11:57:13 +0530 Subject: [PATCH 14/37] advance paymen --- lib/view/finance/advance_payment_screen.dart | 72 ++++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 5cf3edc..2ca1799 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -35,8 +35,8 @@ class _AdvancePaymentScreenState extends State } }); - controller.searchQuery.listen((q) { - if (_searchCtrl.text != q) _searchCtrl.text = q; + _searchCtrl.addListener(() { + controller.searchQuery.value = _searchCtrl.text.trim(); }); } @@ -58,19 +58,25 @@ class _AdvancePaymentScreenState extends State final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: RefreshIndicator( - onRefresh: () async { - final emp = controller.selectedEmployee.value; - if (emp != null) { - await controller.fetchAdvancePayments(emp.id.toString()); + return 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: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); } }, - color: Colors.white, - backgroundColor: contentTheme.primary, - strokeWidth: 2.5, - displacement: 60, // ---------------- PORTRAIT (UNCHANGED) ---------------- child: !isLandscape @@ -195,7 +201,13 @@ class _AdvancePaymentScreenState extends State child: TextField( controller: _searchCtrl, focusNode: _searchFocus, - onChanged: (v) => controller.searchQuery.value = v.trim(), + onTap: () { + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted) { + FocusScope.of(context).requestFocus(_searchFocus); + } + }); + }, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), @@ -269,8 +281,12 @@ class _AdvancePaymentScreenState extends State return InkWell( onTap: () { controller.selectEmployee(e); - _searchCtrl.text = e.name; - controller.searchQuery.value = e.name; + _searchCtrl + ..text = e.name + ..selection = TextSelection.fromPosition( + TextPosition(offset: e.name.length), + ); + FocusScope.of(context).unfocus(); SystemChannels.textInput.invokeMethod('TextInput.hide'); controller.employees.clear(); @@ -397,8 +413,8 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - final formattedTime = - parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; + // final formattedTime = + // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; final project = item.name ?? ''; final desc = item.title ?? ''; @@ -429,16 +445,16 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - if (formattedTime.isNotEmpty) ...[ - const SizedBox(width: 6), - Text( - formattedTime, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade500, - fontStyle: FontStyle.italic), - ), - ] + // if (formattedTime.isNotEmpty) ...[ + // const SizedBox(width: 6), + // Text( + // formattedTime, + // style: TextStyle( + // fontSize: 12, + // color: Colors.grey.shade500, + // fontStyle: FontStyle.italic), + // ), + // ] ], ), const SizedBox(height: 4), From 39d0f96ecdfd95941e4ccce48acfc3b4ef0512be Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 12:00:28 +0530 Subject: [PATCH 15/37] unwanted code removed --- lib/view/finance/advance_payment_screen.dart | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 2ca1799..3b12431 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -413,9 +413,7 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - // final formattedTime = - // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; - + final project = item.name ?? ''; final desc = item.title ?? ''; final amount = (item.amount ?? 0).toDouble(); @@ -445,16 +443,7 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - // if (formattedTime.isNotEmpty) ...[ - // const SizedBox(width: 6), - // Text( - // formattedTime, - // style: TextStyle( - // fontSize: 12, - // color: Colors.grey.shade500, - // fontStyle: FontStyle.italic), - // ), - // ] + ], ), const SizedBox(height: 4), From 6162b9be89ee81af5a684ed9c18104cc8f371927 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:04:40 +0530 Subject: [PATCH 16/37] upadted with select enployee sheet --- .../expense/add_expense_controller.dart | 15 +- .../add_payment_request_controller.dart | 15 +- .../assign_task_bottom_sheet .dart | 246 +++++++++--------- .../directory/edit_bucket_bottom_sheet.dart | 181 +++++-------- .../multiple_select_bottomsheet.dart | 30 ++- .../multiple_select_role_bottomsheet.dart | 244 +++++++++++++++++ .../expense/add_expense_bottom_sheet.dart | 61 +++-- .../add_payment_request_bottom_sheet.dart | 108 ++++---- .../payment_request_filter_bottom_sheet.dart | 8 +- .../expense/expense_filter_bottom_sheet.dart | 10 +- 10 files changed, 577 insertions(+), 341 deletions(-) create mode 100644 lib/model/employees/multiple_select_role_bottomsheet.dart diff --git a/lib/controller/expense/add_expense_controller.dart b/lib/controller/expense/add_expense_controller.dart index 2869be3..40d73d3 100644 --- a/lib/controller/expense/add_expense_controller.dart +++ b/lib/controller/expense/add_expense_controller.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -50,10 +51,22 @@ class AddExpenseController extends GetxController { final isEditMode = false.obs; final isSearchingEmployees = false.obs; +// --- Paid By (Single + Multi Selection Support) --- + +// single selection + final selectedPaidBy = Rxn(); + + + +// helper setters + void setSelectedPaidBy(EmployeeModel? emp) { + selectedPaidBy.value = emp; + } + // --- Dropdown Selections & Data --- final selectedPaymentMode = Rxn(); final selectedExpenseType = Rxn(); - final selectedPaidBy = Rxn(); + // final selectedPaidBy = Rxn(); final selectedProject = ''.obs; final selectedTransactionDate = Rxn(); diff --git a/lib/controller/finance/add_payment_request_controller.dart b/lib/controller/finance/add_payment_request_controller.dart index 1ff0a4e..f05ff45 100644 --- a/lib/controller/finance/add_payment_request_controller.dart +++ b/lib/controller/finance/add_payment_request_controller.dart @@ -14,6 +14,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; import 'package:marco/model/finance/expense_category_model.dart'; import 'package:marco/model/finance/currency_list_model.dart'; +import 'package:marco/model/employees/employee_model.dart'; class AddPaymentRequestController extends GetxController { // Loading States @@ -32,7 +33,7 @@ class AddPaymentRequestController extends GetxController { // Selected Values final selectedProject = Rx?>(null); final selectedCategory = Rx(null); - final selectedPayee = ''.obs; + final selectedPayee = Rx(null); final selectedCurrency = Rx(null); final isAdvancePayment = false.obs; final selectedDueDate = Rx(null); @@ -161,7 +162,7 @@ class AddPaymentRequestController extends GetxController { try { final pickedFile = await _picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { - isProcessingAttachment.value = true; + isProcessingAttachment.value = true; File imageFile = File(pickedFile.path); // Add timestamp to the captured image @@ -184,7 +185,7 @@ class AddPaymentRequestController extends GetxController { selectedProject.value = project; void selectCategory(ExpenseCategory category) => selectedCategory.value = category; - void selectPayee(String payee) => selectedPayee.value = payee; + void selectPayee(EmployeeModel payee) => selectedPayee.value = payee; void selectCurrency(Currency currency) => selectedCurrency.value = currency; void addAttachment(File file) => attachments.add(file); @@ -268,7 +269,7 @@ class AddPaymentRequestController extends GetxController { "amount": double.tryParse(amountController.text.trim()) ?? 0, "currencyId": selectedCurrency.value?.id ?? '', "description": descriptionController.text.trim(), - "payee": selectedPayee.value, + "payee": selectedPayee.value?.id ?? "", "dueDate": selectedDueDate.value?.toIso8601String(), "isAdvancePayment": isAdvancePayment.value, "billAttachments": billAttachments.map((a) { @@ -337,7 +338,7 @@ class AddPaymentRequestController extends GetxController { "amount": double.tryParse(amountController.text.trim()) ?? 0, "currencyId": selectedCurrency.value?.id ?? '', "description": descriptionController.text.trim(), - "payee": selectedPayee.value, + "payee": selectedPayee.value?.id ?? "", "dueDate": selectedDueDate.value?.toIso8601String(), "isAdvancePayment": isAdvancePayment.value, "billAttachments": billAttachments.map((a) { @@ -388,7 +389,7 @@ class AddPaymentRequestController extends GetxController { return _errorSnackbar("Please select a project"); if (selectedCategory.value == null) return _errorSnackbar("Please select a category"); - if (selectedPayee.value.isEmpty) + if (selectedPayee.value == null) return _errorSnackbar("Please select a payee"); if (selectedCurrency.value == null) return _errorSnackbar("Please select currency"); @@ -408,7 +409,7 @@ class AddPaymentRequestController extends GetxController { descriptionController.clear(); selectedProject.value = null; selectedCategory.value = null; - selectedPayee.value = ''; + selectedPayee.value = null; selectedCurrency.value = null; isAdvancePayment.value = false; attachments.clear(); diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index fdfcced..5191079 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -1,3 +1,6 @@ +// Updated AssignTaskBottomSheet with bottom sheet height fix +// Only modified layout for employee selection area to prevent overflow. + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; @@ -12,10 +15,8 @@ import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; - -// Added imports for employee selection import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/multiple_select_role_bottomsheet.dart'; class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; @@ -53,9 +54,9 @@ class _AssignTaskBottomSheetState extends State { final TextEditingController targetController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); - final ScrollController _employeeListScrollController = ScrollController(); String? selectedProjectId; + String? selectedRoleId; Organization? selectedOrganization; Service? selectedService; @@ -84,13 +85,14 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, - serviceId: selectedService?.id); + await controller.fetchTaskData( + selectedProjectId, + serviceId: selectedService?.id, + ); } @override void dispose() { - _employeeListScrollController.dispose(); targetController.dispose(); descriptionController.dispose(); super.dispose(); @@ -98,20 +100,21 @@ class _AssignTaskBottomSheetState extends State { @override Widget build(BuildContext context) { - return Obx(() => BaseBottomSheet( - title: "Assign Task", - child: _buildAssignTaskForm(), - onCancel: () => Get.back(), - onSubmit: _onAssignTaskPressed, - isSubmitting: controller.isAssigningTask.value, - )); + return Obx( + () => BaseBottomSheet( + title: "Assign Task", + child: _buildAssignTaskForm(), + onCancel: () => Get.back(), + onSubmit: _onAssignTaskPressed, + isSubmitting: controller.isAssigningTask.value, + ), + ); } Widget _buildAssignTaskForm() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Organization Selector SizedBox( height: 50, child: OrganizationSelector( @@ -123,9 +126,9 @@ class _AssignTaskBottomSheetState extends State { }, ), ), + MySpacing.height(12), - // Service Selector SizedBox( height: 50, child: ServiceSelector( @@ -137,40 +140,27 @@ class _AssignTaskBottomSheetState extends State { }, ), ), + MySpacing.height(16), - - // Work Location Info - _infoRow( - Icons.location_on, - "Work Location", - "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}", - ), + _infoRow(Icons.location_on, "Work Location", + "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), const Divider(), - - // Pending Task Info _infoRow( Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, - child: Row( - children: [ - MyText.titleMedium("Select Team :", fontWeight: 600), - const SizedBox(width: 4), - const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)), - ], - ), + child: Row(children: [ + MyText.titleMedium("Select Team :", fontWeight: 600), + const SizedBox(width: 4), + const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)), + ]), ), + MySpacing.height(8), - // ------------------------------- - // Employee selector (REPLACED) - // ------------------------------- - // We show a button-like container (with border) that opens the reusable - // EmployeeSelectionBottomSheet. Selected employees are reflected using - // existing controller.uploadingStates & controller.selectedEmployees. + /// TEAM SELECT BOX GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( @@ -180,41 +170,47 @@ class _AssignTaskBottomSheetState extends State { borderRadius: BorderRadius.circular(6), ), child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText("Select team members", - color: Colors.grey.shade700); - } - // show summary text when there are selected employees - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - ), + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], +) + ), ), - MySpacing.height(8), - // Selected Employees Chips (keeps existing behavior) + MySpacing.height(8), _buildSelectedEmployees(), MySpacing.height(8), - // Target Input _buildTextField( icon: Icons.track_changes, label: "Target for Today :", @@ -223,9 +219,9 @@ class _AssignTaskBottomSheetState extends State { keyboardType: const TextInputType.numberWithOptions(decimal: true), validatorType: "target", ), + MySpacing.height(16), - // Description Input _buildTextField( icon: Icons.description, label: "Description :", @@ -253,21 +249,21 @@ class _AssignTaskBottomSheetState extends State { ), items: [ const PopupMenuItem(value: 'all', child: Text("All Roles")), - ...controller.roles.map((role) { - return PopupMenuItem( + ...controller.roles.map( + (role) => PopupMenuItem( value: role['id'].toString(), child: Text(role['name'] ?? 'Unknown Role'), - ); - }), + ), + ), ], ).then((value) { - if (value != null) - controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) { + selectedRoleId = value == 'all' ? null : value; + controller.onRoleSelected(selectedRoleId); + } }); } - // Removed old inline employee list; selection handled by bottom sheet. - Widget _buildSelectedEmployees() { return Obx(() { if (controller.selectedEmployees.isEmpty) return Container(); @@ -319,7 +315,9 @@ class _AssignTaskBottomSheetState extends State { maxLines: maxLines, decoration: InputDecoration( hintText: hintText, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), @@ -344,61 +342,62 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", - fontWeight: 600, color: Colors.black), + child: MyText.titleMedium( + "$title: ", + fontWeight: 600, + color: Colors.black, + ), ), TextSpan( - text: value, style: const TextStyle(color: Colors.black)), + text: value, + style: const TextStyle(color: Colors.black), + ), ], ), ), - ), + ) ], ), ); } Future _openEmployeeSelectionSheet() async { - // Open the existing EmployeeSelectionBottomSheet final result = await showModalBottomSheet>( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectionBottomSheet( - initiallySelected: controller.selectedEmployees.toList(), - multipleSelection: true, - title: 'Select Team Members', - ), + 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, + ), + ); + }, + ); + }, ); - if (result == null) return; - - // Merge returned employees into controller.uploadingStates & controller.selectedEmployees - // 1) Reset all uploadingStates to false, then set true for selected - controller.uploadingStates.forEach((key, rx) { - rx.value = false; - }); - - for (final emp in result) { - final idStr = emp.id.toString(); - if (controller.uploadingStates.containsKey(idStr)) { - controller.uploadingStates[idStr]?.value = true; - } else { - // if uploadingStates doesn't have the id yet, add it (safe fallback) - controller.uploadingStates[idStr] = RxBool(true); - } - } - - // 2) Update selectedEmployees list in controller - controller.selectedEmployees.assignAll(result); - - // 3) Call controller helper (keeps existing behavior) - try { + if (result != null) { + controller.selectedEmployees.assignAll(result); controller.updateSelectedEmployees(); - } catch (_) { - // If controller does not implement updateSelectedEmployees, ignore. } } @@ -410,18 +409,20 @@ class _AssignTaskBottomSheetState extends State { if (selectedTeam.isEmpty) { showAppSnackbar( - title: "Team Required", - message: "Please select at least one team member", - type: SnackbarType.error); + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error, + ); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { showAppSnackbar( - title: "Invalid Input", - message: "Please enter a valid target number", - type: SnackbarType.error); + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error, + ); return; } @@ -437,9 +438,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { showAppSnackbar( - title: "Description Required", - message: "Please enter a description", - type: SnackbarType.error); + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error, + ); return; } diff --git a/lib/model/directory/edit_bucket_bottom_sheet.dart b/lib/model/directory/edit_bucket_bottom_sheet.dart index 7347b63..d115377 100644 --- a/lib/model/directory/edit_bucket_bottom_sheet.dart +++ b/lib/model/directory/edit_bucket_bottom_sheet.dart @@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/directory/contact_bucket_list_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class EditBucketBottomSheet { static void show( @@ -21,10 +22,8 @@ class EditBucketBottomSheet { final nameController = TextEditingController(text: bucket.name); final descController = TextEditingController(text: bucket.description); - final searchController = TextEditingController(); final selectedIds = RxSet({...bucket.employeeIds}); - final searchText = ''.obs; InputDecoration _inputDecoration(String label) { return InputDecoration( @@ -84,6 +83,15 @@ class EditBucketBottomSheet { } } + Future _handleSubmitBottomSheet(BuildContext sheetContext) async { + await _handleSubmit(); + + // close bottom sheet safely + if (Navigator.of(sheetContext).canPop()) { + Navigator.of(sheetContext).pop(); + } + } + Widget _formContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -101,117 +109,72 @@ class EditBucketBottomSheet { MySpacing.height(20), MyText.labelLarge('Shared With', fontWeight: 600), MySpacing.height(8), - Obx(() => TextField( - controller: searchController, - onChanged: (value) => searchText.value = value.toLowerCase(), - decoration: InputDecoration( - hintText: 'Search employee...', - prefixIcon: const Icon(Icons.search, size: 20), - suffixIcon: searchText.value.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, size: 18), - onPressed: () { - searchController.clear(); - searchText.value = ''; - }, - ) - : null, - isDense: true, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - filled: true, - fillColor: Colors.grey.shade100, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), - )), - MySpacing.height(8), Obx(() { - final filtered = allEmployees.where((emp) { - final fullName = '${emp.firstName} ${emp.lastName}'.toLowerCase(); - return fullName.contains(searchText.value); - }).toList(); + if (selectedIds.isEmpty) return const SizedBox.shrink(); - return SizedBox( - height: 180, - child: ListView.separated( - itemCount: filtered.length, - separatorBuilder: (_, __) => const SizedBox(height: 2), - itemBuilder: (context, index) { - final emp = filtered[index]; - final fullName = '${emp.firstName} ${emp.lastName}'.trim(); + final selectedEmployees = + allEmployees.where((e) => selectedIds.contains(e.id)).toList(); - return Obx(() => Theme( - data: Theme.of(context).copyWith( - unselectedWidgetColor: Colors.grey.shade500, - checkboxTheme: CheckboxThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4)), - side: const BorderSide(color: Colors.grey), - fillColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; - } - return Colors.white; - }), - checkColor: MaterialStateProperty.all(Colors.white), - ), - ), - child: CheckboxListTile( - dense: true, - contentPadding: EdgeInsets.zero, - visualDensity: const VisualDensity(vertical: -4), - controlAffinity: ListTileControlAffinity.leading, - value: selectedIds.contains(emp.id), - onChanged: emp.id == ownerId - ? null - : (val) { - if (val == true) { - selectedIds.add(emp.id); - } else { - selectedIds.remove(emp.id); - } - }, - title: Row( - children: [ - Expanded( - child: MyText.bodyMedium( - fullName.isNotEmpty ? fullName : 'Unnamed', - fontWeight: 600, - ), - ), - if (emp.id == ownerId) - Container( - margin: const EdgeInsets.only(left: 6), - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(4), - ), - child: MyText.labelSmall( - "Owner", - fontWeight: 600, - color: Colors.red, - ), - ), - ], - ), - subtitle: emp.jobRole.isNotEmpty - ? MyText.bodySmall( - emp.jobRole, - color: Colors.grey.shade600, - ) - : null, - ), - )); - }, - ), + return Wrap( + spacing: 8, + children: selectedEmployees.map((emp) { + final fullName = '${emp.firstName} ${emp.lastName}'.trim(); + + return Chip( + label: Text(fullName), + onDeleted: emp.id == ownerId + ? null + : () => selectedIds.remove(emp.id), + ); + }).toList(), ); }), + MySpacing.height(8), + +// --- Open new EmployeeSelectionBottomSheet --- + GestureDetector( + onTap: () async { + final initiallySelected = allEmployees + .where((e) => selectedIds.contains(e.id)) + .toList(); + + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(22)), + ), + builder: (_) => EmployeeSelectionBottomSheet( + initiallySelected: initiallySelected, + multipleSelection: true, + title: "Shared With", + ), + ); + + if (result != null) { + selectedIds + ..clear() + ..addAll(result.map((e) => e.id)); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: const [ + Icon(Icons.search, color: Colors.grey), + SizedBox(width: 8), + Expanded(child: Text("Search & Select Employees")), + ], + ), + ), + ), + MySpacing.height(8), + const SizedBox.shrink(), ], ); } @@ -224,7 +187,7 @@ class EditBucketBottomSheet { return BaseBottomSheet( title: "Edit Bucket", onCancel: () => Navigator.pop(context), - onSubmit: _handleSubmit, + onSubmit: () => _handleSubmitBottomSheet(context), child: _formContent(), ); }, diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 0e11530..f96e361 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -159,6 +159,26 @@ class _EmployeeSelectionBottomSheetState final emp = results[index]; final isSelected = _selectedEmployees.contains(emp); + Widget trailingWidget; + + if (widget.multipleSelection) { + // Multiple selection β†’ normal checkbox + trailingWidget = Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, + ), + ); + } else { + // Single selection β†’ check circle + trailingWidget = isSelected + ? const Icon(Icons.check_circle, color: Colors.blueAccent) + : const Icon(Icons.circle_outlined, color: Colors.grey); + } + return ListTile( leading: CircleAvatar( backgroundColor: Colors.blueAccent, @@ -170,15 +190,7 @@ class _EmployeeSelectionBottomSheetState ), title: Text('${emp.firstName} ${emp.lastName}'), subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) => _toggleEmployee(emp), - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), - ), + trailing: trailingWidget, onTap: () => _toggleEmployee(emp), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart new file mode 100644 index 0000000..1ffd1e0 --- /dev/null +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; + +class MultipleSelectRoleBottomSheet extends StatefulWidget { + final String title; + final bool multipleSelection; + + final String projectId; + final String? serviceId; + final String? organizationId; + final String? roleId; + final ScrollController? scrollController; + + final List initiallySelected; + + const MultipleSelectRoleBottomSheet({ + super.key, + this.title = "Select Employees", + this.multipleSelection = true, + required this.projectId, + this.serviceId, + this.organizationId, + this.roleId, + this.initiallySelected = const [], + this.scrollController, + }); + + @override + State createState() => + _MultipleSelectRoleBottomSheetState(); +} + +class _MultipleSelectRoleBottomSheetState + extends State { + final RxList _employees = [].obs; + final RxList _filtered = [].obs; + final RxBool _isLoading = true.obs; + + late RxList _selected; + final TextEditingController _searchController = TextEditingController(); + + late DailyTaskPlanningController controller; + + @override + void initState() { + super.initState(); + _selected = widget.initiallySelected.obs; + controller = Get.find(); + _fetchEmployeesFiltered(); + } + + Future _fetchEmployeesFiltered() async { + _isLoading.value = true; + try { + List employees = controller.employees.toList(); + + if (widget.roleId != null && widget.roleId!.isNotEmpty) { + employees = employees + .where((emp) => emp.jobRoleID == widget.roleId) + .toList(); + } + + // Selected first + employees.sort((a, b) { + final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; + final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; + return aSel != bSel + ? aSel.compareTo(bSel) + : a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _employees.assignAll(employees); + _filtered.assignAll(employees); + } catch (e) { + print("Error fetching employees: $e"); + } finally { + _isLoading.value = false; + } + } + + void _onSearch(String text) { + if (text.isEmpty) { + _filtered.assignAll(_employees); + } else { + _filtered.assignAll( + _employees.where((e) => + e.name.toLowerCase().contains(text.toLowerCase()) || + e.designation.toLowerCase().contains(text.toLowerCase())), + ); + } + + // Selected on top + _filtered.sort((a, b) { + final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; + final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; + return aSel != bSel + ? aSel.compareTo(bSel) + : a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + } + + void _onTap(EmployeeModel emp) { + if (widget.multipleSelection) { + if (_selected.any((e) => e.id == emp.id)) { + _selected.removeWhere((e) => e.id == emp.id); + } else { + _selected.add(emp); + } + } else { + _selected.assignAll([emp]); + Get.back(result: _selected); + } + + _onSearch(_searchController.text.trim()); + } + + bool _isSelected(EmployeeModel emp) { + return _selected.any((e) => e.id == emp.id); + } + + Widget _searchBar() => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextField( + controller: _searchController, + onChanged: _onSearch, + decoration: InputDecoration( + hintText: 'Search employees...', + filled: true, + fillColor: Colors.grey.shade100, + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: () { + _searchController.clear(); + _onSearch(''); + }, + ) + : null, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + ), + ); + + /// ⭐ NEW β€” Chips showing selected employees + Widget _selectedChips() { + return Obx(() { + if (_selected.isEmpty) return const SizedBox(); + + return Wrap( + spacing: 8, + runSpacing: 8, + children: _selected.map((emp) { + return Chip( + label: Text(emp.name), + deleteIcon: const Icon(Icons.close), + onDeleted: () { + _selected.remove(emp); + _onSearch(_searchController.text.trim()); + }, + backgroundColor: Colors.blue.shade50, + ); + }).toList(), + ); + }); + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: widget.title, + onCancel: () => Get.back(), + onSubmit: () => Get.back(result: _selected), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.55, + child: Column( + children: [ + _searchBar(), + + /// ⭐ Chips shown right below search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _selectedChips(), + ), + + const SizedBox(height: 6), + + Expanded( + child: Obx(() { + if (_isLoading.value) { + return SkeletonLoaders.employeeSkeletonCard(); + } + + if (_filtered.isEmpty) { + return const Center(child: Text("No employees found")); + } + + return ListView.builder( + controller: widget.scrollController, + padding: const EdgeInsets.only(bottom: 20), + itemCount: _filtered.length, + itemBuilder: (_, index) { + final emp = _filtered[index]; + final isSelected = _isSelected(emp); + + return ListTile( + onTap: () => _onTap(emp), + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + style: const TextStyle(color: Colors.white), + ), + ), + title: Text(emp.name), + subtitle: Text(emp.designation), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _onTap(emp), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + ); + }, + ); + }), + ), + ], + ), + ), + ); + } +} diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 5659de7..88ef2ec 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -5,13 +5,15 @@ import 'package:marco/controller/expense/add_expense_controller.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/model/expense/expense_type_model.dart'; import 'package:marco/model/expense/payment_types_model.dart'; -import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; +// import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; import 'package:marco/helpers/utils/base_bottom_sheet.dart'; import 'package:marco/helpers/utils/validators.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; /// Show bottom sheet wrapper Future showAddExpenseBottomSheet({ @@ -52,24 +54,36 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> /// Show employee list Future _showEmployeeList() async { - await showModalBottomSheet( + final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (_) => ReusableEmployeeSelectorBottomSheet( - searchController: controller.employeeSearchController, - searchResults: controller.employeeSearchResults, - isSearching: controller.isSearchingEmployees, - onSearch: controller.searchEmployees, - onSelect: (emp) => controller.selectedPaidBy.value = emp, + builder: (_) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedPaidBy.value != null + ? [controller.selectedPaidBy.value!] + : [], + multipleSelection: false, + title: "Select Paid By", ), ); - controller.employeeSearchController.clear(); - controller.employeeSearchResults.clear(); + if (result == null) return; + + // result will be EmployeeModel or [EmployeeModel] + if (result is EmployeeModel) { + controller.setSelectedPaidBy(result); + } else if (result is List && result.isNotEmpty) { + controller.setSelectedPaidBy(result.first as EmployeeModel); + } + + // cleanup + try { + controller.employeeSearchController.clear(); + controller.employeeSearchResults.clear(); + } catch (_) {} } /// Generic option list @@ -343,23 +357,26 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> const SectionTitle( icon: Icons.person_outline, title: "Paid By", requiredField: true), MySpacing.height(6), + // Main tile: tap to choose mode + selection sheet GestureDetector( onTap: _showEmployeeList, child: TileContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - controller.selectedPaidBy.value == null - ? "Select Paid By" - : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', - style: const TextStyle(fontSize: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + controller.selectedPaidBy.value?.name ?? "Select Paid By", + style: TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, ), - const Icon(Icons.arrow_drop_down, size: 22), - ], - ), - ), + ), + Icon(Icons.arrow_drop_down, size: 22), + ], + )), ), + // small helper: long-press to quickly open multi-select directly (optional) + const SizedBox(height: 6), ], ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index abe1527..a23bfcf 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -9,6 +9,8 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; Future showPaymentRequestBottomSheet({ bool isEdit = false, @@ -206,7 +208,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ? null : "Enter valid amount"), _gap(), - _buildPayeeAutocompleteField(), + _buildPayeeField(), _gap(), _buildDropdown( "Currency", @@ -347,74 +349,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ); } - Widget _buildPayeeAutocompleteField() { + Widget _buildPayeeField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SectionTitle( - icon: Icons.person_outline, title: "Payee", requiredField: true), - const SizedBox(height: 6), - Autocomplete( - optionsBuilder: (textEditingValue) { - final query = textEditingValue.text.toLowerCase(); - return query.isEmpty - ? const Iterable.empty() - : controller.payees - .where((p) => p.toLowerCase().contains(query)); - }, - displayStringForOption: (option) => option, - fieldViewBuilder: - (context, fieldController, focusNode, onFieldSubmitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (fieldController.text != controller.selectedPayee.value) { - fieldController.text = controller.selectedPayee.value; - fieldController.selection = TextSelection.fromPosition( - TextPosition(offset: fieldController.text.length)); - } - }); - - return TextFormField( - controller: fieldController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: "Type or select payee", - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), + const SectionTitle( + icon: Icons.person_outline, + title: "Payee", + requiredField: true, + ), + MySpacing.height(6), + GestureDetector( + onTap: _showPayeeSelector, + child: TileContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Obx(() => Text( + controller.selectedPayee.value?.name ?? "Select Payee", + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + )), ), - ), - validator: (v) => - v == null || v.trim().isEmpty ? "Please enter payee" : null, - onChanged: (val) => controller.selectedPayee.value = val, - ); - }, - onSelected: (selection) => controller.selectedPayee.value = selection, - optionsViewBuilder: (context, onSelected, options) => Material( - color: Colors.white, - elevation: 4, - borderRadius: BorderRadius.circular(8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (_, index) => InkWell( - onTap: () => onSelected(options.elementAt(index)), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 12), - child: Text(options.elementAt(index), - style: const TextStyle(fontSize: 14)), - ), - ), - ), + const Icon(Icons.arrow_drop_down, size: 22), + ], ), ), ), + const SizedBox(height: 6), ], ); } @@ -533,7 +496,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (controller.selectedCategory.value == null) { return _showError("Please select a category"); } - if (controller.selectedPayee.value.isEmpty) { + if (controller.selectedPayee.value == null) { return _showError("Please select a payee"); } if (controller.selectedCurrency.value == null) { @@ -542,6 +505,25 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> return true; } + Future _showPayeeSelector() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EmployeeSelectionBottomSheet( + title: "Select Payee", + multipleSelection: false, + initiallySelected: controller.selectedPayee.value != null + ? [controller.selectedPayee.value!] + : [], + ), + ); + + if (result is EmployeeModel) { + controller.selectedPayee.value = result; + } + } + bool _showError(String msg) { showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error); return false; diff --git a/lib/model/finance/payment_request_filter_bottom_sheet.dart b/lib/model/finance/payment_request_filter_bottom_sheet.dart index 8d680b2..737495d 100644 --- a/lib/model/finance/payment_request_filter_bottom_sheet.dart +++ b/lib/model/finance/payment_request_filter_bottom_sheet.dart @@ -8,7 +8,7 @@ import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/date_range_picker.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class PaymentRequestFilterBottomSheet extends StatefulWidget { final PaymentRequestController controller; @@ -441,9 +441,9 @@ class _PaymentRequestFilterBottomSheetState shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: (query) => searchEmployees(query, items), + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: selectedEmployees.toList(), + multipleSelection: true, title: title, ), ); diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index 0c6d565..380aed2 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -8,9 +8,10 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/date_range_picker.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + class ExpenseFilterBottomSheet extends StatefulWidget { final ExpenseController expenseController; @@ -303,12 +304,13 @@ class _ExpenseFilterBottomSheetState extends State shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: searchEmployees, + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: selectedEmployees.toList(), + multipleSelection: true, title: title, ), ); + if (result != null) selectedEmployees.assignAll(result); }, child: Container( From 84395765fde53a42c207eb538b1e8564fbea12a2 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:17:00 +0530 Subject: [PATCH 17/37] check box color change to white --- .../multiple_select_role_bottomsheet.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index 1ffd1e0..f7e5dff 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -59,9 +59,8 @@ class _MultipleSelectRoleBottomSheetState List employees = controller.employees.toList(); if (widget.roleId != null && widget.roleId!.isNotEmpty) { - employees = employees - .where((emp) => emp.jobRoleID == widget.roleId) - .toList(); + employees = + employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } // Selected first @@ -226,6 +225,16 @@ class _MultipleSelectRoleBottomSheetState trailing: Checkbox( value: isSelected, onChanged: (_) => _onTap(emp), + fillColor: + MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.blueAccent; // Selected color + } + return Colors.white; // Unselected square color + }), + checkColor: Colors.white, // Check mark color + side: const BorderSide( + color: Colors.grey), // Outline for unselected ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From 19e6979c0e358f556a668d2351e60ef13db320e6 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 17:36:56 +0530 Subject: [PATCH 18/37] UI enhancement --- .../assign_task_bottom_sheet .dart | 106 ++++++++---------- .../multiple_select_bottomsheet.dart | 13 ++- .../multiple_select_role_bottomsheet.dart | 49 ++------ 3 files changed, 63 insertions(+), 105 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 5191079..ce19774 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -164,47 +164,45 @@ class _AssignTaskBottomSheetState extends State { GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), - // Expanded name area - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText( - "Select team members", - color: Colors.grey.shade700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], -) - - ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + )), ), MySpacing.height(8), @@ -272,20 +270,14 @@ class _AssignTaskBottomSheetState extends State { spacing: 4, runSpacing: 4, children: controller.selectedEmployees.map((e) { - return Obx(() { - final isSelected = controller.uploadingStates[e.id]?.value ?? false; - if (!isSelected) return Container(); - - return Chip( - label: Text(e.name, style: const TextStyle(color: Colors.white)), - backgroundColor: const Color.fromARGB(255, 95, 132, 255), - deleteIcon: const Icon(Icons.close, color: Colors.white), - onDeleted: () { - controller.uploadingStates[e.id]?.value = false; - controller.updateSelectedEmployees(); - }, - ); - }); + return Chip( + label: Text(e.name, style: const TextStyle(color: Colors.white)), + backgroundColor: const Color.fromARGB(255, 95, 132, 255), + deleteIcon: const Icon(Icons.close, color: Colors.white), + onDeleted: () { + controller.selectedEmployees.remove(e); + }, + ); }).toList(), ); }); @@ -380,7 +372,6 @@ class _AssignTaskBottomSheetState extends State { color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - child: MultipleSelectRoleBottomSheet( projectId: selectedProjectId!, organizationId: selectedOrganization?.id, @@ -396,16 +387,13 @@ class _AssignTaskBottomSheetState extends State { ); if (result != null) { - controller.selectedEmployees.assignAll(result); - controller.updateSelectedEmployees(); + controller.selectedEmployees + .assignAll(result); // RxList updates UI automatically } } void _onAssignTaskPressed() { - final selectedTeam = controller.uploadingStates.entries - .where((e) => e.value.value) - .map((e) => e.key) - .toList(); + final selectedTeam = controller.selectedEmployees; if (selectedTeam.isEmpty) { showAppSnackbar( @@ -449,7 +437,7 @@ class _AssignTaskBottomSheetState extends State { workItemId: widget.workItemId, plannedTask: target.toInt(), description: description, - taskTeam: selectedTeam, + taskTeam: selectedTeam.map((e) => e.id).toList(), // pass IDs assignmentDate: widget.assignmentDate, organizationId: selectedOrganization?.id, serviceId: selectedService?.id, diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index f96e361..1509cf9 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -68,13 +68,18 @@ class _EmployeeSelectionBottomSheetState .toList(); // ------------------------------------------------------ - // πŸ”₯ Auto-move selected employees to top + // Auto-move selected employees to top // ------------------------------------------------------ results.sort((a, b) { - final aSel = _selectedEmployees.contains(a) ? 0 : 1; - final bSel = _selectedEmployees.contains(b) ? 0 : 1; + if (widget.multipleSelection) { + // Only move selected employees to top in multi-select + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; - if (aSel != bSel) return aSel.compareTo(bSel); + if (aSel != bSel) return aSel.compareTo(bSel); + } + + // Otherwise, keep original order (or alphabetically if needed) return a.name.toLowerCase().compareTo(b.name.toLowerCase()); }); diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index f7e5dff..a1f4951 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -63,7 +63,6 @@ class _MultipleSelectRoleBottomSheetState employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } - // Selected first employees.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -92,7 +91,6 @@ class _MultipleSelectRoleBottomSheetState ); } - // Selected on top _filtered.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -110,8 +108,8 @@ class _MultipleSelectRoleBottomSheetState _selected.add(emp); } } else { - _selected.assignAll([emp]); - Get.back(result: _selected); + // Single selection β†’ return immediately + Get.back(result: [emp]); } _onSearch(_searchController.text.trim()); @@ -150,49 +148,17 @@ class _MultipleSelectRoleBottomSheetState ), ); - /// ⭐ NEW β€” Chips showing selected employees - Widget _selectedChips() { - return Obx(() { - if (_selected.isEmpty) return const SizedBox(); - - return Wrap( - spacing: 8, - runSpacing: 8, - children: _selected.map((emp) { - return Chip( - label: Text(emp.name), - deleteIcon: const Icon(Icons.close), - onDeleted: () { - _selected.remove(emp); - _onSearch(_searchController.text.trim()); - }, - backgroundColor: Colors.blue.shade50, - ); - }).toList(), - ); - }); - } - @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Get.back(), - onSubmit: () => Get.back(result: _selected), + onSubmit: () => Get.back(result: _selected.toList()), // Return plain list child: SizedBox( height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ _searchBar(), - - /// ⭐ Chips shown right below search bar - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: _selectedChips(), - ), - - const SizedBox(height: 6), - Expanded( child: Obx(() { if (_isLoading.value) { @@ -228,13 +194,12 @@ class _MultipleSelectRoleBottomSheetState fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; // Selected color + return Colors.blueAccent; } - return Colors.white; // Unselected square color + return Colors.white; }), - checkColor: Colors.white, // Check mark color - side: const BorderSide( - color: Colors.grey), // Outline for unselected + checkColor: Colors.white, + side: const BorderSide(color: Colors.grey), ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From 3ff97eb0d84c9655ca8ad4ceda95f10181f53806 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 24 Nov 2025 10:16:41 +0530 Subject: [PATCH 19/37] done tags UX updates --- .../add_service_project_job_controller.dart | 4 +- .../add_service_project_job_bottom_sheet.dart | 34 +++++++++++++-- lib/view/service_project/jobs_tab.dart | 4 +- .../service_project_job_detail_screen.dart | 43 ++++++++++++++++--- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/lib/controller/service_project/add_service_project_job_controller.dart b/lib/controller/service_project/add_service_project_job_controller.dart index cf29f3f..586e411 100644 --- a/lib/controller/service_project/add_service_project_job_controller.dart +++ b/lib/controller/service_project/add_service_project_job_controller.dart @@ -71,11 +71,11 @@ class AddServiceProjectJobController extends GetxController { title: titleCtrl.text.trim(), description: descCtrl.text.trim(), projectId: projectId, - branchId: selectedBranch.value?.id, + branchId: selectedBranch.value?.id, assignees: assigneeIds.map((id) => {"id": id}).toList(), startDate: startDate.value!, dueDate: dueDate.value!, - tags: enteredTags.map((tag) => {"name": tag}).toList(), + tags: enteredTags.map((tag) => {"name": tag.trim()}).toList(), ); isLoading.value = false; diff --git a/lib/model/service_project/add_service_project_job_bottom_sheet.dart b/lib/model/service_project/add_service_project_job_bottom_sheet.dart index a08e0be..311f67f 100644 --- a/lib/model/service_project/add_service_project_job_bottom_sheet.dart +++ b/lib/model/service_project/add_service_project_job_bottom_sheet.dart @@ -204,11 +204,37 @@ class _AddServiceProjectJobBottomSheetState height: 48, child: TextFormField( controller: controller.tagCtrl, + textInputAction: TextInputAction.done, + onEditingComplete: () { + final raw = controller.tagCtrl.text.trim(); + if (raw.isEmpty) return; + + final parts = raw + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty); + + for (final p in parts) { + if (!controller.enteredTags.contains(p)) { + controller.enteredTags.add(p); + } + } + controller.tagCtrl.clear(); + }, onFieldSubmitted: (v) { - final value = v.trim(); - if (value.isNotEmpty && - !controller.enteredTags.contains(value)) { - controller.enteredTags.add(value); + // also handle normal submit + final raw = v.trim(); + if (raw.isEmpty) return; + + final parts = raw + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty); + + for (final p in parts) { + if (!controller.enteredTags.contains(p)) { + controller.enteredTags.add(p); + } } controller.tagCtrl.clear(); }, diff --git a/lib/view/service_project/jobs_tab.dart b/lib/view/service_project/jobs_tab.dart index bc18038..bacc113 100644 --- a/lib/view/service_project/jobs_tab.dart +++ b/lib/view/service_project/jobs_tab.dart @@ -200,7 +200,7 @@ class _JobsTabState extends State { children: job.tags!.map((tag) { return Chip( label: Text( - tag.name, + tag.name.replaceAll('_', ' '), style: const TextStyle(fontSize: 12), ), @@ -317,7 +317,7 @@ class _JobsTabState extends State { final success = await ApiService.editServiceProjectJobApi( - jobId: job.id , + jobId: job.id, operations: operations, ); diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart index 34ac74a..4fdc076 100644 --- a/lib/view/service_project/service_project_job_detail_screen.dart +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -38,6 +38,10 @@ class _JobDetailsScreenState extends State with UIMixin { final TextEditingController _dueDateController = TextEditingController(); final TextEditingController _tagTextController = TextEditingController(); + final RxList tags = [].obs; // For showing/editing tag chips + final TextEditingController tagController = + TextEditingController(); // For tag input + final RxList _selectedAssignees = [].obs; final RxList _selectedTags = [].obs; final RxBool isEditing = false.obs; @@ -46,21 +50,38 @@ class _JobDetailsScreenState extends State with UIMixin { @override void initState() { super.initState(); - controller = Get.put(ServiceProjectDetailsController()); - controller.fetchJobDetail(widget.jobId).then((_) { + controller = Get.find(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await controller.fetchJobDetail(widget.jobId); + final job = controller.jobDetail.value?.data; if (job != null) { _titleController.text = job.title ?? ''; _descriptionController.text = job.description ?? ''; + _startDateController.text = DateTimeUtils.convertUtcToLocal( job.startDate ?? DateTime.now().toIso8601String(), format: "yyyy-MM-dd"); + _dueDateController.text = DateTimeUtils.convertUtcToLocal( job.dueDate ?? '', format: "yyyy-MM-dd"); + _selectedAssignees.value = job.assignees ?? []; - _selectedTags.value = job.tags ?? []; + + // ---------- TAG FIX ---------- + final tagList = job.tags ?? []; + + final cleanedTags = tagList + .map((t) => (t.name ?? "").replaceAll("_", " ").trim()) + .toList(); + + tags.assignAll(cleanedTags); + tagController.clear(); } + + setState(() {}); }); } @@ -164,11 +185,21 @@ class _JobDetailsScreenState extends State with UIMixin { if (success) { showAppSnackbar( - title: "Success", - message: "Job updated successfully", - type: SnackbarType.success); + title: "Success", + message: "Job updated successfully", + type: SnackbarType.success, + ); + + /// Refresh detail screen await controller.fetchJobDetail(widget.jobId); + + /// πŸ”₯ Auto refresh job list UI (main Service Project Details screen) + if (Get.isRegistered()) { + await Get.find().refreshJobsAfterAdd(); + } + isEditing.value = false; + Navigator.pop(context); // optional if you want auto-close } else { showAppSnackbar( title: "Error", From c6419c0737d9f5c5530baeb59330d20834f5ac8a Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 10:50:50 +0530 Subject: [PATCH 20/37] done landscape responsive of all screen --- .../expense/expense_main_components.dart | 179 ++++++---- .../attendance/attendence_filter_sheet.dart | 69 ++-- .../add_payment_request_bottom_sheet.dart | 176 +++++----- lib/view/directory/directory_main_screen.dart | 76 ++-- lib/view/finance/advance_payment_screen.dart | 93 +++-- lib/view/finance/finance_screen.dart | 328 ++++++++++-------- lib/view/finance/payment_request_screen.dart | 103 ++++-- 7 files changed, 606 insertions(+), 418 deletions(-) diff --git a/lib/helpers/widgets/expense/expense_main_components.dart b/lib/helpers/widgets/expense/expense_main_components.dart index dccfff1..e000f68 100644 --- a/lib/helpers/widgets/expense/expense_main_components.dart +++ b/lib/helpers/widgets/expense/expense_main_components.dart @@ -282,86 +282,129 @@ class ExpenseList extends StatelessWidget { @override Widget build(BuildContext context) { - if (expenseList.isEmpty && !Get.find().isLoading.value) { - return Center(child: MyText.bodyMedium('No expenses found.')); - } + final ExpenseController controller = Get.find(); - 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( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - 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, + if (controller.isLoading.value && expenseList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (expenseList.isEmpty) { + return const Center( + child: Text( + 'No expenses found.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + // PORTRAIT MODE + if (!isLandscape) { + return ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ); + } + + // LANDSCAPE β†’ WRAP IN SCROLL FOR SAFETY + return SingleChildScrollView( + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 80), + itemCount: expenseList.length, + separatorBuilder: (_, __) => + Divider(color: Colors.grey.shade300, height: 20), + itemBuilder: _buildItem, + ), + ), + ); + }, + ), + ); + } + + Widget _buildItem(BuildContext context, int 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 { + final result = await Get.to( + () => ExpenseDetailScreen(expenseId: expense.id), + arguments: {'expense': expense}, + ); + if (result == true && onViewDetail != null) { + await onViewDetail!(); + } + }, + 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( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - MyText.bodyMedium(expense.expenseCategory.name, + MyText.bodyMedium('${expense.formattedAmount}', 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), + 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), ), - 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, + ), + ), + ], + ), + ], ), - ); - }, + ), + ), ); } } diff --git a/lib/model/attendance/attendence_filter_sheet.dart b/lib/model/attendance/attendence_filter_sheet.dart index 38e1fc8..628e8dd 100644 --- a/lib/model/attendance/attendence_filter_sheet.dart +++ b/lib/model/attendance/attendence_filter_sheet.dart @@ -123,7 +123,6 @@ class _AttendanceFilterBottomSheetState }).toList(); final List widgets = [ - // πŸ”Ή View Section Padding( padding: const EdgeInsets.only(bottom: 4), child: Align( @@ -146,7 +145,6 @@ class _AttendanceFilterBottomSheetState }), ]; - // πŸ”Ή Organization filter widgets.addAll([ const Divider(), Padding( @@ -165,24 +163,6 @@ class _AttendanceFilterBottomSheetState color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - width: 100, - height: 14, - color: Colors.grey.shade400, - ), - Container( - width: 18, - height: 18, - decoration: BoxDecoration( - color: Colors.grey.shade400, - shape: BoxShape.circle, - ), - ), - ], - ), ); } else if (widget.controller.organizations.isEmpty) { return Center( @@ -200,7 +180,6 @@ class _AttendanceFilterBottomSheetState }), ]); - // πŸ”Ή Date Range (only for Attendance Logs) if (tempSelectedTab == 'attendanceLogs') { widgets.addAll([ const Divider(), @@ -211,16 +190,12 @@ class _AttendanceFilterBottomSheetState child: MyText.titleSmall("Date Range", fontWeight: 600), ), ), - // βœ… Reusable DateRangePickerWidget DateRangePickerWidget( startDate: widget.controller.startDateAttendance, endDate: widget.controller.endDateAttendance, startLabel: "Start Date", endLabel: "End Date", - onDateRangeSelected: (start, end) { - // Optional: trigger UI updates if needed - setState(() {}); - }, + onDateRangeSelected: (_, __) => setState(() {}), ), ]); } @@ -232,18 +207,36 @@ class _AttendanceFilterBottomSheetState Widget build(BuildContext context) { return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: BaseBottomSheet( - title: "Attendance Filter", - submitText: "Apply", - onCancel: () => Navigator.pop(context), - onSubmit: () => Navigator.pop(context, { - 'selectedTab': tempSelectedTab, - 'selectedOrganization': widget.controller.selectedOrganization?.id, - }), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: buildMainFilters(), - ), + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = constraints.maxWidth > constraints.maxHeight; + + return BaseBottomSheet( + title: "Attendance Filter", + submitText: "Apply", + onCancel: () => Navigator.pop(context), + onSubmit: () => Navigator.pop(context, { + 'selectedTab': tempSelectedTab, + 'selectedOrganization': + widget.controller.selectedOrganization?.id, + }), + + // ---------------- UPDATED RESPONSIVE CHILD ---------------- + child: SizedBox( + height: isLandscape + ? constraints.maxHeight // πŸ”₯ Full screen in landscape + : constraints.maxHeight * 0.78, // normal in portrait + + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: buildMainFilters(), + ), + ), + ), + ); + }, ), ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index b27dde4..630885d 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -58,12 +58,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (widget.isEdit && widget.existingData != null) { final data = widget.existingData!; - // Prefill text fields controller.titleController.text = data["title"] ?? ""; controller.amountController.text = data["amount"]?.toString() ?? ""; controller.descriptionController.text = data["description"] ?? ""; - // Prefill due date if (data["dueDate"] != null && data["dueDate"].toString().isNotEmpty) { DateTime? dueDate = DateTime.tryParse(data["dueDate"].toString()); if (dueDate != null) { @@ -73,15 +71,14 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> } } - // Prefill dropdowns & toggles controller.selectedProject.value = { 'id': data["projectId"], 'name': data["projectName"], }; + controller.selectedPayee.value = data["payee"] ?? ""; controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; - // Categories & currencies everAll([controller.categories, controller.currencies], (_) { controller.selectedCategory.value = controller.categories .firstWhereOrNull((c) => c.id == data["expenseCategoryId"]); @@ -89,7 +86,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> .firstWhereOrNull((c) => c.id == data["currencyId"]); }); - // Attachments final attachmentsData = data["attachments"]; if (attachmentsData != null && attachmentsData is List && @@ -116,51 +112,56 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> @override Widget build(BuildContext context) { - return Obx(() => Form( - key: _formKey, - child: BaseBottomSheet( - title: widget.isEdit - ? "Edit Payment Request" - : "Create Payment Request", - isSubmitting: controller.isSubmitting.value, - onCancel: Get.back, - submitText: "Save as Draft", - onSubmit: () async { - if (_formKey.currentState!.validate() && _validateSelections()) { - bool success = false; - if (widget.isEdit && widget.existingData != null) { - final requestId = - widget.existingData!['id']?.toString() ?? ''; - if (requestId.isNotEmpty) { - success = await controller.submitEditedPaymentRequest( - requestId: requestId); + return Obx(() => SafeArea( + child: Form( + key: _formKey, + child: BaseBottomSheet( + title: widget.isEdit + ? "Edit Payment Request" + : "Create Payment Request", + isSubmitting: controller.isSubmitting.value, + onCancel: Get.back, + submitText: "Save as Draft", + onSubmit: () async { + if (_formKey.currentState!.validate() && + _validateSelections()) { + bool success = false; + + if (widget.isEdit && widget.existingData != null) { + final requestId = + widget.existingData!['id']?.toString() ?? ''; + if (requestId.isNotEmpty) { + success = await controller.submitEditedPaymentRequest( + requestId: requestId); + } else { + _showError("Invalid Payment Request ID"); + return; + } } else { - _showError("Invalid Payment Request ID"); - return; + success = await controller.submitPaymentRequest(); } - } else { - success = await controller.submitPaymentRequest(); - } - if (success) { - Get.back(); - if (widget.onUpdated != null) widget.onUpdated!(); + if (success) { + Get.back(); + widget.onUpdated?.call(); - showAppSnackbar( - title: "Success", - message: widget.isEdit - ? "Payment request updated successfully!" - : "Payment request created successfully!", - type: SnackbarType.success, - ); + showAppSnackbar( + title: "Success", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", + type: SnackbarType.success, + ); + } } - } - }, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDropdown( + }, + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDropdown( "Select Project", Icons.work_outline, controller.selectedProject.value?['name'] ?? @@ -168,9 +169,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.globalProjects, (p) => p['name'], controller.selectProject, - key: _projectDropdownKey), - _gap(), - _buildDropdown( + key: _projectDropdownKey, + ), + _gap(), + _buildDropdown( "Expense Category", Icons.category_outlined, controller.selectedCategory.value?.name ?? @@ -178,30 +180,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.categories, (c) => c.name, controller.selectCategory, - key: _categoryDropdownKey), - _gap(), - _buildTextField( - "Title", Icons.title_outlined, controller.titleController, - hint: "Enter title", validator: Validators.requiredField), - _gap(), - _buildRadio("Is Advance Payment", Icons.attach_money_outlined, - controller.isAdvancePayment, ["Yes", "No"]), - _gap(), - _buildDueDateField(), - _gap(), - _buildTextField("Amount", Icons.currency_rupee, - controller.amountController, - hint: "Enter Amount", - keyboardType: TextInputType.number, - validator: (v) => (v != null && - v.isNotEmpty && - double.tryParse(v) != null) - ? null - : "Enter valid amount"), - _gap(), - _buildPayeeAutocompleteField(), - _gap(), - _buildDropdown( + key: _categoryDropdownKey, + ), + _gap(), + _buildTextField("Title", Icons.title_outlined, + controller.titleController, + hint: "Enter title", + validator: Validators.requiredField), + _gap(), + _buildRadio( + "Is Advance Payment", + Icons.attach_money_outlined, + controller.isAdvancePayment, + ["Yes", "No"]), + _gap(), + _buildDueDateField(), + _gap(), + _buildTextField("Amount", Icons.currency_rupee, + controller.amountController, + hint: "Enter Amount", + keyboardType: TextInputType.number, + validator: (v) => (v != null && + v.isNotEmpty && + double.tryParse(v) != null) + ? null + : "Enter valid amount"), + _gap(), + _buildPayeeAutocompleteField(), + _gap(), + _buildDropdown( "Currency", Icons.monetization_on_outlined, controller.selectedCurrency.value?.currencyName ?? @@ -209,16 +216,19 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> controller.currencies, (c) => c.currencyName, controller.selectCurrency, - key: _currencyDropdownKey), - _gap(), - _buildTextField("Description", Icons.description_outlined, - controller.descriptionController, - hint: "Enter description", - maxLines: 3, - validator: Validators.requiredField), - _gap(), - _buildAttachmentsSection(), - ], + key: _currencyDropdownKey, + ), + _gap(), + _buildTextField("Description", Icons.description_outlined, + controller.descriptionController, + hint: "Enter description", + maxLines: 3, + validator: Validators.requiredField), + _gap(), + _buildAttachmentsSection(), + MySpacing.height(30), + ], + ), ), ), ), @@ -284,6 +294,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final i = entry.key; final label = entry.value; final value = i == 0; + return Expanded( child: RadioListTile( contentPadding: EdgeInsets.zero, @@ -354,7 +365,6 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> displayStringForOption: (option) => option, fieldViewBuilder: (context, fieldController, focusNode, onFieldSubmitted) { - // Avoid updating during build WidgetsBinding.instance.addPostFrameCallback((_) { if (fieldController.text != controller.selectedPayee.value) { fieldController.text = controller.selectedPayee.value; diff --git a/lib/view/directory/directory_main_screen.dart b/lib/view/directory/directory_main_screen.dart index 473efaf..9d44da0 100644 --- a/lib/view/directory/directory_main_screen.dart +++ b/lib/view/directory/directory_main_screen.dart @@ -99,34 +99,60 @@ class _DirectoryMainScreenState extends State ), ), ), - 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"), - ], + body: SafeArea( + child: 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(), - ], + // ---------------- TabBarView + Scroll / Landscape Support ---------------- + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + if (isLandscape) { + return SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ), + ), + ); + } + + // Portrait + return TabBarView( + controller: _tabController, + children: [ + DirectoryView(), + NotesView(), + ], + ); + }, + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index afa41bb..8e334ca 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -49,39 +49,72 @@ class _AdvancePaymentScreenState extends State @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(), - ], + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return 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, + + // ---------------- PORTRAIT (UNCHANGED) ---------------- + child: !isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + color: const Color(0xFFF5F5F5), + child: Column( + children: [ + _buildSearchBar(), + _buildEmployeeDropdown(context), + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ) + + // ---------------- LANDSCAPE (FIXED) ---------------- + : SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Container( + width: double.infinity, + color: const Color(0xFFF5F5F5), + + // ❗ Removed IntrinsicHeight + // ❗ Removed ConstrainedBox + // Dropdown can now open freely + + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSearchBar(), + _buildEmployeeDropdown( + context), // now overlay works + _buildTopBalance(), + _buildPaymentList(), + ], + ), + ), + ), ), - ), - ), + ); + }, ), ), ); diff --git a/lib/view/finance/finance_screen.dart b/lib/view/finance/finance_screen.dart index 32cba67..b7142f0 100644 --- a/lib/view/finance/finance_screen.dart +++ b/lib/view/finance/finance_screen.dart @@ -113,171 +113,219 @@ class _FinanceScreenState extends State ), ), ), - body: FadeTransition( - opacity: _fadeAnimation, - child: Obx(() { - if (menuController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } + body: SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; - 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), - ), - ); - } + return Obx(() { + if (menuController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - // Filter allowed Finance menus dynamically - final financeMenuIds = [ - MenuItems.expenseReimbursement, - MenuItems.paymentRequests, - MenuItems.advancePaymentStatements, - ]; + 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 financeMenus = menuController.menuItems - .where((m) => financeMenuIds.contains(m.id) && m.available) - .toList(); + // Filter allowed Finance menus dynamically + final financeMenuIds = [ + MenuItems.expenseReimbursement, + MenuItems.paymentRequests, + MenuItems.advancePaymentStatements, + ]; - if (financeMenus.isEmpty) { - return const Center( - child: Text( - "You don’t have access to the Finance section.", - style: TextStyle(color: Colors.grey), - ), - ); - } + final financeMenus = menuController.menuItems + .where((m) => financeMenuIds.contains(m.id) && m.available) + .toList(); - 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(), - ], - ), - ); - }), + if (financeMenus.isEmpty) { + return const Center( + child: Text( + "You don’t have access to the Finance section.", + style: TextStyle(color: Colors.grey), + ), + ); + } + + // ---------------------- PORTRAIT MODE ---------------------- + if (!isLandscape) { + 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(), + ], + ), + ); + } + + // ---------------------- LANDSCAPE MODE ---------------------- + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildFinanceModulesCompact(financeMenus), + MySpacing.height(24), + + // Wider charts behave better side-by-side or full width + SizedBox( + width: constraints.maxWidth, + child: ExpenseByStatusWidget( + controller: dashboardController), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: ExpenseTypeReportChart(), + ), + MySpacing.height(24), + + SizedBox( + width: constraints.maxWidth, + child: MonthlyExpenseDashboardChart(), + ), + ], + ), + ); + }); + }, + ), + ), ), ); } // --- Finance Modules (Compact Dashboard-style) --- -Widget _buildFinanceModulesCompact(List financeMenus) { - // Map menu IDs to icon + color - final Map 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 financeMenus) { + // Map menu IDs to icon + color + final Map 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; diff --git a/lib/view/finance/payment_request_screen.dart b/lib/view/finance/payment_request_screen.dart index e259b97..8fb2a0c 100644 --- a/lib/view/finance/payment_request_screen.dart +++ b/lib/view/finance/payment_request_screen.dart @@ -99,41 +99,76 @@ class _PaymentRequestMainScreenState extends State 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), - ], - ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final bool isLandscape = + constraints.maxWidth > constraints.maxHeight; + + return Column( + children: [ + // ---------------- TabBar ---------------- + 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"), + ], ), - ], - ), - ), - ), - ], + ), + + // ---------------- Content Area ---------------- + Expanded( + child: Container( + color: Colors.grey[100], + child: isLandscape + ? SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: SizedBox( + height: constraints.maxHeight * 1.3, + child: Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList( + isHistory: false), + _buildPaymentRequestList( + isHistory: true), + ], + ), + ), + ], + ), + ), + ) + : Column( + children: [ + _buildSearchBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildPaymentRequestList(isHistory: false), + _buildPaymentRequestList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), ), floatingActionButton: Obx(() { if (permissionController.permissions.isEmpty) { From 380b6d3552a91797d4d0cfc35ba21c70805fa560 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 16:25:25 +0530 Subject: [PATCH 21/37] Done all screen landscape responsive for mobile and tablet --- .../directory_filter_bottom_sheet.dart | 173 ++++++----- .../assign_employee_bottom_sheet.dart | 229 +++++++------- lib/view/layouts/user_profile_right_bar.dart | 186 ++++-------- .../service_project_screen.dart | 282 ++++++++++-------- 4 files changed, 435 insertions(+), 435 deletions(-) diff --git a/lib/model/directory/directory_filter_bottom_sheet.dart b/lib/model/directory/directory_filter_bottom_sheet.dart index e851591..dabd609 100644 --- a/lib/model/directory/directory_filter_bottom_sheet.dart +++ b/lib/model/directory/directory_filter_bottom_sheet.dart @@ -15,6 +15,7 @@ class DirectoryFilterBottomSheet extends StatefulWidget { class _DirectoryFilterBottomSheetState extends State { final DirectoryController controller = Get.find(); + final _categorySearchQuery = ''.obs; final _bucketSearchQuery = ''.obs; @@ -59,84 +60,100 @@ class _DirectoryFilterBottomSheetState Get.back(); }, onCancel: Get.back, - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() { - final hasSelections = _tempSelectedCategories.isNotEmpty || - _tempSelectedBuckets.isNotEmpty; - if (!hasSelections) return const SizedBox.shrink(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 16, left: 4, right: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final hasSelections = _tempSelectedCategories.isNotEmpty || + _tempSelectedBuckets.isNotEmpty; + + if (!hasSelections) return const SizedBox.shrink(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText("Selected Filters:", fontWeight: 600), + const SizedBox(height: 4), + _buildChips(_tempSelectedCategories, + controller.contactCategories, _toggleCategory), + _buildChips(_tempSelectedBuckets, + controller.contactBuckets, _toggleBucket), + const SizedBox(height: 10), + ], + ); + }), + + // RESET BUTTON + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - MyText("Selected Filters:", fontWeight: 600), - const SizedBox(height: 4), - _buildChips(_tempSelectedCategories, - controller.contactCategories, _toggleCategory), - _buildChips(_tempSelectedBuckets, controller.contactBuckets, - _toggleBucket), - ], - ); - }), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( - onPressed: _resetFilters, - icon: const Icon(Icons.restart_alt, size: 18), - label: MyText("Reset All", color: Colors.red), - style: TextButton.styleFrom( - foregroundColor: Colors.red.shade400, + TextButton.icon( + onPressed: _resetFilters, + icon: const Icon(Icons.restart_alt, size: 18), + label: MyText("Reset All", color: Colors.red), ), - ), - ], - ), - if (controller.contactCategories.isNotEmpty) - Obx(() => _buildExpandableFilterSection( - title: "Categories", - expanded: _categoryExpanded, - searchQuery: _categorySearchQuery, - allItems: controller.contactCategories, - selectedItems: _tempSelectedCategories, - onToggle: _toggleCategory, - )), - if (controller.contactBuckets.isNotEmpty) - Obx(() => _buildExpandableFilterSection( - title: "Buckets", - expanded: _bucketExpanded, - searchQuery: _bucketSearchQuery, - allItems: controller.contactBuckets, - selectedItems: _tempSelectedBuckets, - onToggle: _toggleBucket, - )), - ], - ), - ), + ], + ), + + // CATEGORIES + if (controller.contactCategories.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Categories", + expanded: _categoryExpanded, + searchQuery: _categorySearchQuery, + allItems: controller.contactCategories, + selectedItems: _tempSelectedCategories, + onToggle: _toggleCategory, + )), + + // BUCKETS + if (controller.contactBuckets.isNotEmpty) + Obx(() => _buildExpandableFilterSection( + title: "Buckets", + expanded: _bucketExpanded, + searchQuery: _bucketSearchQuery, + allItems: controller.contactBuckets, + selectedItems: _tempSelectedBuckets, + onToggle: _toggleBucket, + )), + + const SizedBox(height: 20), + ], + ), + ); + }, ), ); } + // ------------------------------ + // CHIP UI FOR SELECTED FILTERS + // ------------------------------ Widget _buildChips(RxList selectedIds, List allItems, Function(String) onRemoved) { final idToName = {for (var item in allItems) item.id: item.name}; + return Wrap( spacing: 4, runSpacing: 4, - children: selectedIds - .map((id) => Chip( - label: MyText(idToName[id] ?? "", color: Colors.black87), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () => onRemoved(id), - backgroundColor: Colors.blue.shade50, - )) - .toList(), + children: selectedIds.map((id) { + return Chip( + label: MyText(idToName[id] ?? "", color: Colors.black87), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => onRemoved(id), + backgroundColor: Colors.blue.shade50, + ); + }).toList(), ); } + // ------------------------------ + // EXPANDABLE FILTER UI + // ------------------------------ Widget _buildExpandableFilterSection({ required String title, required RxBool expanded, @@ -146,7 +163,7 @@ class _DirectoryFilterBottomSheetState required Function(String) onToggle, }) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 8), child: Column( children: [ GestureDetector( @@ -159,28 +176,27 @@ class _DirectoryFilterBottomSheetState : Icons.keyboard_arrow_right, size: 20, ), - const SizedBox(width: 4), - MyText( - "$title", - fontWeight: 600, - fontSize: 16, - ), + const SizedBox(width: 6), + MyText(title, fontWeight: 600, fontSize: 16), ], ), ), if (expanded.value) _buildFilterSection( + title: title, searchQuery: searchQuery, allItems: allItems, selectedItems: selectedItems, onToggle: onToggle, - title: title, ), ], ), ); } + // ------------------------------ + // FILTER LIST + SEARCH + // ------------------------------ Widget _buildFilterSection({ required String title, required RxString searchQuery, @@ -189,14 +205,16 @@ class _DirectoryFilterBottomSheetState required Function(String) onToggle, }) { final filteredList = allItems.where((item) { - if (searchQuery.isEmpty) return true; + if (searchQuery.value.isEmpty) return true; return item.name.toLowerCase().contains(searchQuery.value.toLowerCase()); }).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 6), + const SizedBox(height: 8), + + // SEARCH BOX TextField( onChanged: (value) => searchQuery.value = value, style: const TextStyle(fontSize: 13), @@ -215,7 +233,10 @@ class _DirectoryFilterBottomSheetState fillColor: Colors.grey.shade100, ), ), + const SizedBox(height: 8), + + // NO RESULTS if (filteredList.isEmpty) Row( children: [ @@ -227,7 +248,7 @@ class _DirectoryFilterBottomSheetState ) else ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 230), + constraints: const BoxConstraints(maxHeight: 260), child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, @@ -238,7 +259,7 @@ class _DirectoryFilterBottomSheetState return Obx(() { final isSelected = selectedItems.contains(item.id); - return GestureDetector( + return InkWell( onTap: () => onToggle(item.id), child: Container( padding: const EdgeInsets.symmetric( @@ -271,7 +292,7 @@ class _DirectoryFilterBottomSheetState }); }, ), - ) + ), ], ); } diff --git a/lib/view/employees/assign_employee_bottom_sheet.dart b/lib/view/employees/assign_employee_bottom_sheet.dart index f6ae76a..4a3c350 100644 --- a/lib/view/employees/assign_employee_bottom_sheet.dart +++ b/lib/view/employees/assign_employee_bottom_sheet.dart @@ -48,116 +48,133 @@ class _AssignProjectBottomSheetState extends State { onCancel: () => Navigator.pop(context), onSubmit: _handleAssign, submitText: "Assign", - child: Obx(() { - if (assignController.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - final projects = assignController.allProjects; - if (projects.isEmpty) { - return const Center(child: Text('No projects available.')); - } + /// πŸ”₯ MAKE BODY SCROLLABLE (fix for landscape) + child: LayoutBuilder( + builder: (context, constraints) { + return Obx(() { + if (assignController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall( - 'Select the projects to assign this employee.', - color: Colors.grey[600], - ), - MySpacing.height(8), + final projects = assignController.allProjects; + if (projects.isEmpty) { + return const Center(child: Text('No projects available.')); + } - // Select All - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Projects (${projects.length})', - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - TextButton( - onPressed: () { - assignController.toggleSelectAll(); - }, - child: Obx(() { - return Text( - assignController.areAllSelected() - ? 'Deselect All' - : 'Select All', - style: const TextStyle( - color: Colors.blueAccent, - fontWeight: FontWeight.w600, - ), - ); - }), - ), - ], - ), - - // List of Projects - SizedBox( - height: 300, - child: ListView.builder( - controller: _scrollController, - itemCount: projects.length, - itemBuilder: (context, index) { - final GlobalProjectModel project = projects[index]; - return Obx(() { - final bool isSelected = - assignController.isProjectSelected( - project.id.toString(), - ); - return Theme( - data: Theme.of(context).copyWith( - checkboxTheme: CheckboxThemeData( - fillColor: WidgetStateProperty.resolveWith( - (states) => states.contains(WidgetState.selected) - ? Colors.blueAccent - : Colors.white, - ), - side: const BorderSide( - color: Colors.black, - width: 2, - ), - checkColor: - WidgetStateProperty.all(Colors.white), - ), - ), - child: CheckboxListTile( - dense: true, - value: isSelected, - title: Text( - project.name, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - ), - onChanged: (checked) { - assignController.toggleProjectSelection( - project.id.toString(), - checked ?? false, - ); - }, - activeColor: Colors.blueAccent, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), - ), - ); - }); - }, + return ConstrainedBox( + constraints: BoxConstraints( + /// πŸ”₯ Always allow enough height for scroll + maxHeight: constraints.maxHeight, ), - ), - ], - ); - }), + child: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodySmall( + 'Select the projects to assign this employee.', + color: Colors.grey[600], + ), + MySpacing.height(8), + + // Header Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Projects (${projects.length})', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + TextButton( + onPressed: () { + assignController.toggleSelectAll(); + }, + child: Obx(() { + return Text( + assignController.areAllSelected() + ? 'Deselect All' + : 'Select All', + style: const TextStyle( + color: Colors.blueAccent, + fontWeight: FontWeight.w600, + ), + ); + }), + ), + ], + ), + + /// πŸ”₯ List auto grows and scrolls β€” no fixed height + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: projects.length, + itemBuilder: (context, index) { + final GlobalProjectModel project = projects[index]; + return Obx(() { + final bool isSelected = + assignController.isProjectSelected( + project.id.toString(), + ); + return Theme( + data: Theme.of(context).copyWith( + checkboxTheme: CheckboxThemeData( + fillColor: + WidgetStateProperty.resolveWith( + (states) => + states.contains(WidgetState.selected) + ? Colors.blueAccent + : Colors.white, + ), + side: const BorderSide( + color: Colors.black, + width: 2, + ), + checkColor: + WidgetStateProperty.all(Colors.white), + ), + ), + child: CheckboxListTile( + dense: true, + value: isSelected, + title: Text( + project.name, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + onChanged: (checked) { + assignController.toggleProjectSelection( + project.id.toString(), + checked ?? false, + ); + }, + activeColor: Colors.blueAccent, + controlAffinity: + ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + ); + }); + }, + ), + ], + ), + ), + ); + }); + }, + ), ); }, ); diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index ddca327..17ef20d 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -52,6 +52,7 @@ class _UserProfileBarState extends State @override Widget build(BuildContext context) { final bool isCondensed = widget.isCondensed; + return Padding( padding: const EdgeInsets.only(left: 14), child: ClipRRect( @@ -88,41 +89,52 @@ class _UserProfileBarState extends State bottom: true, child: Stack( children: [ + // ======================= MAIN PROFILE SIDEBAR ======================= Offstage( offstage: _isThemeEditorVisible, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _isLoading - ? const _LoadingSection() - : _userProfileSection(isCondensed), - if (!_isLoading && !isCondensed) _switchTenantRow(), - MySpacing.height(12), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.7, - color: Colors.grey.withOpacity(0.25), - ), - MySpacing.height(12), - _supportAndSettingsMenu(isCondensed), - MySpacing.height(12), - - // Subtle version text for expanded mode - if (!isCondensed && _appVersion.isNotEmpty) - _versionText(), - - const Spacer(), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.35, - color: Colors.grey.withOpacity(0.18), - ), - _logoutButton(isCondensed), - ], + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _isLoading + ? const _LoadingSection() + : _userProfileSection(isCondensed), + if (!_isLoading && !isCondensed) + _switchTenantRow(), + MySpacing.height(12), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.7, + color: Colors.grey.withOpacity(0.25), + ), + MySpacing.height(12), + _supportAndSettingsMenu(isCondensed), + const Spacer(), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.35, + color: Colors.grey.withOpacity(0.18), + ), + _logoutButton(isCondensed), + ], + ), + ), + ), + ); + }, ), ), + + // ======================= THEME EDITOR VIEW ======================= Offstage( offstage: !_isThemeEditorVisible, child: ThemeEditorWidget( @@ -131,9 +143,6 @@ class _UserProfileBarState extends State }, ), ), - - // Floating badge for condensed mode - if (isCondensed && _appVersion.isNotEmpty) _versionBadge(), ], ), ), @@ -143,96 +152,7 @@ class _UserProfileBarState extends State ); } - // =================== Version Widgets =================== - - Widget _versionText() { - return Padding( - padding: const EdgeInsets.only(top: 4, bottom: 12), - child: Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), - decoration: BoxDecoration( - color: Colors.grey.shade100.withOpacity(0.85), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.grey.shade200, - width: 0.7, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.12), - blurRadius: 3, - offset: const Offset(0, 1), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.info_outline, size: 14, color: Colors.grey[700]), - const SizedBox(width: 4), - Text( - 'Version: $_appVersion', - style: TextStyle( - fontSize: 12, - color: Colors.grey[800], - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget _versionBadge() { - return Positioned( - bottom: 10, - right: 14, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - decoration: BoxDecoration( - color: Colors.grey.shade100.withOpacity(0.85), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.grey.shade300, - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.17), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - _appVersion, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.black87, - letterSpacing: 0.4, - ), - ), - ), - ), - ), - ); - } - - // =================== Existing methods =================== + // ==================== EXISTING CODE (UNCHANGED) ===================== Widget _switchTenantRow() { final TenantSwitchController tenantSwitchController = @@ -337,17 +257,25 @@ class _UserProfileBarState extends State child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ); + // ⭐ FIXED β€” YOUR ORIGINAL INTENT, COMPLETED PROPERLY Widget _noTenantContainer() => Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.blue.shade200, width: 1), + border: Border.all( + color: Colors.blue.shade200, + width: 1, + ), ), - child: MyText.bodyMedium( - "No tenants available", - color: Colors.blueAccent, - fontWeight: 600, + child: const Center( + child: Text( + "No organizations available", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), ), ); diff --git a/lib/view/service_project/service_project_screen.dart b/lib/view/service_project/service_project_screen.dart index bf07d58..77f1545 100644 --- a/lib/view/service_project/service_project_screen.dart +++ b/lib/view/service_project/service_project_screen.dart @@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State 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(); }); @@ -42,38 +42,29 @@ class _ServiceProjectScreenState extends State Widget _buildProjectCard(ProjectItem project) { return Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shadowColor: Colors.indigo.withOpacity(0.10), color: Colors.white, child: InkWell( - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(5), onTap: () { - // Navigate to ServiceProjectDetailsScreen - Get.to(() => ServiceProjectDetailsScreen( - projectId: project.id, - projectName: project.name, - )); + Get.to(() => ServiceProjectDetailsScreen(projectId: project.id)); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - /// Project Header Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleMedium( - project.name, - fontWeight: 700, - ), - MySpacing.height(4), - ], + child: MyText.titleMedium( + project.name, + fontWeight: 700, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), if (project.status?.status.isNotEmpty ?? false) @@ -92,47 +83,32 @@ class _ServiceProjectScreenState extends State ), ], ), - - MySpacing.height(10), - - /// Assigned Date + MySpacing.height(8), _buildDetailRow( Icons.date_range_outlined, Colors.teal, "Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}", - fontSize: 13, ), - - MySpacing.height(8), - - /// Client Info + MySpacing.height(6), if (project.client != null) _buildDetailRow( Icons.account_circle_outlined, Colors.indigo, "Client: ${project.client!.name} (${project.client!.contactPerson})", - fontSize: 13, ), - - MySpacing.height(8), - - /// Contact Info + MySpacing.height(6), _buildDetailRow( Icons.phone, Colors.green, "Contact: ${project.contactName} (${project.contactPhone})", - fontSize: 13, ), - - MySpacing.height(12), - - /// Services List + MySpacing.height(10), if (project.services.isNotEmpty) Wrap( spacing: 6, runSpacing: 4, children: project.services - .map((service) => _buildServiceChip(service.name)) + .map((e) => _buildServiceChip(e.name)) .toList(), ), ], @@ -148,7 +124,7 @@ class _ServiceProjectScreenState extends State color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(6), ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: MyText.labelSmall( name, color: Colors.orange[800], @@ -157,19 +133,18 @@ class _ServiceProjectScreenState extends State ); } - Widget _buildDetailRow(IconData icon, Color iconColor, String value, - {double fontSize = 12}) { + Widget _buildDetailRow(IconData icon, Color color, String value) { return Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(icon, size: 18, color: iconColor), + Icon(icon, size: 18, color: color), MySpacing.width(8), - Flexible( + Expanded( child: MyText.bodySmall( value, - color: Colors.grey[900], + maxLines: 2, + overflow: TextOverflow.ellipsis, fontWeight: 500, - fontSize: fontSize, + color: Colors.grey[900], ), ), ], @@ -184,7 +159,7 @@ class _ServiceProjectScreenState extends State const Icon(Icons.work_outline, size: 60, color: Colors.grey), MySpacing.height(18), MyText.titleMedium('No matching projects found.', - fontWeight: 600, color: Colors.grey), + color: Colors.grey, fontWeight: 600), MySpacing.height(10), MyText.bodySmall('Try adjusting your filters or refresh.', color: Colors.grey), @@ -195,91 +170,150 @@ class _ServiceProjectScreenState extends State @override 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( + return SafeArea( + child: Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: CustomAppBar( + title: "Service Projects", + onBackPressed: () => Get.toNamed('/dashboard'), + ), + body: LayoutBuilder( + builder: (context, constraints) { + return Column( 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( - valueListenable: searchController, - builder: (context, value, _) { - if (value.text.isEmpty) { - return const SizedBox.shrink(); - } - return IconButton( - icon: const Icon(Icons.clear, + /// SEARCH BAR AREA + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: SizedBox( + height: 38, + child: TextField( + controller: searchController, + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: const Icon(Icons.search, 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), + suffixIcon: + ValueListenableBuilder( + valueListenable: searchController, + builder: (context, value, _) { + return value.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear, + size: 20, color: Colors.grey), + onPressed: () { + searchController.clear(); + controller.updateSearch(''); + }, + ) + : const SizedBox.shrink(); + }, + ), + hintText: 'Search projects...', + fillColor: Colors.white, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: + BorderSide(color: Colors.grey.shade300), + ), + ), + ), ), ), - ), + MySpacing.width(8), + + /// FILTER BUTTON + _roundIconButton(Icons.tune), + + MySpacing.width(8), + + /// ACTION MENU + _roundMenuButton(), + ], ), ), + + /// LIST AREA + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + final projects = controller.filteredProjects; + + return MyRefreshIndicator( + onRefresh: _refreshProjects, + color: Colors.white, + backgroundColor: Colors.indigo, + child: projects.isEmpty + ? _buildEmptyState() + : ListView.separated( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 4, bottom: 20), + itemCount: projects.length, + separatorBuilder: (_, __) => MySpacing.height(12), + itemBuilder: (_, index) => + _buildProjectCard(projects[index]), + ), + ); + }), + ), + ], + ); + }, + ), + ), + ); + } + + Widget _roundIconButton(IconData icon) { + return Container( + height: 38, + width: 38, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.grey.shade300), + ), + child: Icon(icon, size: 20, color: Colors.black87), + ); + } + + Widget _roundMenuButton() { + return Container( + height: 38, + width: 38, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + itemBuilder: (context) => [ + const PopupMenuItem( + enabled: false, + height: 30, + child: Text("Actions", + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)), + ), + const PopupMenuItem( + value: 1, + child: Row( + children: [ + Expanded(child: Text("Manage Projects")), + Icon(Icons.chevron_right, size: 20, color: Colors.indigo), ], ), ), - - /// Project List - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - - 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]), - ), - ); - }), - ), ], ), ); From b9847e03b11754291bc96ef681bf444c0e015e3e Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:13:44 +0530 Subject: [PATCH 22/37] added multiselect bottom sheet in assign task bottom sheet form --- .../assign_task_bottom_sheet .dart | 197 +++++++++++------- lib/model/employees/employee_model.dart | 2 +- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 99dae49..763f88e 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -13,6 +13,10 @@ import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart'; import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; import 'package:on_field_work/model/tenant/tenant_services_model.dart'; +// Added imports for employee selection +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; final String activityName; @@ -43,7 +47,8 @@ class _AssignTaskBottomSheetState extends State { final DailyTaskPlanningController controller = Get.find(); final ProjectController projectController = Get.find(); - final OrganizationController orgController = Get.put(OrganizationController()); + final OrganizationController orgController = + Get.put(OrganizationController()); final ServiceController serviceController = Get.put(ServiceController()); final TextEditingController targetController = TextEditingController(); @@ -79,7 +84,8 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, serviceId: selectedService?.id); + await controller.fetchTaskData(selectedProjectId, + serviceId: selectedService?.id); } @override @@ -142,10 +148,11 @@ class _AssignTaskBottomSheetState extends State { const Divider(), // Pending Task Info - _infoRow(Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), + _infoRow( + Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector + // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, child: Row( @@ -158,18 +165,52 @@ class _AssignTaskBottomSheetState extends State { ), MySpacing.height(8), - // Employee List - Container( - constraints: const BoxConstraints(maxHeight: 180), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), + // ------------------------------- + // Employee selector (REPLACED) + // ------------------------------- + // We show a button-like container (with border) that opens the reusable + // EmployeeSelectionBottomSheet. Selected employees are reflected using + // existing controller.uploadingStates & controller.selectedEmployees. + GestureDetector( + onTap: _openEmployeeSelectionSheet, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText("Select team members", + color: Colors.grey.shade700); + } + // show summary text when there are selected employees + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), ), - child: _buildEmployeeList(), ), MySpacing.height(8), - // Selected Employees Chips + // Selected Employees Chips (keeps existing behavior) _buildSelectedEmployees(), MySpacing.height(8), @@ -198,7 +239,8 @@ class _AssignTaskBottomSheetState extends State { } void _onRoleMenuPressed() { - final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; final Size screenSize = overlay.size; showMenu( @@ -219,63 +261,12 @@ class _AssignTaskBottomSheetState extends State { }), ], ).then((value) { - if (value != null) controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) + controller.onRoleSelected(value == 'all' ? null : value); }); } - Widget _buildEmployeeList() { - return Obx(() { - if (controller.isFetchingEmployees.value) { - return Center(child: CircularProgressIndicator()); - } - - final filteredEmployees = controller.selectedRoleId.value == null - ? controller.employees - : controller.employees - .where((e) => e.jobRoleID.toString() == controller.selectedRoleId.value) - .toList(); - - if (filteredEmployees.isEmpty) { - return Center(child: Text("No employees available for selected role.")); - } - - return Scrollbar( - controller: _employeeListScrollController, - thumbVisibility: true, - child: ListView.builder( - controller: _employeeListScrollController, - itemCount: filteredEmployees.length, - itemBuilder: (context, index) { - final employee = filteredEmployees[index]; - final rxBool = controller.uploadingStates[employee.id]; - - return Obx(() => ListTile( - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - leading: Checkbox( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), - value: rxBool?.value ?? false, - onChanged: (selected) { - if (rxBool != null) { - rxBool.value = selected ?? false; - controller.updateSelectedEmployees(); - } - }, - fillColor: MaterialStateProperty.resolveWith((states) => - states.contains(MaterialState.selected) - ? const Color.fromARGB(255, 95, 132, 255) - : Colors.transparent), - checkColor: Colors.white, - side: const BorderSide(color: Colors.black), - ), - title: Text(employee.name, style: const TextStyle(fontSize: 14)), - visualDensity: VisualDensity.compact, - )); - }, - ), - ); - }); - } + // Removed old inline employee list; selection handled by bottom sheet. Widget _buildSelectedEmployees() { return Obx(() { @@ -329,9 +320,12 @@ class _AssignTaskBottomSheetState extends State { decoration: InputDecoration( hintText: hintText, border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), - validator: (value) => this.controller.formFieldValidator(value, fieldType: validatorType), + validator: (value) => this + .controller + .formFieldValidator(value, fieldType: validatorType), ), ], ); @@ -350,9 +344,11 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", fontWeight: 600, color: Colors.black), + child: MyText.titleMedium("$title: ", + fontWeight: 600, color: Colors.black), ), - TextSpan(text: value, style: const TextStyle(color: Colors.black)), + TextSpan( + text: value, style: const TextStyle(color: Colors.black)), ], ), ), @@ -362,6 +358,50 @@ class _AssignTaskBottomSheetState extends State { ); } + Future _openEmployeeSelectionSheet() async { + // Open the existing EmployeeSelectionBottomSheet + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedEmployees.toList(), + multipleSelection: true, + title: 'Select Team Members', + ), + ); + + if (result == null) return; + + // Merge returned employees into controller.uploadingStates & controller.selectedEmployees + // 1) Reset all uploadingStates to false, then set true for selected + controller.uploadingStates.forEach((key, rx) { + rx.value = false; + }); + + for (final emp in result) { + final idStr = emp.id.toString(); + if (controller.uploadingStates.containsKey(idStr)) { + controller.uploadingStates[idStr]?.value = true; + } else { + // if uploadingStates doesn't have the id yet, add it (safe fallback) + controller.uploadingStates[idStr] = RxBool(true); + } + } + + // 2) Update selectedEmployees list in controller + controller.selectedEmployees.assignAll(result); + + // 3) Call controller helper (keeps existing behavior) + try { + controller.updateSelectedEmployees(); + } catch (_) { + // If controller does not implement updateSelectedEmployees, ignore. + } + } + void _onAssignTaskPressed() { final selectedTeam = controller.uploadingStates.entries .where((e) => e.value.value) @@ -369,13 +409,19 @@ class _AssignTaskBottomSheetState extends State { .toList(); if (selectedTeam.isEmpty) { - showAppSnackbar(title: "Team Required", message: "Please select at least one team member", type: SnackbarType.error); + showAppSnackbar( + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { - showAppSnackbar(title: "Invalid Input", message: "Please enter a valid target number", type: SnackbarType.error); + showAppSnackbar( + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error); return; } @@ -390,7 +436,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { - showAppSnackbar(title: "Description Required", message: "Please enter a description", type: SnackbarType.error); + showAppSnackbar( + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error); return; } diff --git a/lib/model/employees/employee_model.dart b/lib/model/employees/employee_model.dart index d774f23..0abc764 100644 --- a/lib/model/employees/employee_model.dart +++ b/lib/model/employees/employee_model.dart @@ -72,7 +72,7 @@ class EmployeeModel { }; } - /// βœ… Add equality based on unique `id` + ///Equality based on unique `id` β€” required for multi-selection to work @override bool operator ==(Object other) { if (identical(this, other)) return true; From a9fa0448e329377e5cc241955eead6ea45dbcd0e Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:49:26 +0530 Subject: [PATCH 23/37] done with employee list dropdown updation --- .../multiple_select_bottomsheet.dart | 139 +++++++++++------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 829dc4f..5ad9375 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; @@ -24,33 +25,67 @@ class EmployeeSelectionBottomSheet extends StatefulWidget { class _EmployeeSelectionBottomSheetState extends State { final TextEditingController _searchController = TextEditingController(); + final RxBool _isSearching = false.obs; - final RxList _searchResults = [].obs; + final RxList _allResults = [].obs; + late RxList _selectedEmployees; + Timer? _debounce; + @override void initState() { super.initState(); _selectedEmployees = RxList.from(widget.initiallySelected); - _searchEmployees(''); + + _performSearch(''); } @override void dispose() { + _debounce?.cancel(); _searchController.dispose(); super.dispose(); } - Future _searchEmployees(String query) async { + // ------------------------------------------------------ + // πŸ”₯ Optimized debounce-based search + // ------------------------------------------------------ + void _onSearchChanged(String query) { + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 300), () { + _performSearch(query.trim()); + }); + } + + Future _performSearch(String query) async { _isSearching.value = true; + final data = await ApiService.searchEmployeesBasic(searchString: query); + final results = (data as List) .map((e) => EmployeeModel.fromJson(e as Map)) .toList(); - _searchResults.assignAll(results); + + // ------------------------------------------------------ + // πŸ”₯ Auto-move selected employees to top + // ------------------------------------------------------ + results.sort((a, b) { + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; + + if (aSel != bSel) return aSel.compareTo(bSel); + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _allResults.assignAll(results); + _isSearching.value = false; } + // ------------------------------------------------------ + // Handle tap & checkbox + // ------------------------------------------------------ void _toggleEmployee(EmployeeModel emp) { if (widget.multipleSelection) { if (_selectedEmployees.contains(emp)) { @@ -61,9 +96,14 @@ class _EmployeeSelectionBottomSheetState } else { _selectedEmployees.assignAll([emp]); } - _selectedEmployees.refresh(); // important for Obx rebuild + + // Re-sort list after each toggle + _performSearch(_searchController.text.trim()); } + // ------------------------------------------------------ + // Submit selection + // ------------------------------------------------------ void _handleSubmit() { if (widget.multipleSelection) { Navigator.of(context).pop(_selectedEmployees.toList()); @@ -73,11 +113,14 @@ class _EmployeeSelectionBottomSheetState } } + // ------------------------------------------------------ + // Search bar widget + // ------------------------------------------------------ Widget _searchBar() => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: TextField( controller: _searchController, - onChanged: _searchEmployees, + onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Search employees...', filled: true, @@ -88,7 +131,7 @@ class _EmployeeSelectionBottomSheetState icon: const Icon(Icons.close, color: Colors.grey), onPressed: () { _searchController.clear(); - _searchEmployees(''); + _performSearch(''); }, ) : null, @@ -102,60 +145,52 @@ class _EmployeeSelectionBottomSheetState ), ); + // ------------------------------------------------------ + // Employee list (optimized) + // ------------------------------------------------------ Widget _employeeList() => Expanded( child: Obx(() { - if (_isSearching.value) { - return const Center(child: CircularProgressIndicator()); - } - - if (_searchResults.isEmpty) { - return const Center(child: Text("No employees found")); - } + final results = _allResults; return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: _searchResults.length, + itemCount: results.length, itemBuilder: (context, index) { - final emp = _searchResults[index]; + final emp = results[index]; + final isSelected = _selectedEmployees.contains(emp); - return Obx(() { - final isSelected = _selectedEmployees.contains(emp); - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blueAccent, - child: Text( - (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') - .toUpperCase(), - style: const TextStyle(color: Colors.white), - ), + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') + .toUpperCase(), + style: const TextStyle(color: Colors.white), ), - title: Text('${emp.firstName} ${emp.lastName}'), - subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) { - FocusScope.of(context).unfocus(); // hide keyboard - _toggleEmployee(emp); - }, - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), + ), + title: Text('${emp.firstName} ${emp.lastName}'), + subtitle: Text(emp.email), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, ), - onTap: () { - FocusScope.of(context).unfocus(); - _toggleEmployee(emp); - }, - contentPadding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - ); - }); + ), + onTap: () => _toggleEmployee(emp), + contentPadding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); }, ); }), ); + // ------------------------------------------------------ + // Build bottom sheet + // ------------------------------------------------------ @override Widget build(BuildContext context) { return BaseBottomSheet( @@ -164,10 +199,12 @@ class _EmployeeSelectionBottomSheetState onSubmit: _handleSubmit, child: SizedBox( height: MediaQuery.of(context).size.height * 0.7, - child: Column(children: [ - _searchBar(), - _employeeList(), - ]), + child: Column( + children: [ + _searchBar(), + _employeeList(), + ], + ), ), ); } From d3e3061d31599dcce272bb2c9702680005a2ad7b Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 11:57:13 +0530 Subject: [PATCH 24/37] advance paymen --- lib/view/finance/advance_payment_screen.dart | 72 ++++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 8e334ca..abff416 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -35,8 +35,8 @@ class _AdvancePaymentScreenState extends State } }); - controller.searchQuery.listen((q) { - if (_searchCtrl.text != q) _searchCtrl.text = q; + _searchCtrl.addListener(() { + controller.searchQuery.value = _searchCtrl.text.trim(); }); } @@ -58,19 +58,25 @@ class _AdvancePaymentScreenState extends State final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: RefreshIndicator( - onRefresh: () async { - final emp = controller.selectedEmployee.value; - if (emp != null) { - await controller.fetchAdvancePayments(emp.id.toString()); + return 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: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); } }, - color: Colors.white, - backgroundColor: contentTheme.primary, - strokeWidth: 2.5, - displacement: 60, // ---------------- PORTRAIT (UNCHANGED) ---------------- child: !isLandscape @@ -195,7 +201,13 @@ class _AdvancePaymentScreenState extends State child: TextField( controller: _searchCtrl, focusNode: _searchFocus, - onChanged: (v) => controller.searchQuery.value = v.trim(), + onTap: () { + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted) { + FocusScope.of(context).requestFocus(_searchFocus); + } + }); + }, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), @@ -269,8 +281,12 @@ class _AdvancePaymentScreenState extends State return InkWell( onTap: () { controller.selectEmployee(e); - _searchCtrl.text = e.name; - controller.searchQuery.value = e.name; + _searchCtrl + ..text = e.name + ..selection = TextSelection.fromPosition( + TextPosition(offset: e.name.length), + ); + FocusScope.of(context).unfocus(); SystemChannels.textInput.invokeMethod('TextInput.hide'); controller.employees.clear(); @@ -397,8 +413,8 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - final formattedTime = - parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; + // final formattedTime = + // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; final project = item.name ?? ''; final desc = item.title ?? ''; @@ -429,16 +445,16 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - if (formattedTime.isNotEmpty) ...[ - const SizedBox(width: 6), - Text( - formattedTime, - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade500, - fontStyle: FontStyle.italic), - ), - ] + // if (formattedTime.isNotEmpty) ...[ + // const SizedBox(width: 6), + // Text( + // formattedTime, + // style: TextStyle( + // fontSize: 12, + // color: Colors.grey.shade500, + // fontStyle: FontStyle.italic), + // ), + // ] ], ), const SizedBox(height: 4), From 40e8a0d9116763b2ce31b9686dc10168ca5beb1b Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 12:00:28 +0530 Subject: [PATCH 25/37] unwanted code removed --- lib/view/finance/advance_payment_screen.dart | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index abff416..4a59de8 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -413,9 +413,7 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - // final formattedTime = - // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; - + final project = item.name ?? ''; final desc = item.title ?? ''; final amount = (item.amount ?? 0).toDouble(); @@ -445,16 +443,7 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - // if (formattedTime.isNotEmpty) ...[ - // const SizedBox(width: 6), - // Text( - // formattedTime, - // style: TextStyle( - // fontSize: 12, - // color: Colors.grey.shade500, - // fontStyle: FontStyle.italic), - // ), - // ] + ], ), const SizedBox(height: 4), From 2e45d0de81095404024e420e39a6fa121746884b Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:04:40 +0530 Subject: [PATCH 26/37] upadted with select enployee sheet --- .../expense/add_expense_controller.dart | 15 +- .../add_payment_request_controller.dart | 27 +- .../assign_task_bottom_sheet .dart | 270 +++++++++--------- .../directory/edit_bucket_bottom_sheet.dart | 197 ++++++------- .../multiple_select_bottomsheet.dart | 30 +- .../multiple_select_role_bottomsheet.dart | 244 ++++++++++++++++ .../expense/add_expense_bottom_sheet.dart | 81 +++--- .../add_payment_request_bottom_sheet.dart | 124 ++++---- .../payment_request_filter_bottom_sheet.dart | 24 +- .../expense/expense_filter_bottom_sheet.dart | 26 +- 10 files changed, 637 insertions(+), 401 deletions(-) create mode 100644 lib/model/employees/multiple_select_role_bottomsheet.dart diff --git a/lib/controller/expense/add_expense_controller.dart b/lib/controller/expense/add_expense_controller.dart index 32e53fd..efdc423 100644 --- a/lib/controller/expense/add_expense_controller.dart +++ b/lib/controller/expense/add_expense_controller.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:async'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -50,10 +51,22 @@ class AddExpenseController extends GetxController { final isEditMode = false.obs; final isSearchingEmployees = false.obs; +// --- Paid By (Single + Multi Selection Support) --- + +// single selection + final selectedPaidBy = Rxn(); + + + +// helper setters + void setSelectedPaidBy(EmployeeModel? emp) { + selectedPaidBy.value = emp; + } + // --- Dropdown Selections & Data --- final selectedPaymentMode = Rxn(); final selectedExpenseType = Rxn(); - final selectedPaidBy = Rxn(); + // final selectedPaidBy = Rxn(); final selectedProject = ''.obs; final selectedTransactionDate = Rxn(); diff --git a/lib/controller/finance/add_payment_request_controller.dart b/lib/controller/finance/add_payment_request_controller.dart index e7688b9..f05ff45 100644 --- a/lib/controller/finance/add_payment_request_controller.dart +++ b/lib/controller/finance/add_payment_request_controller.dart @@ -8,12 +8,13 @@ import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:intl/intl.dart'; -import 'package:on_field_work/helpers/services/api_service.dart'; -import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; -import 'package:on_field_work/helpers/widgets/time_stamp_image_helper.dart'; -import 'package:on_field_work/model/finance/expense_category_model.dart'; -import 'package:on_field_work/model/finance/currency_list_model.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/services/app_logger.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/helpers/widgets/time_stamp_image_helper.dart'; +import 'package:marco/model/finance/expense_category_model.dart'; +import 'package:marco/model/finance/currency_list_model.dart'; +import 'package:marco/model/employees/employee_model.dart'; class AddPaymentRequestController extends GetxController { // Loading States @@ -32,7 +33,7 @@ class AddPaymentRequestController extends GetxController { // Selected Values final selectedProject = Rx?>(null); final selectedCategory = Rx(null); - final selectedPayee = ''.obs; + final selectedPayee = Rx(null); final selectedCurrency = Rx(null); final isAdvancePayment = false.obs; final selectedDueDate = Rx(null); @@ -161,7 +162,7 @@ class AddPaymentRequestController extends GetxController { try { final pickedFile = await _picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { - isProcessingAttachment.value = true; + isProcessingAttachment.value = true; File imageFile = File(pickedFile.path); // Add timestamp to the captured image @@ -184,7 +185,7 @@ class AddPaymentRequestController extends GetxController { selectedProject.value = project; void selectCategory(ExpenseCategory category) => selectedCategory.value = category; - void selectPayee(String payee) => selectedPayee.value = payee; + void selectPayee(EmployeeModel payee) => selectedPayee.value = payee; void selectCurrency(Currency currency) => selectedCurrency.value = currency; void addAttachment(File file) => attachments.add(file); @@ -268,7 +269,7 @@ class AddPaymentRequestController extends GetxController { "amount": double.tryParse(amountController.text.trim()) ?? 0, "currencyId": selectedCurrency.value?.id ?? '', "description": descriptionController.text.trim(), - "payee": selectedPayee.value, + "payee": selectedPayee.value?.id ?? "", "dueDate": selectedDueDate.value?.toIso8601String(), "isAdvancePayment": isAdvancePayment.value, "billAttachments": billAttachments.map((a) { @@ -337,7 +338,7 @@ class AddPaymentRequestController extends GetxController { "amount": double.tryParse(amountController.text.trim()) ?? 0, "currencyId": selectedCurrency.value?.id ?? '', "description": descriptionController.text.trim(), - "payee": selectedPayee.value, + "payee": selectedPayee.value?.id ?? "", "dueDate": selectedDueDate.value?.toIso8601String(), "isAdvancePayment": isAdvancePayment.value, "billAttachments": billAttachments.map((a) { @@ -388,7 +389,7 @@ class AddPaymentRequestController extends GetxController { return _errorSnackbar("Please select a project"); if (selectedCategory.value == null) return _errorSnackbar("Please select a category"); - if (selectedPayee.value.isEmpty) + if (selectedPayee.value == null) return _errorSnackbar("Please select a payee"); if (selectedCurrency.value == null) return _errorSnackbar("Please select currency"); @@ -408,7 +409,7 @@ class AddPaymentRequestController extends GetxController { descriptionController.clear(); selectedProject.value = null; selectedCategory.value = null; - selectedPayee.value = ''; + selectedPayee.value = null; selectedCurrency.value = null; isAdvancePayment.value = false; attachments.clear(); diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 763f88e..5191079 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -1,21 +1,22 @@ +// Updated AssignTaskBottomSheet with bottom sheet height fix +// Only modified layout for employee selection area to prevent overflow. + import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:on_field_work/controller/task_Planning/daily_task_Planning_controller.dart'; -import 'package:on_field_work/controller/project_controller.dart'; -import 'package:on_field_work/controller/tenant/organization_selection_controller.dart'; -import 'package:on_field_work/controller/tenant/service_controller.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; -import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; -import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; -import 'package:on_field_work/helpers/widgets/tenant/organization_selector.dart'; -import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart'; -import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart'; -import 'package:on_field_work/model/tenant/tenant_services_model.dart'; - -// Added imports for employee selection +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; +import 'package:marco/controller/project_controller.dart'; +import 'package:marco/controller/tenant/organization_selection_controller.dart'; +import 'package:marco/controller/tenant/service_controller.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; +import 'package:marco/helpers/widgets/tenant/service_selector.dart'; +import 'package:marco/model/attendance/organization_per_project_list_model.dart'; +import 'package:marco/model/tenant/tenant_services_model.dart'; import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/multiple_select_role_bottomsheet.dart'; class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; @@ -53,9 +54,9 @@ class _AssignTaskBottomSheetState extends State { final TextEditingController targetController = TextEditingController(); final TextEditingController descriptionController = TextEditingController(); - final ScrollController _employeeListScrollController = ScrollController(); String? selectedProjectId; + String? selectedRoleId; Organization? selectedOrganization; Service? selectedService; @@ -84,13 +85,14 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, - serviceId: selectedService?.id); + await controller.fetchTaskData( + selectedProjectId, + serviceId: selectedService?.id, + ); } @override void dispose() { - _employeeListScrollController.dispose(); targetController.dispose(); descriptionController.dispose(); super.dispose(); @@ -98,20 +100,21 @@ class _AssignTaskBottomSheetState extends State { @override Widget build(BuildContext context) { - return Obx(() => BaseBottomSheet( - title: "Assign Task", - child: _buildAssignTaskForm(), - onCancel: () => Get.back(), - onSubmit: _onAssignTaskPressed, - isSubmitting: controller.isAssigningTask.value, - )); + return Obx( + () => BaseBottomSheet( + title: "Assign Task", + child: _buildAssignTaskForm(), + onCancel: () => Get.back(), + onSubmit: _onAssignTaskPressed, + isSubmitting: controller.isAssigningTask.value, + ), + ); } Widget _buildAssignTaskForm() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Organization Selector SizedBox( height: 50, child: OrganizationSelector( @@ -123,9 +126,9 @@ class _AssignTaskBottomSheetState extends State { }, ), ), + MySpacing.height(12), - // Service Selector SizedBox( height: 50, child: ServiceSelector( @@ -137,40 +140,27 @@ class _AssignTaskBottomSheetState extends State { }, ), ), + MySpacing.height(16), - - // Work Location Info - _infoRow( - Icons.location_on, - "Work Location", - "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}", - ), + _infoRow(Icons.location_on, "Work Location", + "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), const Divider(), - - // Pending Task Info _infoRow( Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, - child: Row( - children: [ - MyText.titleMedium("Select Team :", fontWeight: 600), - const SizedBox(width: 4), - const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)), - ], - ), + child: Row(children: [ + MyText.titleMedium("Select Team :", fontWeight: 600), + const SizedBox(width: 4), + const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)), + ]), ), + MySpacing.height(8), - // ------------------------------- - // Employee selector (REPLACED) - // ------------------------------- - // We show a button-like container (with border) that opens the reusable - // EmployeeSelectionBottomSheet. Selected employees are reflected using - // existing controller.uploadingStates & controller.selectedEmployees. + /// TEAM SELECT BOX GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( @@ -180,41 +170,47 @@ class _AssignTaskBottomSheetState extends State { borderRadius: BorderRadius.circular(6), ), child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText("Select team members", - color: Colors.grey.shade700); - } - // show summary text when there are selected employees - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - ), + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], +) + ), ), - MySpacing.height(8), - // Selected Employees Chips (keeps existing behavior) + MySpacing.height(8), _buildSelectedEmployees(), MySpacing.height(8), - // Target Input _buildTextField( icon: Icons.track_changes, label: "Target for Today :", @@ -223,9 +219,9 @@ class _AssignTaskBottomSheetState extends State { keyboardType: const TextInputType.numberWithOptions(decimal: true), validatorType: "target", ), + MySpacing.height(16), - // Description Input _buildTextField( icon: Icons.description, label: "Description :", @@ -253,21 +249,21 @@ class _AssignTaskBottomSheetState extends State { ), items: [ const PopupMenuItem(value: 'all', child: Text("All Roles")), - ...controller.roles.map((role) { - return PopupMenuItem( + ...controller.roles.map( + (role) => PopupMenuItem( value: role['id'].toString(), child: Text(role['name'] ?? 'Unknown Role'), - ); - }), + ), + ), ], ).then((value) { - if (value != null) - controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) { + selectedRoleId = value == 'all' ? null : value; + controller.onRoleSelected(selectedRoleId); + } }); } - // Removed old inline employee list; selection handled by bottom sheet. - Widget _buildSelectedEmployees() { return Obx(() { if (controller.selectedEmployees.isEmpty) return Container(); @@ -319,7 +315,9 @@ class _AssignTaskBottomSheetState extends State { maxLines: maxLines, decoration: InputDecoration( hintText: hintText, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), @@ -344,61 +342,62 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", - fontWeight: 600, color: Colors.black), + child: MyText.titleMedium( + "$title: ", + fontWeight: 600, + color: Colors.black, + ), ), TextSpan( - text: value, style: const TextStyle(color: Colors.black)), + text: value, + style: const TextStyle(color: Colors.black), + ), ], ), ), - ), + ) ], ), ); } Future _openEmployeeSelectionSheet() async { - // Open the existing EmployeeSelectionBottomSheet final result = await showModalBottomSheet>( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectionBottomSheet( - initiallySelected: controller.selectedEmployees.toList(), - multipleSelection: true, - title: 'Select Team Members', - ), + 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, + ), + ); + }, + ); + }, ); - if (result == null) return; - - // Merge returned employees into controller.uploadingStates & controller.selectedEmployees - // 1) Reset all uploadingStates to false, then set true for selected - controller.uploadingStates.forEach((key, rx) { - rx.value = false; - }); - - for (final emp in result) { - final idStr = emp.id.toString(); - if (controller.uploadingStates.containsKey(idStr)) { - controller.uploadingStates[idStr]?.value = true; - } else { - // if uploadingStates doesn't have the id yet, add it (safe fallback) - controller.uploadingStates[idStr] = RxBool(true); - } - } - - // 2) Update selectedEmployees list in controller - controller.selectedEmployees.assignAll(result); - - // 3) Call controller helper (keeps existing behavior) - try { + if (result != null) { + controller.selectedEmployees.assignAll(result); controller.updateSelectedEmployees(); - } catch (_) { - // If controller does not implement updateSelectedEmployees, ignore. } } @@ -410,18 +409,20 @@ class _AssignTaskBottomSheetState extends State { if (selectedTeam.isEmpty) { showAppSnackbar( - title: "Team Required", - message: "Please select at least one team member", - type: SnackbarType.error); + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error, + ); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { showAppSnackbar( - title: "Invalid Input", - message: "Please enter a valid target number", - type: SnackbarType.error); + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error, + ); return; } @@ -437,9 +438,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { showAppSnackbar( - title: "Description Required", - message: "Please enter a description", - type: SnackbarType.error); + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error, + ); return; } diff --git a/lib/model/directory/edit_bucket_bottom_sheet.dart b/lib/model/directory/edit_bucket_bottom_sheet.dart index f4dedfb..d115377 100644 --- a/lib/model/directory/edit_bucket_bottom_sheet.dart +++ b/lib/model/directory/edit_bucket_bottom_sheet.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:collection/collection.dart'; -import 'package:on_field_work/controller/directory/manage_bucket_controller.dart'; -import 'package:on_field_work/controller/directory/directory_controller.dart'; -import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; -import 'package:on_field_work/model/employees/employee_model.dart'; -import 'package:on_field_work/model/directory/contact_bucket_list_model.dart'; +import 'package:marco/controller/directory/manage_bucket_controller.dart'; +import 'package:marco/controller/directory/directory_controller.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/directory/contact_bucket_list_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class EditBucketBottomSheet { static void show( @@ -21,10 +22,8 @@ class EditBucketBottomSheet { final nameController = TextEditingController(text: bucket.name); final descController = TextEditingController(text: bucket.description); - final searchController = TextEditingController(); final selectedIds = RxSet({...bucket.employeeIds}); - final searchText = ''.obs; InputDecoration _inputDecoration(String label) { return InputDecoration( @@ -84,6 +83,15 @@ class EditBucketBottomSheet { } } + Future _handleSubmitBottomSheet(BuildContext sheetContext) async { + await _handleSubmit(); + + // close bottom sheet safely + if (Navigator.of(sheetContext).canPop()) { + Navigator.of(sheetContext).pop(); + } + } + Widget _formContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -101,117 +109,72 @@ class EditBucketBottomSheet { MySpacing.height(20), MyText.labelLarge('Shared With', fontWeight: 600), MySpacing.height(8), - Obx(() => TextField( - controller: searchController, - onChanged: (value) => searchText.value = value.toLowerCase(), - decoration: InputDecoration( - hintText: 'Search employee...', - prefixIcon: const Icon(Icons.search, size: 20), - suffixIcon: searchText.value.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, size: 18), - onPressed: () { - searchController.clear(); - searchText.value = ''; - }, - ) - : null, - isDense: true, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - filled: true, - fillColor: Colors.grey.shade100, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: Colors.grey.shade300), - ), - ), - )), - MySpacing.height(8), Obx(() { - final filtered = allEmployees.where((emp) { - final fullName = '${emp.firstName} ${emp.lastName}'.toLowerCase(); - return fullName.contains(searchText.value); - }).toList(); + if (selectedIds.isEmpty) return const SizedBox.shrink(); - return SizedBox( - height: 180, - child: ListView.separated( - itemCount: filtered.length, - separatorBuilder: (_, __) => const SizedBox(height: 2), - itemBuilder: (context, index) { - final emp = filtered[index]; - final fullName = '${emp.firstName} ${emp.lastName}'.trim(); + final selectedEmployees = + allEmployees.where((e) => selectedIds.contains(e.id)).toList(); - return Obx(() => Theme( - data: Theme.of(context).copyWith( - unselectedWidgetColor: Colors.grey.shade500, - checkboxTheme: CheckboxThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4)), - side: const BorderSide(color: Colors.grey), - fillColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; - } - return Colors.white; - }), - checkColor: MaterialStateProperty.all(Colors.white), - ), - ), - child: CheckboxListTile( - dense: true, - contentPadding: EdgeInsets.zero, - visualDensity: const VisualDensity(vertical: -4), - controlAffinity: ListTileControlAffinity.leading, - value: selectedIds.contains(emp.id), - onChanged: emp.id == ownerId - ? null - : (val) { - if (val == true) { - selectedIds.add(emp.id); - } else { - selectedIds.remove(emp.id); - } - }, - title: Row( - children: [ - Expanded( - child: MyText.bodyMedium( - fullName.isNotEmpty ? fullName : 'Unnamed', - fontWeight: 600, - ), - ), - if (emp.id == ownerId) - Container( - margin: const EdgeInsets.only(left: 6), - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(4), - ), - child: MyText.labelSmall( - "Owner", - fontWeight: 600, - color: Colors.red, - ), - ), - ], - ), - subtitle: emp.jobRole.isNotEmpty - ? MyText.bodySmall( - emp.jobRole, - color: Colors.grey.shade600, - ) - : null, - ), - )); - }, - ), + return Wrap( + spacing: 8, + children: selectedEmployees.map((emp) { + final fullName = '${emp.firstName} ${emp.lastName}'.trim(); + + return Chip( + label: Text(fullName), + onDeleted: emp.id == ownerId + ? null + : () => selectedIds.remove(emp.id), + ); + }).toList(), ); }), + MySpacing.height(8), + +// --- Open new EmployeeSelectionBottomSheet --- + GestureDetector( + onTap: () async { + final initiallySelected = allEmployees + .where((e) => selectedIds.contains(e.id)) + .toList(); + + final result = await showModalBottomSheet>( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(22)), + ), + builder: (_) => EmployeeSelectionBottomSheet( + initiallySelected: initiallySelected, + multipleSelection: true, + title: "Shared With", + ), + ); + + if (result != null) { + selectedIds + ..clear() + ..addAll(result.map((e) => e.id)); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + children: const [ + Icon(Icons.search, color: Colors.grey), + SizedBox(width: 8), + Expanded(child: Text("Search & Select Employees")), + ], + ), + ), + ), + MySpacing.height(8), + const SizedBox.shrink(), ], ); } @@ -224,7 +187,7 @@ class EditBucketBottomSheet { return BaseBottomSheet( title: "Edit Bucket", onCancel: () => Navigator.pop(context), - onSubmit: _handleSubmit, + onSubmit: () => _handleSubmitBottomSheet(context), child: _formContent(), ); }, diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 5ad9375..984aeff 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -159,6 +159,26 @@ class _EmployeeSelectionBottomSheetState final emp = results[index]; final isSelected = _selectedEmployees.contains(emp); + Widget trailingWidget; + + if (widget.multipleSelection) { + // Multiple selection β†’ normal checkbox + trailingWidget = Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, + ), + ); + } else { + // Single selection β†’ check circle + trailingWidget = isSelected + ? const Icon(Icons.check_circle, color: Colors.blueAccent) + : const Icon(Icons.circle_outlined, color: Colors.grey); + } + return ListTile( leading: CircleAvatar( backgroundColor: Colors.blueAccent, @@ -170,15 +190,7 @@ class _EmployeeSelectionBottomSheetState ), title: Text('${emp.firstName} ${emp.lastName}'), subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) => _toggleEmployee(emp), - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), - ), + trailing: trailingWidget, onTap: () => _toggleEmployee(emp), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart new file mode 100644 index 0000000..1ffd1e0 --- /dev/null +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart'; + +class MultipleSelectRoleBottomSheet extends StatefulWidget { + final String title; + final bool multipleSelection; + + final String projectId; + final String? serviceId; + final String? organizationId; + final String? roleId; + final ScrollController? scrollController; + + final List initiallySelected; + + const MultipleSelectRoleBottomSheet({ + super.key, + this.title = "Select Employees", + this.multipleSelection = true, + required this.projectId, + this.serviceId, + this.organizationId, + this.roleId, + this.initiallySelected = const [], + this.scrollController, + }); + + @override + State createState() => + _MultipleSelectRoleBottomSheetState(); +} + +class _MultipleSelectRoleBottomSheetState + extends State { + final RxList _employees = [].obs; + final RxList _filtered = [].obs; + final RxBool _isLoading = true.obs; + + late RxList _selected; + final TextEditingController _searchController = TextEditingController(); + + late DailyTaskPlanningController controller; + + @override + void initState() { + super.initState(); + _selected = widget.initiallySelected.obs; + controller = Get.find(); + _fetchEmployeesFiltered(); + } + + Future _fetchEmployeesFiltered() async { + _isLoading.value = true; + try { + List employees = controller.employees.toList(); + + if (widget.roleId != null && widget.roleId!.isNotEmpty) { + employees = employees + .where((emp) => emp.jobRoleID == widget.roleId) + .toList(); + } + + // Selected first + employees.sort((a, b) { + final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; + final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; + return aSel != bSel + ? aSel.compareTo(bSel) + : a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _employees.assignAll(employees); + _filtered.assignAll(employees); + } catch (e) { + print("Error fetching employees: $e"); + } finally { + _isLoading.value = false; + } + } + + void _onSearch(String text) { + if (text.isEmpty) { + _filtered.assignAll(_employees); + } else { + _filtered.assignAll( + _employees.where((e) => + e.name.toLowerCase().contains(text.toLowerCase()) || + e.designation.toLowerCase().contains(text.toLowerCase())), + ); + } + + // Selected on top + _filtered.sort((a, b) { + final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; + final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; + return aSel != bSel + ? aSel.compareTo(bSel) + : a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + } + + void _onTap(EmployeeModel emp) { + if (widget.multipleSelection) { + if (_selected.any((e) => e.id == emp.id)) { + _selected.removeWhere((e) => e.id == emp.id); + } else { + _selected.add(emp); + } + } else { + _selected.assignAll([emp]); + Get.back(result: _selected); + } + + _onSearch(_searchController.text.trim()); + } + + bool _isSelected(EmployeeModel emp) { + return _selected.any((e) => e.id == emp.id); + } + + Widget _searchBar() => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: TextField( + controller: _searchController, + onChanged: _onSearch, + decoration: InputDecoration( + hintText: 'Search employees...', + filled: true, + fillColor: Colors.grey.shade100, + prefixIcon: const Icon(Icons.search, color: Colors.grey), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.close, color: Colors.grey), + onPressed: () { + _searchController.clear(); + _onSearch(''); + }, + ) + : null, + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + ), + ); + + /// ⭐ NEW β€” Chips showing selected employees + Widget _selectedChips() { + return Obx(() { + if (_selected.isEmpty) return const SizedBox(); + + return Wrap( + spacing: 8, + runSpacing: 8, + children: _selected.map((emp) { + return Chip( + label: Text(emp.name), + deleteIcon: const Icon(Icons.close), + onDeleted: () { + _selected.remove(emp); + _onSearch(_searchController.text.trim()); + }, + backgroundColor: Colors.blue.shade50, + ); + }).toList(), + ); + }); + } + + @override + Widget build(BuildContext context) { + return BaseBottomSheet( + title: widget.title, + onCancel: () => Get.back(), + onSubmit: () => Get.back(result: _selected), + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.55, + child: Column( + children: [ + _searchBar(), + + /// ⭐ Chips shown right below search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _selectedChips(), + ), + + const SizedBox(height: 6), + + Expanded( + child: Obx(() { + if (_isLoading.value) { + return SkeletonLoaders.employeeSkeletonCard(); + } + + if (_filtered.isEmpty) { + return const Center(child: Text("No employees found")); + } + + return ListView.builder( + controller: widget.scrollController, + padding: const EdgeInsets.only(bottom: 20), + itemCount: _filtered.length, + itemBuilder: (_, index) { + final emp = _filtered[index]; + final isSelected = _isSelected(emp); + + return ListTile( + onTap: () => _onTap(emp), + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + style: const TextStyle(color: Colors.white), + ), + ), + title: Text(emp.name), + subtitle: Text(emp.designation), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _onTap(emp), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + ); + }, + ); + }), + ), + ], + ), + ), + ); + } +} diff --git a/lib/model/expense/add_expense_bottom_sheet.dart b/lib/model/expense/add_expense_bottom_sheet.dart index 1f6711a..88ef2ec 100644 --- a/lib/model/expense/add_expense_bottom_sheet.dart +++ b/lib/model/expense/add_expense_bottom_sheet.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:on_field_work/controller/expense/add_expense_controller.dart'; -import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/model/expense/expense_type_model.dart'; -import 'package:on_field_work/model/expense/payment_types_model.dart'; -import 'package:on_field_work/model/expense/employee_selector_bottom_sheet.dart'; -import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; -import 'package:on_field_work/helpers/utils/validators.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; -import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart'; -import 'package:on_field_work/helpers/widgets/expense/expense_form_widgets.dart'; +import 'package:marco/controller/expense/add_expense_controller.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/model/expense/expense_type_model.dart'; +import 'package:marco/model/expense/payment_types_model.dart'; +// import 'package:marco/model/expense/employee_selector_bottom_sheet.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/utils/validators.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; /// Show bottom sheet wrapper Future showAddExpenseBottomSheet({ @@ -52,24 +54,36 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> /// Show employee list Future _showEmployeeList() async { - await showModalBottomSheet( + final result = await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (_) => ReusableEmployeeSelectorBottomSheet( - searchController: controller.employeeSearchController, - searchResults: controller.employeeSearchResults, - isSearching: controller.isSearchingEmployees, - onSearch: controller.searchEmployees, - onSelect: (emp) => controller.selectedPaidBy.value = emp, + builder: (_) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedPaidBy.value != null + ? [controller.selectedPaidBy.value!] + : [], + multipleSelection: false, + title: "Select Paid By", ), ); - controller.employeeSearchController.clear(); - controller.employeeSearchResults.clear(); + if (result == null) return; + + // result will be EmployeeModel or [EmployeeModel] + if (result is EmployeeModel) { + controller.setSelectedPaidBy(result); + } else if (result is List && result.isNotEmpty) { + controller.setSelectedPaidBy(result.first as EmployeeModel); + } + + // cleanup + try { + controller.employeeSearchController.clear(); + controller.employeeSearchResults.clear(); + } catch (_) {} } /// Generic option list @@ -343,23 +357,26 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> const SectionTitle( icon: Icons.person_outline, title: "Paid By", requiredField: true), MySpacing.height(6), + // Main tile: tap to choose mode + selection sheet GestureDetector( onTap: _showEmployeeList, child: TileContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - controller.selectedPaidBy.value == null - ? "Select Paid By" - : '${controller.selectedPaidBy.value?.firstName ?? ''} ${controller.selectedPaidBy.value?.lastName ?? ''}', - style: const TextStyle(fontSize: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + controller.selectedPaidBy.value?.name ?? "Select Paid By", + style: TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, ), - const Icon(Icons.arrow_drop_down, size: 22), - ], - ), - ), + ), + Icon(Icons.arrow_drop_down, size: 22), + ], + )), ), + // small helper: long-press to quickly open multi-select directly (optional) + const SizedBox(height: 6), ], ); } diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index 630885d..a23bfcf 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:on_field_work/controller/finance/add_payment_request_controller.dart'; -import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; -import 'package:on_field_work/helpers/utils/validators.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; -import 'package:on_field_work/helpers/widgets/expense/expense_form_widgets.dart'; -import 'package:on_field_work/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/controller/finance/add_payment_request_controller.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/utils/validators.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; +import 'package:marco/helpers/widgets/expense/expense_form_widgets.dart'; +import 'package:marco/helpers/widgets/my_confirmation_dialog.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; Future showPaymentRequestBottomSheet({ bool isEdit = false, @@ -206,7 +208,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ? null : "Enter valid amount"), _gap(), - _buildPayeeAutocompleteField(), + _buildPayeeField(), _gap(), _buildDropdown( "Currency", @@ -347,74 +349,35 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ); } - Widget _buildPayeeAutocompleteField() { + Widget _buildPayeeField() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SectionTitle( - icon: Icons.person_outline, title: "Payee", requiredField: true), - const SizedBox(height: 6), - Autocomplete( - optionsBuilder: (textEditingValue) { - final query = textEditingValue.text.toLowerCase(); - return query.isEmpty - ? const Iterable.empty() - : controller.payees - .where((p) => p.toLowerCase().contains(query)); - }, - displayStringForOption: (option) => option, - fieldViewBuilder: - (context, fieldController, focusNode, onFieldSubmitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (fieldController.text != controller.selectedPayee.value) { - fieldController.text = controller.selectedPayee.value; - fieldController.selection = TextSelection.fromPosition( - TextPosition(offset: fieldController.text.length)); - } - }); - - return TextFormField( - controller: fieldController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: "Type or select payee", - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), + const SectionTitle( + icon: Icons.person_outline, + title: "Payee", + requiredField: true, + ), + MySpacing.height(6), + GestureDetector( + onTap: _showPayeeSelector, + child: TileContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Obx(() => Text( + controller.selectedPayee.value?.name ?? "Select Payee", + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + )), ), - ), - validator: (v) => - v == null || v.trim().isEmpty ? "Please enter payee" : null, - onChanged: (val) => controller.selectedPayee.value = val, - ); - }, - onSelected: (selection) => controller.selectedPayee.value = selection, - optionsViewBuilder: (context, onSelected, options) => Material( - color: Colors.white, - elevation: 4, - borderRadius: BorderRadius.circular(8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (_, index) => InkWell( - onTap: () => onSelected(options.elementAt(index)), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 12), - child: Text(options.elementAt(index), - style: const TextStyle(fontSize: 14)), - ), - ), - ), + const Icon(Icons.arrow_drop_down, size: 22), + ], ), ), ), + const SizedBox(height: 6), ], ); } @@ -533,7 +496,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> if (controller.selectedCategory.value == null) { return _showError("Please select a category"); } - if (controller.selectedPayee.value.isEmpty) { + if (controller.selectedPayee.value == null) { return _showError("Please select a payee"); } if (controller.selectedCurrency.value == null) { @@ -542,6 +505,25 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> return true; } + Future _showPayeeSelector() async { + final result = await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EmployeeSelectionBottomSheet( + title: "Select Payee", + multipleSelection: false, + initiallySelected: controller.selectedPayee.value != null + ? [controller.selectedPayee.value!] + : [], + ), + ); + + if (result is EmployeeModel) { + controller.selectedPayee.value = result; + } + } + bool _showError(String msg) { showAppSnackbar(title: "Error", message: msg, type: SnackbarType.error); return false; diff --git a/lib/model/finance/payment_request_filter_bottom_sheet.dart b/lib/model/finance/payment_request_filter_bottom_sheet.dart index d468da1..737495d 100644 --- a/lib/model/finance/payment_request_filter_bottom_sheet.dart +++ b/lib/model/finance/payment_request_filter_bottom_sheet.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:on_field_work/controller/finance/payment_request_controller.dart'; -import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; -import 'package:on_field_work/helpers/widgets/my_text_style.dart'; -import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/helpers/widgets/date_range_picker.dart'; -import 'package:on_field_work/model/employees/employee_model.dart'; -import 'package:on_field_work/model/expense/employee_selector_for_filter_bottom_sheet.dart'; +import 'package:marco/controller/finance/payment_request_controller.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class PaymentRequestFilterBottomSheet extends StatefulWidget { final PaymentRequestController controller; @@ -441,9 +441,9 @@ class _PaymentRequestFilterBottomSheetState shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: (query) => searchEmployees(query, items), + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: selectedEmployees.toList(), + multipleSelection: true, title: title, ), ); diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index f2da450..380aed2 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -2,15 +2,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:on_field_work/controller/expense/expense_screen_controller.dart'; -import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; -import 'package:on_field_work/helpers/widgets/my_spacing.dart'; -import 'package:on_field_work/helpers/widgets/my_text.dart'; -import 'package:on_field_work/helpers/widgets/my_text_style.dart'; -import 'package:on_field_work/model/employees/employee_model.dart'; -import 'package:on_field_work/model/expense/employee_selector_for_filter_bottom_sheet.dart'; -import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; -import 'package:on_field_work/helpers/widgets/date_range_picker.dart'; +import 'package:marco/controller/expense/expense_screen_controller.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_text_style.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/date_range_picker.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; + class ExpenseFilterBottomSheet extends StatefulWidget { final ExpenseController expenseController; @@ -303,12 +304,13 @@ class _ExpenseFilterBottomSheetState extends State shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectorBottomSheet( - selectedEmployees: selectedEmployees, - searchEmployees: searchEmployees, + builder: (context) => EmployeeSelectionBottomSheet( + initiallySelected: selectedEmployees.toList(), + multipleSelection: true, title: title, ), ); + if (result != null) selectedEmployees.assignAll(result); }, child: Container( From 02758a03bfebbec791219b02cb7d8af8af46c093 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:17:00 +0530 Subject: [PATCH 27/37] check box color change to white --- .../multiple_select_role_bottomsheet.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index 1ffd1e0..f7e5dff 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -59,9 +59,8 @@ class _MultipleSelectRoleBottomSheetState List employees = controller.employees.toList(); if (widget.roleId != null && widget.roleId!.isNotEmpty) { - employees = employees - .where((emp) => emp.jobRoleID == widget.roleId) - .toList(); + employees = + employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } // Selected first @@ -226,6 +225,16 @@ class _MultipleSelectRoleBottomSheetState trailing: Checkbox( value: isSelected, onChanged: (_) => _onTap(emp), + fillColor: + MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.blueAccent; // Selected color + } + return Colors.white; // Unselected square color + }), + checkColor: Colors.white, // Check mark color + side: const BorderSide( + color: Colors.grey), // Outline for unselected ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From a11fc8005049476d7162eddfc28cb31db997b8ea Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 17:36:56 +0530 Subject: [PATCH 28/37] UI enhancement --- .../assign_task_bottom_sheet .dart | 106 ++++++++---------- .../multiple_select_bottomsheet.dart | 13 ++- .../multiple_select_role_bottomsheet.dart | 49 ++------ 3 files changed, 63 insertions(+), 105 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 5191079..ce19774 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -164,47 +164,45 @@ class _AssignTaskBottomSheetState extends State { GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), - // Expanded name area - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText( - "Select team members", - color: Colors.grey.shade700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], -) - - ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + )), ), MySpacing.height(8), @@ -272,20 +270,14 @@ class _AssignTaskBottomSheetState extends State { spacing: 4, runSpacing: 4, children: controller.selectedEmployees.map((e) { - return Obx(() { - final isSelected = controller.uploadingStates[e.id]?.value ?? false; - if (!isSelected) return Container(); - - return Chip( - label: Text(e.name, style: const TextStyle(color: Colors.white)), - backgroundColor: const Color.fromARGB(255, 95, 132, 255), - deleteIcon: const Icon(Icons.close, color: Colors.white), - onDeleted: () { - controller.uploadingStates[e.id]?.value = false; - controller.updateSelectedEmployees(); - }, - ); - }); + return Chip( + label: Text(e.name, style: const TextStyle(color: Colors.white)), + backgroundColor: const Color.fromARGB(255, 95, 132, 255), + deleteIcon: const Icon(Icons.close, color: Colors.white), + onDeleted: () { + controller.selectedEmployees.remove(e); + }, + ); }).toList(), ); }); @@ -380,7 +372,6 @@ class _AssignTaskBottomSheetState extends State { color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - child: MultipleSelectRoleBottomSheet( projectId: selectedProjectId!, organizationId: selectedOrganization?.id, @@ -396,16 +387,13 @@ class _AssignTaskBottomSheetState extends State { ); if (result != null) { - controller.selectedEmployees.assignAll(result); - controller.updateSelectedEmployees(); + controller.selectedEmployees + .assignAll(result); // RxList updates UI automatically } } void _onAssignTaskPressed() { - final selectedTeam = controller.uploadingStates.entries - .where((e) => e.value.value) - .map((e) => e.key) - .toList(); + final selectedTeam = controller.selectedEmployees; if (selectedTeam.isEmpty) { showAppSnackbar( @@ -449,7 +437,7 @@ class _AssignTaskBottomSheetState extends State { workItemId: widget.workItemId, plannedTask: target.toInt(), description: description, - taskTeam: selectedTeam, + taskTeam: selectedTeam.map((e) => e.id).toList(), // pass IDs assignmentDate: widget.assignmentDate, organizationId: selectedOrganization?.id, serviceId: selectedService?.id, diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 984aeff..f3be00c 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -68,13 +68,18 @@ class _EmployeeSelectionBottomSheetState .toList(); // ------------------------------------------------------ - // πŸ”₯ Auto-move selected employees to top + // Auto-move selected employees to top // ------------------------------------------------------ results.sort((a, b) { - final aSel = _selectedEmployees.contains(a) ? 0 : 1; - final bSel = _selectedEmployees.contains(b) ? 0 : 1; + if (widget.multipleSelection) { + // Only move selected employees to top in multi-select + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; - if (aSel != bSel) return aSel.compareTo(bSel); + if (aSel != bSel) return aSel.compareTo(bSel); + } + + // Otherwise, keep original order (or alphabetically if needed) return a.name.toLowerCase().compareTo(b.name.toLowerCase()); }); diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index f7e5dff..a1f4951 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -63,7 +63,6 @@ class _MultipleSelectRoleBottomSheetState employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } - // Selected first employees.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -92,7 +91,6 @@ class _MultipleSelectRoleBottomSheetState ); } - // Selected on top _filtered.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -110,8 +108,8 @@ class _MultipleSelectRoleBottomSheetState _selected.add(emp); } } else { - _selected.assignAll([emp]); - Get.back(result: _selected); + // Single selection β†’ return immediately + Get.back(result: [emp]); } _onSearch(_searchController.text.trim()); @@ -150,49 +148,17 @@ class _MultipleSelectRoleBottomSheetState ), ); - /// ⭐ NEW β€” Chips showing selected employees - Widget _selectedChips() { - return Obx(() { - if (_selected.isEmpty) return const SizedBox(); - - return Wrap( - spacing: 8, - runSpacing: 8, - children: _selected.map((emp) { - return Chip( - label: Text(emp.name), - deleteIcon: const Icon(Icons.close), - onDeleted: () { - _selected.remove(emp); - _onSearch(_searchController.text.trim()); - }, - backgroundColor: Colors.blue.shade50, - ); - }).toList(), - ); - }); - } - @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Get.back(), - onSubmit: () => Get.back(result: _selected), + onSubmit: () => Get.back(result: _selected.toList()), // Return plain list child: SizedBox( height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ _searchBar(), - - /// ⭐ Chips shown right below search bar - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: _selectedChips(), - ), - - const SizedBox(height: 6), - Expanded( child: Obx(() { if (_isLoading.value) { @@ -228,13 +194,12 @@ class _MultipleSelectRoleBottomSheetState fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; // Selected color + return Colors.blueAccent; } - return Colors.white; // Unselected square color + return Colors.white; }), - checkColor: Colors.white, // Check mark color - side: const BorderSide( - color: Colors.grey), // Outline for unselected + checkColor: Colors.white, + side: const BorderSide(color: Colors.grey), ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From c4897bccc87a9178aad11721c68691d8338c8865 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 10:50:50 +0530 Subject: [PATCH 29/37] done landscape responsive of all screen --- .../add_payment_request_bottom_sheet.dart | 149 +++++++++++++++--- lib/view/finance/advance_payment_screen.dart | 29 ++-- 2 files changed, 140 insertions(+), 38 deletions(-) diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index a23bfcf..7fcd508 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -78,6 +78,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> 'name': data["projectName"], }; + controller.selectedPayee.value = data["payee"] ?? ""; controller.isAdvancePayment.value = data["isAdvancePayment"] ?? false; @@ -142,7 +143,38 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> } else { success = await controller.submitPaymentRequest(); } + return Obx(() => SafeArea( + child: Form( + key: _formKey, + child: BaseBottomSheet( + title: widget.isEdit + ? "Edit Payment Request" + : "Create Payment Request", + isSubmitting: controller.isSubmitting.value, + onCancel: Get.back, + submitText: "Save as Draft", + onSubmit: () async { + if (_formKey.currentState!.validate() && + _validateSelections()) { + bool success = false; + + if (widget.isEdit && widget.existingData != null) { + final requestId = + widget.existingData!['id']?.toString() ?? ''; + if (requestId.isNotEmpty) { + success = await controller.submitEditedPaymentRequest( + requestId: requestId); + } else { + _showError("Invalid Payment Request ID"); + return; + } + } else { + success = await controller.submitPaymentRequest(); + } + if (success) { + Get.back(); + widget.onUpdated?.call(); if (success) { Get.back(); widget.onUpdated?.call(); @@ -157,6 +189,23 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> } } }, + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildDropdown( + showAppSnackbar( + title: "Success", + message: widget.isEdit + ? "Payment request updated successfully!" + : "Payment request created successfully!", + type: SnackbarType.success, + ); + } + } + }, child: SingleChildScrollView( padding: const EdgeInsets.only(bottom: 20), child: Column( @@ -174,6 +223,10 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> key: _projectDropdownKey, ), _gap(), + _buildDropdown( + key: _projectDropdownKey, + ), + _gap(), _buildDropdown( "Expense Category", Icons.category_outlined, @@ -208,7 +261,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ? null : "Enter valid amount"), _gap(), - _buildPayeeField(), + _buildPayeeAutocompleteField(), _gap(), _buildDropdown( "Currency", @@ -230,6 +283,19 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> _buildAttachmentsSection(), MySpacing.height(30), ], + ), + key: _currencyDropdownKey, + ), + _gap(), + _buildTextField("Description", Icons.description_outlined, + controller.descriptionController, + hint: "Enter description", + maxLines: 3, + validator: Validators.requiredField), + _gap(), + _buildAttachmentsSection(), + MySpacing.height(30), + ], ), ), ), @@ -297,6 +363,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> final label = entry.value; final value = i == 0; + return Expanded( child: RadioListTile( contentPadding: EdgeInsets.zero, @@ -353,27 +420,67 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SectionTitle( - icon: Icons.person_outline, - title: "Payee", - requiredField: true, - ), - MySpacing.height(6), - GestureDetector( - onTap: _showPayeeSelector, - child: TileContainer( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Obx(() => Text( - controller.selectedPayee.value?.name ?? "Select Payee", - style: const TextStyle(fontSize: 15), - overflow: TextOverflow.ellipsis, - )), + SectionTitle( + icon: Icons.person_outline, title: "Payee", requiredField: true), + const SizedBox(height: 6), + Autocomplete( + optionsBuilder: (textEditingValue) { + final query = textEditingValue.text.toLowerCase(); + return query.isEmpty + ? const Iterable.empty() + : controller.payees + .where((p) => p.toLowerCase().contains(query)); + }, + displayStringForOption: (option) => option, + fieldViewBuilder: + (context, fieldController, focusNode, onFieldSubmitted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (fieldController.text != controller.selectedPayee.value) { + fieldController.text = controller.selectedPayee.value; + fieldController.selection = TextSelection.fromPosition( + TextPosition(offset: fieldController.text.length)); + } + }); + + return TextFormField( + controller: fieldController, + focusNode: focusNode, + decoration: InputDecoration( + hintText: "Type or select payee", + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide(color: Colors.grey.shade300), ), - const Icon(Icons.arrow_drop_down, size: 22), - ], + ), + validator: (v) => + v == null || v.trim().isEmpty ? "Please enter payee" : null, + onChanged: (val) => controller.selectedPayee.value = val, + ); + }, + onSelected: (selection) => controller.selectedPayee.value = selection, + optionsViewBuilder: (context, onSelected, options) => Material( + color: Colors.white, + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: options.length, + itemBuilder: (_, index) => InkWell( + onTap: () => onSelected(options.elementAt(index)), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 12), + child: Text(options.elementAt(index), + style: const TextStyle(fontSize: 14)), + ), + ), + ), ), ), ), diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 4a59de8..795c94e 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -50,6 +50,7 @@ class _AdvancePaymentScreenState extends State @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5), appBar: _buildAppBar(), body: SafeArea( @@ -58,25 +59,19 @@ class _AdvancePaymentScreenState extends State final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - return 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: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); + return 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, // ---------------- PORTRAIT (UNCHANGED) ---------------- child: !isLandscape From 6bae9e389c01ed5f443782fab0b9701f4aa7f155 Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 16:25:25 +0530 Subject: [PATCH 30/37] Done all screen landscape responsive for mobile and tablet --- lib/view/layouts/user_profile_right_bar.dart | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 17ef20d..a067379 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -15,6 +15,18 @@ import 'package:on_field_work/helpers/services/tenant_service.dart'; import 'package:on_field_work/view/tenant/tenant_selection_screen.dart'; import 'package:on_field_work/controller/tenant/tenant_switch_controller.dart'; import 'package:on_field_work/helpers/theme/theme_editor_widget.dart'; +import 'package:marco/helpers/services/storage/local_storage.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/avatar.dart'; +import 'package:marco/model/employees/employee_info.dart'; +import 'package:marco/controller/auth/mpin_controller.dart'; +import 'package:marco/view/employees/employee_profile_screen.dart'; +import 'package:marco/helpers/services/tenant_service.dart'; +import 'package:marco/view/tenant/tenant_selection_screen.dart'; +import 'package:marco/controller/tenant/tenant_switch_controller.dart'; +import 'package:marco/helpers/theme/theme_editor_widget.dart'; class UserProfileBar extends StatefulWidget { final bool isCondensed; @@ -53,6 +65,7 @@ class _UserProfileBarState extends State Widget build(BuildContext context) { final bool isCondensed = widget.isCondensed; + return Padding( padding: const EdgeInsets.only(left: 14), child: ClipRRect( @@ -146,6 +159,66 @@ class _UserProfileBarState extends State ], ), ), + bottom: true, + child: Stack( + children: [ + // ======================= MAIN PROFILE SIDEBAR ======================= + Offstage( + offstage: _isThemeEditorVisible, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _isLoading + ? const _LoadingSection() + : _userProfileSection(isCondensed), + if (!_isLoading && !isCondensed) + _switchTenantRow(), + MySpacing.height(12), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.7, + color: Colors.grey.withOpacity(0.25), + ), + MySpacing.height(12), + _supportAndSettingsMenu(isCondensed), + const Spacer(), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.35, + color: Colors.grey.withOpacity(0.18), + ), + _logoutButton(isCondensed), + ], + ), + ), + ), + ); + }, + ), + ), + + // ======================= THEME EDITOR VIEW ======================= + Offstage( + offstage: !_isThemeEditorVisible, + child: ThemeEditorWidget( + onClose: () { + setState(() => _isThemeEditorVisible = false); + }, + ), + ), + ], + ), + ), ), ), ), @@ -154,6 +227,8 @@ class _UserProfileBarState extends State // ==================== EXISTING CODE (UNCHANGED) ===================== + // ==================== EXISTING CODE (UNCHANGED) ===================== + Widget _switchTenantRow() { final TenantSwitchController tenantSwitchController = Get.put(TenantSwitchController()); @@ -257,6 +332,7 @@ class _UserProfileBarState extends State child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ); + // ⭐ FIXED β€” YOUR ORIGINAL INTENT, COMPLETED PROPERLY // ⭐ FIXED β€” YOUR ORIGINAL INTENT, COMPLETED PROPERLY Widget _noTenantContainer() => Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), @@ -267,7 +343,19 @@ class _UserProfileBarState extends State color: Colors.blue.shade200, width: 1, ), + border: Border.all( + color: Colors.blue.shade200, + width: 1, + ), ), + child: const Center( + child: Text( + "No organizations available", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), child: const Center( child: Text( "No organizations available", From d83bc838f475a72405dc799677342012410b4b4f Mon Sep 17 00:00:00 2001 From: Manish Date: Tue, 18 Nov 2025 17:13:44 +0530 Subject: [PATCH 31/37] added multiselect bottom sheet in assign task bottom sheet form --- .../assign_task_bottom_sheet .dart | 205 +++++++++--------- 1 file changed, 105 insertions(+), 100 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index ce19774..ff9b33b 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -15,8 +15,10 @@ import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; + +// Added imports for employee selection import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/employees/multiple_select_role_bottomsheet.dart'; +import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; @@ -48,6 +50,8 @@ class _AssignTaskBottomSheetState extends State { final DailyTaskPlanningController controller = Get.find(); final ProjectController projectController = Get.find(); + final OrganizationController orgController = + Get.put(OrganizationController()); final OrganizationController orgController = Get.put(OrganizationController()); final ServiceController serviceController = Get.put(ServiceController()); @@ -85,10 +89,8 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData( - selectedProjectId, - serviceId: selectedService?.id, - ); + await controller.fetchTaskData(selectedProjectId, + serviceId: selectedService?.id); } @override @@ -145,10 +147,13 @@ class _AssignTaskBottomSheetState extends State { _infoRow(Icons.location_on, "Work Location", "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), const Divider(), + + // Pending Task Info _infoRow( Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), + // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, child: Row(children: [ @@ -160,52 +165,52 @@ class _AssignTaskBottomSheetState extends State { MySpacing.height(8), - /// TEAM SELECT BOX + // ------------------------------- + // Employee selector (REPLACED) + // ------------------------------- + // We show a button-like container (with border) that opens the reusable + // EmployeeSelectionBottomSheet. Selected employees are reflected using + // existing controller.uploadingStates & controller.selectedEmployees. GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), - - // Expanded name area - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText( - "Select team members", - color: Colors.grey.shade700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } - - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); - - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), - - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - )), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText("Select team members", + color: Colors.grey.shade700); + } + // show summary text when there are selected employees + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), ), - MySpacing.height(8), + + // Selected Employees Chips (keeps existing behavior) _buildSelectedEmployees(), MySpacing.height(8), @@ -233,6 +238,8 @@ class _AssignTaskBottomSheetState extends State { } void _onRoleMenuPressed() { + final RenderBox overlay = + Overlay.of(context).context.findRenderObject() as RenderBox; final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final Size screenSize = overlay.size; @@ -255,13 +262,13 @@ class _AssignTaskBottomSheetState extends State { ), ], ).then((value) { - if (value != null) { - selectedRoleId = value == 'all' ? null : value; - controller.onRoleSelected(selectedRoleId); - } + if (value != null) + controller.onRoleSelected(value == 'all' ? null : value); }); } + // Removed old inline employee list; selection handled by bottom sheet. + Widget _buildSelectedEmployees() { return Obx(() { if (controller.selectedEmployees.isEmpty) return Container(); @@ -307,15 +314,16 @@ class _AssignTaskBottomSheetState extends State { maxLines: maxLines, decoration: InputDecoration( hintText: hintText, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), validator: (value) => this .controller .formFieldValidator(value, fieldType: validatorType), + validator: (value) => this + .controller + .formFieldValidator(value, fieldType: validatorType), ), ], ); @@ -334,16 +342,11 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium( - "$title: ", - fontWeight: 600, - color: Colors.black, - ), + child: MyText.titleMedium("$title: ", + fontWeight: 600, color: Colors.black), ), TextSpan( - text: value, - style: const TextStyle(color: Colors.black), - ), + text: value, style: const TextStyle(color: Colors.black)), ], ), ), @@ -354,41 +357,46 @@ class _AssignTaskBottomSheetState extends State { } Future _openEmployeeSelectionSheet() async { + // Open the existing EmployeeSelectionBottomSheet final result = await showModalBottomSheet>( context: context, isScrollControlled: 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: (context) => EmployeeSelectionBottomSheet( + initiallySelected: controller.selectedEmployees.toList(), + multipleSelection: true, + title: 'Select Team Members', + ), ); - if (result != null) { - controller.selectedEmployees - .assignAll(result); // RxList updates UI automatically + if (result == null) return; + + // Merge returned employees into controller.uploadingStates & controller.selectedEmployees + // 1) Reset all uploadingStates to false, then set true for selected + controller.uploadingStates.forEach((key, rx) { + rx.value = false; + }); + + for (final emp in result) { + final idStr = emp.id.toString(); + if (controller.uploadingStates.containsKey(idStr)) { + controller.uploadingStates[idStr]?.value = true; + } else { + // if uploadingStates doesn't have the id yet, add it (safe fallback) + controller.uploadingStates[idStr] = RxBool(true); + } + } + + // 2) Update selectedEmployees list in controller + controller.selectedEmployees.assignAll(result); + + // 3) Call controller helper (keeps existing behavior) + try { + controller.updateSelectedEmployees(); + } catch (_) { + // If controller does not implement updateSelectedEmployees, ignore. } } @@ -397,20 +405,18 @@ class _AssignTaskBottomSheetState extends State { if (selectedTeam.isEmpty) { showAppSnackbar( - title: "Team Required", - message: "Please select at least one team member", - type: SnackbarType.error, - ); + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { showAppSnackbar( - title: "Invalid Input", - message: "Please enter a valid target number", - type: SnackbarType.error, - ); + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error); return; } @@ -426,10 +432,9 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { showAppSnackbar( - title: "Description Required", - message: "Please enter a description", - type: SnackbarType.error, - ); + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error); return; } From c6c11cc6fca4696420df793b931c77367cac34a2 Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 11:57:13 +0530 Subject: [PATCH 32/37] advance paymen --- lib/view/finance/advance_payment_screen.dart | 58 +++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 795c94e..736cdd1 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -35,6 +35,8 @@ class _AdvancePaymentScreenState extends State } }); + _searchCtrl.addListener(() { + controller.searchQuery.value = _searchCtrl.text.trim(); _searchCtrl.addListener(() { controller.searchQuery.value = _searchCtrl.text.trim(); }); @@ -59,19 +61,25 @@ class _AdvancePaymentScreenState extends State final bool isLandscape = constraints.maxWidth > constraints.maxHeight; - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: RefreshIndicator( - onRefresh: () async { - final emp = controller.selectedEmployee.value; - if (emp != null) { - await controller.fetchAdvancePayments(emp.id.toString()); + return 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: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); } }, - color: Colors.white, - backgroundColor: contentTheme.primary, - strokeWidth: 2.5, - displacement: 60, // ---------------- PORTRAIT (UNCHANGED) ---------------- child: !isLandscape @@ -203,6 +211,13 @@ class _AdvancePaymentScreenState extends State } }); }, + onTap: () { + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted) { + FocusScope.of(context).requestFocus(_searchFocus); + } + }); + }, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), @@ -282,6 +297,12 @@ class _AdvancePaymentScreenState extends State TextPosition(offset: e.name.length), ); + _searchCtrl + ..text = e.name + ..selection = TextSelection.fromPosition( + TextPosition(offset: e.name.length), + ); + FocusScope.of(context).unfocus(); SystemChannels.textInput.invokeMethod('TextInput.hide'); controller.employees.clear(); @@ -408,7 +429,9 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - + // final formattedTime = + // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; + final project = item.name ?? ''; final desc = item.title ?? ''; final amount = (item.amount ?? 0).toDouble(); @@ -438,7 +461,16 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - + // if (formattedTime.isNotEmpty) ...[ + // const SizedBox(width: 6), + // Text( + // formattedTime, + // style: TextStyle( + // fontSize: 12, + // color: Colors.grey.shade500, + // fontStyle: FontStyle.italic), + // ), + // ] ], ), const SizedBox(height: 4), From 3c08ee447c37430de6d0d4f32ccb021df5ff962c Mon Sep 17 00:00:00 2001 From: Manish Date: Thu, 20 Nov 2025 12:00:28 +0530 Subject: [PATCH 33/37] unwanted code removed --- lib/view/finance/advance_payment_screen.dart | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 736cdd1..8aa9269 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -429,9 +429,7 @@ class _AdvancePaymentScreenState extends State ? DateFormat('dd MMM yyyy').format(parsedDate) : (dateStr.isNotEmpty ? dateStr : 'β€”'); - // final formattedTime = - // parsedDate != null ? DateFormat('hh:mm a').format(parsedDate) : ''; - + final project = item.name ?? ''; final desc = item.title ?? ''; final amount = (item.amount ?? 0).toDouble(); @@ -461,16 +459,7 @@ class _AdvancePaymentScreenState extends State style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - // if (formattedTime.isNotEmpty) ...[ - // const SizedBox(width: 6), - // Text( - // formattedTime, - // style: TextStyle( - // fontSize: 12, - // color: Colors.grey.shade500, - // fontStyle: FontStyle.italic), - // ), - // ] + ], ), const SizedBox(height: 4), From 40467074caa6dd76de17e295efd19cbd2e6abdd1 Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:04:40 +0530 Subject: [PATCH 34/37] upadted with select enployee sheet --- .../assign_task_bottom_sheet .dart | 185 +++++++++--------- .../multiple_select_role_bottomsheet.dart | 54 +++-- .../add_payment_request_bottom_sheet.dart | 82 ++------ 3 files changed, 156 insertions(+), 165 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index ff9b33b..2abbdf9 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -15,10 +15,8 @@ import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; - -// Added imports for employee selection import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/multiple_select_role_bottomsheet.dart'; class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; @@ -89,8 +87,10 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, - serviceId: selectedService?.id); + await controller.fetchTaskData( + selectedProjectId, + serviceId: selectedService?.id, + ); } @override @@ -147,13 +147,10 @@ class _AssignTaskBottomSheetState extends State { _infoRow(Icons.location_on, "Work Location", "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), const Divider(), - - // Pending Task Info _infoRow( Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, child: Row(children: [ @@ -165,12 +162,7 @@ class _AssignTaskBottomSheetState extends State { MySpacing.height(8), - // ------------------------------- - // Employee selector (REPLACED) - // ------------------------------- - // We show a button-like container (with border) that opens the reusable - // EmployeeSelectionBottomSheet. Selected employees are reflected using - // existing controller.uploadingStates & controller.selectedEmployees. + /// TEAM SELECT BOX GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( @@ -180,37 +172,44 @@ class _AssignTaskBottomSheetState extends State { borderRadius: BorderRadius.circular(6), ), child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText("Select team members", - color: Colors.grey.shade700); - } - // show summary text when there are selected employees - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - ), + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], +) + ), ), - MySpacing.height(8), - // Selected Employees Chips (keeps existing behavior) + MySpacing.height(8), _buildSelectedEmployees(), MySpacing.height(8), @@ -262,13 +261,13 @@ class _AssignTaskBottomSheetState extends State { ), ], ).then((value) { - if (value != null) - controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) { + selectedRoleId = value == 'all' ? null : value; + controller.onRoleSelected(selectedRoleId); + } }); } - // Removed old inline employee list; selection handled by bottom sheet. - Widget _buildSelectedEmployees() { return Obx(() { if (controller.selectedEmployees.isEmpty) return Container(); @@ -314,7 +313,9 @@ class _AssignTaskBottomSheetState extends State { maxLines: maxLines, decoration: InputDecoration( hintText: hintText, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), @@ -342,11 +343,16 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", - fontWeight: 600, color: Colors.black), + child: MyText.titleMedium( + "$title: ", + fontWeight: 600, + color: Colors.black, + ), ), TextSpan( - text: value, style: const TextStyle(color: Colors.black)), + text: value, + style: const TextStyle(color: Colors.black), + ), ], ), ), @@ -357,46 +363,42 @@ class _AssignTaskBottomSheetState extends State { } Future _openEmployeeSelectionSheet() async { - // Open the existing EmployeeSelectionBottomSheet final result = await showModalBottomSheet>( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectionBottomSheet( - initiallySelected: controller.selectedEmployees.toList(), - multipleSelection: true, - title: 'Select Team Members', - ), + 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, + ), + ); + }, + ); + }, ); - if (result == null) return; - - // Merge returned employees into controller.uploadingStates & controller.selectedEmployees - // 1) Reset all uploadingStates to false, then set true for selected - controller.uploadingStates.forEach((key, rx) { - rx.value = false; - }); - - for (final emp in result) { - final idStr = emp.id.toString(); - if (controller.uploadingStates.containsKey(idStr)) { - controller.uploadingStates[idStr]?.value = true; - } else { - // if uploadingStates doesn't have the id yet, add it (safe fallback) - controller.uploadingStates[idStr] = RxBool(true); - } - } - - // 2) Update selectedEmployees list in controller - controller.selectedEmployees.assignAll(result); - - // 3) Call controller helper (keeps existing behavior) - try { + if (result != null) { + controller.selectedEmployees.assignAll(result); controller.updateSelectedEmployees(); - } catch (_) { - // If controller does not implement updateSelectedEmployees, ignore. } } @@ -405,18 +407,20 @@ class _AssignTaskBottomSheetState extends State { if (selectedTeam.isEmpty) { showAppSnackbar( - title: "Team Required", - message: "Please select at least one team member", - type: SnackbarType.error); + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error, + ); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { showAppSnackbar( - title: "Invalid Input", - message: "Please enter a valid target number", - type: SnackbarType.error); + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error, + ); return; } @@ -432,9 +436,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { showAppSnackbar( - title: "Description Required", - message: "Please enter a description", - type: SnackbarType.error); + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error, + ); return; } diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index a1f4951..1ffd1e0 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -59,10 +59,12 @@ class _MultipleSelectRoleBottomSheetState List employees = controller.employees.toList(); if (widget.roleId != null && widget.roleId!.isNotEmpty) { - employees = - employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); + employees = employees + .where((emp) => emp.jobRoleID == widget.roleId) + .toList(); } + // Selected first employees.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -91,6 +93,7 @@ class _MultipleSelectRoleBottomSheetState ); } + // Selected on top _filtered.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -108,8 +111,8 @@ class _MultipleSelectRoleBottomSheetState _selected.add(emp); } } else { - // Single selection β†’ return immediately - Get.back(result: [emp]); + _selected.assignAll([emp]); + Get.back(result: _selected); } _onSearch(_searchController.text.trim()); @@ -148,17 +151,49 @@ class _MultipleSelectRoleBottomSheetState ), ); + /// ⭐ NEW β€” Chips showing selected employees + Widget _selectedChips() { + return Obx(() { + if (_selected.isEmpty) return const SizedBox(); + + return Wrap( + spacing: 8, + runSpacing: 8, + children: _selected.map((emp) { + return Chip( + label: Text(emp.name), + deleteIcon: const Icon(Icons.close), + onDeleted: () { + _selected.remove(emp); + _onSearch(_searchController.text.trim()); + }, + backgroundColor: Colors.blue.shade50, + ); + }).toList(), + ); + }); + } + @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Get.back(), - onSubmit: () => Get.back(result: _selected.toList()), // Return plain list + onSubmit: () => Get.back(result: _selected), child: SizedBox( height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ _searchBar(), + + /// ⭐ Chips shown right below search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _selectedChips(), + ), + + const SizedBox(height: 6), + Expanded( child: Obx(() { if (_isLoading.value) { @@ -191,15 +226,6 @@ class _MultipleSelectRoleBottomSheetState trailing: Checkbox( value: isSelected, onChanged: (_) => _onTap(emp), - fillColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; - } - return Colors.white; - }), - checkColor: Colors.white, - side: const BorderSide(color: Colors.grey), ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index 7fcd508..9c72a81 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -261,7 +261,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ? null : "Enter valid amount"), _gap(), - _buildPayeeAutocompleteField(), + _buildPayeeField(), _gap(), _buildDropdown( "Currency", @@ -420,67 +420,27 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SectionTitle( - icon: Icons.person_outline, title: "Payee", requiredField: true), - const SizedBox(height: 6), - Autocomplete( - optionsBuilder: (textEditingValue) { - final query = textEditingValue.text.toLowerCase(); - return query.isEmpty - ? const Iterable.empty() - : controller.payees - .where((p) => p.toLowerCase().contains(query)); - }, - displayStringForOption: (option) => option, - fieldViewBuilder: - (context, fieldController, focusNode, onFieldSubmitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (fieldController.text != controller.selectedPayee.value) { - fieldController.text = controller.selectedPayee.value; - fieldController.selection = TextSelection.fromPosition( - TextPosition(offset: fieldController.text.length)); - } - }); - - return TextFormField( - controller: fieldController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: "Type or select payee", - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), + const SectionTitle( + icon: Icons.person_outline, + title: "Payee", + requiredField: true, + ), + MySpacing.height(6), + GestureDetector( + onTap: _showPayeeSelector, + child: TileContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Obx(() => Text( + controller.selectedPayee.value?.name ?? "Select Payee", + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + )), ), - ), - validator: (v) => - v == null || v.trim().isEmpty ? "Please enter payee" : null, - onChanged: (val) => controller.selectedPayee.value = val, - ); - }, - onSelected: (selection) => controller.selectedPayee.value = selection, - optionsViewBuilder: (context, onSelected, options) => Material( - color: Colors.white, - elevation: 4, - borderRadius: BorderRadius.circular(8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (_, index) => InkWell( - onTap: () => onSelected(options.elementAt(index)), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 12), - child: Text(options.elementAt(index), - style: const TextStyle(fontSize: 14)), - ), - ), - ), + const Icon(Icons.arrow_drop_down, size: 22), + ], ), ), ), From e6eb0178c0f74c7e0356c52c4c6baf53675547aa Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 16:17:00 +0530 Subject: [PATCH 35/37] check box color change to white --- .../multiple_select_role_bottomsheet.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index 1ffd1e0..f7e5dff 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -59,9 +59,8 @@ class _MultipleSelectRoleBottomSheetState List employees = controller.employees.toList(); if (widget.roleId != null && widget.roleId!.isNotEmpty) { - employees = employees - .where((emp) => emp.jobRoleID == widget.roleId) - .toList(); + employees = + employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } // Selected first @@ -226,6 +225,16 @@ class _MultipleSelectRoleBottomSheetState trailing: Checkbox( value: isSelected, onChanged: (_) => _onTap(emp), + fillColor: + MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.selected)) { + return Colors.blueAccent; // Selected color + } + return Colors.white; // Unselected square color + }), + checkColor: Colors.white, // Check mark color + side: const BorderSide( + color: Colors.grey), // Outline for unselected ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From 490ecbc5671bedc670ba8e5f49083ee6aa6ebf2d Mon Sep 17 00:00:00 2001 From: Manish Date: Fri, 21 Nov 2025 17:36:56 +0530 Subject: [PATCH 36/37] UI enhancement --- .../assign_task_bottom_sheet .dart | 77 +++++++++---------- .../multiple_select_role_bottomsheet.dart | 49 ++---------- 2 files changed, 44 insertions(+), 82 deletions(-) diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index 2abbdf9..903dc2d 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -166,47 +166,45 @@ class _AssignTaskBottomSheetState extends State { GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(6), - ), - child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), - // Expanded name area - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText( - "Select team members", - color: Colors.grey.shade700, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], -) - - ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + )), ), MySpacing.height(8), @@ -381,7 +379,6 @@ class _AssignTaskBottomSheetState extends State { color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - child: MultipleSelectRoleBottomSheet( projectId: selectedProjectId!, organizationId: selectedOrganization?.id, @@ -397,8 +394,8 @@ class _AssignTaskBottomSheetState extends State { ); if (result != null) { - controller.selectedEmployees.assignAll(result); - controller.updateSelectedEmployees(); + controller.selectedEmployees + .assignAll(result); // RxList updates UI automatically } } diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index f7e5dff..a1f4951 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -63,7 +63,6 @@ class _MultipleSelectRoleBottomSheetState employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); } - // Selected first employees.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -92,7 +91,6 @@ class _MultipleSelectRoleBottomSheetState ); } - // Selected on top _filtered.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -110,8 +108,8 @@ class _MultipleSelectRoleBottomSheetState _selected.add(emp); } } else { - _selected.assignAll([emp]); - Get.back(result: _selected); + // Single selection β†’ return immediately + Get.back(result: [emp]); } _onSearch(_searchController.text.trim()); @@ -150,49 +148,17 @@ class _MultipleSelectRoleBottomSheetState ), ); - /// ⭐ NEW β€” Chips showing selected employees - Widget _selectedChips() { - return Obx(() { - if (_selected.isEmpty) return const SizedBox(); - - return Wrap( - spacing: 8, - runSpacing: 8, - children: _selected.map((emp) { - return Chip( - label: Text(emp.name), - deleteIcon: const Icon(Icons.close), - onDeleted: () { - _selected.remove(emp); - _onSearch(_searchController.text.trim()); - }, - backgroundColor: Colors.blue.shade50, - ); - }).toList(), - ); - }); - } - @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Get.back(), - onSubmit: () => Get.back(result: _selected), + onSubmit: () => Get.back(result: _selected.toList()), // Return plain list child: SizedBox( height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ _searchBar(), - - /// ⭐ Chips shown right below search bar - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: _selectedChips(), - ), - - const SizedBox(height: 6), - Expanded( child: Obx(() { if (_isLoading.value) { @@ -228,13 +194,12 @@ class _MultipleSelectRoleBottomSheetState fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; // Selected color + return Colors.blueAccent; } - return Colors.white; // Unselected square color + return Colors.white; }), - checkColor: Colors.white, // Check mark color - side: const BorderSide( - color: Colors.grey), // Outline for unselected + checkColor: Colors.white, + side: const BorderSide(color: Colors.grey), ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, From dc734e96d0df3811f99699a693e2564551fd3192 Mon Sep 17 00:00:00 2001 From: Manish Date: Mon, 24 Nov 2025 10:16:41 +0530 Subject: [PATCH 37/37] done tags UX updates --- .../add_service_project_job_controller.dart | 4 +- .../add_service_project_job_bottom_sheet.dart | 34 +++++++++++++-- lib/view/service_project/jobs_tab.dart | 4 +- .../service_project_job_detail_screen.dart | 43 ++++++++++++++++--- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/lib/controller/service_project/add_service_project_job_controller.dart b/lib/controller/service_project/add_service_project_job_controller.dart index 3d07097..eb84fee 100644 --- a/lib/controller/service_project/add_service_project_job_controller.dart +++ b/lib/controller/service_project/add_service_project_job_controller.dart @@ -71,11 +71,11 @@ class AddServiceProjectJobController extends GetxController { title: titleCtrl.text.trim(), description: descCtrl.text.trim(), projectId: projectId, - branchId: selectedBranch.value?.id, + branchId: selectedBranch.value?.id, assignees: assigneeIds.map((id) => {"id": id}).toList(), startDate: startDate.value!, dueDate: dueDate.value!, - tags: enteredTags.map((tag) => {"name": tag}).toList(), + tags: enteredTags.map((tag) => {"name": tag.trim()}).toList(), ); isLoading.value = false; diff --git a/lib/model/service_project/add_service_project_job_bottom_sheet.dart b/lib/model/service_project/add_service_project_job_bottom_sheet.dart index 3b4d133..e02bdd1 100644 --- a/lib/model/service_project/add_service_project_job_bottom_sheet.dart +++ b/lib/model/service_project/add_service_project_job_bottom_sheet.dart @@ -204,11 +204,37 @@ class _AddServiceProjectJobBottomSheetState height: 48, child: TextFormField( controller: controller.tagCtrl, + textInputAction: TextInputAction.done, + onEditingComplete: () { + final raw = controller.tagCtrl.text.trim(); + if (raw.isEmpty) return; + + final parts = raw + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty); + + for (final p in parts) { + if (!controller.enteredTags.contains(p)) { + controller.enteredTags.add(p); + } + } + controller.tagCtrl.clear(); + }, onFieldSubmitted: (v) { - final value = v.trim(); - if (value.isNotEmpty && - !controller.enteredTags.contains(value)) { - controller.enteredTags.add(value); + // also handle normal submit + final raw = v.trim(); + if (raw.isEmpty) return; + + final parts = raw + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty); + + for (final p in parts) { + if (!controller.enteredTags.contains(p)) { + controller.enteredTags.add(p); + } } controller.tagCtrl.clear(); }, diff --git a/lib/view/service_project/jobs_tab.dart b/lib/view/service_project/jobs_tab.dart index 096db50..ed2d084 100644 --- a/lib/view/service_project/jobs_tab.dart +++ b/lib/view/service_project/jobs_tab.dart @@ -201,7 +201,7 @@ class _JobsTabState extends State { children: job.tags!.map((tag) { return Chip( label: Text( - tag.name, + tag.name.replaceAll('_', ' '), style: const TextStyle(fontSize: 12), ), @@ -318,7 +318,7 @@ class _JobsTabState extends State { final success = await ApiService.editServiceProjectJobApi( - jobId: job.id , + jobId: job.id, operations: operations, ); diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart index 977c323..ac6375c 100644 --- a/lib/view/service_project/service_project_job_detail_screen.dart +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -39,6 +39,10 @@ class _JobDetailsScreenState extends State with UIMixin { final TextEditingController _dueDateController = TextEditingController(); final TextEditingController _tagTextController = TextEditingController(); + final RxList tags = [].obs; // For showing/editing tag chips + final TextEditingController tagController = + TextEditingController(); // For tag input + final RxList _selectedAssignees = [].obs; final RxList _selectedTags = [].obs; final RxBool isEditing = false.obs; @@ -47,21 +51,38 @@ class _JobDetailsScreenState extends State with UIMixin { @override void initState() { super.initState(); - controller = Get.put(ServiceProjectDetailsController()); - controller.fetchJobDetail(widget.jobId).then((_) { + controller = Get.find(); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + await controller.fetchJobDetail(widget.jobId); + final job = controller.jobDetail.value?.data; if (job != null) { _titleController.text = job.title ?? ''; _descriptionController.text = job.description ?? ''; + _startDateController.text = DateTimeUtils.convertUtcToLocal( job.startDate ?? DateTime.now().toIso8601String(), format: "yyyy-MM-dd"); + _dueDateController.text = DateTimeUtils.convertUtcToLocal( job.dueDate ?? '', format: "yyyy-MM-dd"); + _selectedAssignees.value = job.assignees ?? []; - _selectedTags.value = job.tags ?? []; + + // ---------- TAG FIX ---------- + final tagList = job.tags ?? []; + + final cleanedTags = tagList + .map((t) => (t.name ?? "").replaceAll("_", " ").trim()) + .toList(); + + tags.assignAll(cleanedTags); + tagController.clear(); } + + setState(() {}); }); } @@ -165,11 +186,21 @@ class _JobDetailsScreenState extends State with UIMixin { if (success) { showAppSnackbar( - title: "Success", - message: "Job updated successfully", - type: SnackbarType.success); + title: "Success", + message: "Job updated successfully", + type: SnackbarType.success, + ); + + /// Refresh detail screen await controller.fetchJobDetail(widget.jobId); + + /// πŸ”₯ Auto refresh job list UI (main Service Project Details screen) + if (Get.isRegistered()) { + await Get.find().refreshJobsAfterAdd(); + } + isEditing.value = false; + Navigator.pop(context); // optional if you want auto-close } else { showAppSnackbar( title: "Error",