Done all screen landscape responsive for mobile and tablet

This commit is contained in:
Manish 2025-11-18 16:25:25 +05:30
parent c94efac1de
commit 0b4f429f54
4 changed files with 449 additions and 406 deletions

View File

@ -15,6 +15,7 @@ class DirectoryFilterBottomSheet extends StatefulWidget {
class _DirectoryFilterBottomSheetState class _DirectoryFilterBottomSheetState
extends State<DirectoryFilterBottomSheet> { extends State<DirectoryFilterBottomSheet> {
final DirectoryController controller = Get.find<DirectoryController>(); final DirectoryController controller = Get.find<DirectoryController>();
final _categorySearchQuery = ''.obs; final _categorySearchQuery = ''.obs;
final _bucketSearchQuery = ''.obs; final _bucketSearchQuery = ''.obs;
@ -59,84 +60,100 @@ class _DirectoryFilterBottomSheetState
Get.back(); Get.back();
}, },
onCancel: Get.back, onCancel: Get.back,
child: SafeArea( child: LayoutBuilder(
child: Padding( builder: (context, constraints) {
padding: const EdgeInsets.symmetric(horizontal: 8), return SingleChildScrollView(
child: Column( padding: const EdgeInsets.only(bottom: 16, left: 4, right: 4),
mainAxisSize: MainAxisSize.min, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Obx(() { Obx(() {
final hasSelections = _tempSelectedCategories.isNotEmpty || final hasSelections = _tempSelectedCategories.isNotEmpty ||
_tempSelectedBuckets.isNotEmpty; _tempSelectedBuckets.isNotEmpty;
if (!hasSelections) return const SizedBox.shrink();
return Column( if (!hasSelections) return const SizedBox.shrink();
crossAxisAlignment: CrossAxisAlignment.start,
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: [ children: [
MyText("Selected Filters:", fontWeight: 600), TextButton.icon(
const SizedBox(height: 4), onPressed: _resetFilters,
_buildChips(_tempSelectedCategories, icon: const Icon(Icons.restart_alt, size: 18),
controller.contactCategories, _toggleCategory), label: MyText("Reset All", color: Colors.red),
_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,
), ),
), ],
], ),
),
if (controller.contactCategories.isNotEmpty) // CATEGORIES
Obx(() => _buildExpandableFilterSection( if (controller.contactCategories.isNotEmpty)
title: "Categories", Obx(() => _buildExpandableFilterSection(
expanded: _categoryExpanded, title: "Categories",
searchQuery: _categorySearchQuery, expanded: _categoryExpanded,
allItems: controller.contactCategories, searchQuery: _categorySearchQuery,
selectedItems: _tempSelectedCategories, allItems: controller.contactCategories,
onToggle: _toggleCategory, selectedItems: _tempSelectedCategories,
)), onToggle: _toggleCategory,
if (controller.contactBuckets.isNotEmpty) )),
Obx(() => _buildExpandableFilterSection(
title: "Buckets", // BUCKETS
expanded: _bucketExpanded, if (controller.contactBuckets.isNotEmpty)
searchQuery: _bucketSearchQuery, Obx(() => _buildExpandableFilterSection(
allItems: controller.contactBuckets, title: "Buckets",
selectedItems: _tempSelectedBuckets, expanded: _bucketExpanded,
onToggle: _toggleBucket, searchQuery: _bucketSearchQuery,
)), allItems: controller.contactBuckets,
], selectedItems: _tempSelectedBuckets,
), onToggle: _toggleBucket,
), )),
const SizedBox(height: 20),
],
),
);
},
), ),
); );
} }
// ------------------------------
// CHIP UI FOR SELECTED FILTERS
// ------------------------------
Widget _buildChips(RxList<String> selectedIds, List<dynamic> allItems, Widget _buildChips(RxList<String> selectedIds, List<dynamic> allItems,
Function(String) onRemoved) { Function(String) onRemoved) {
final idToName = {for (var item in allItems) item.id: item.name}; final idToName = {for (var item in allItems) item.id: item.name};
return Wrap( return Wrap(
spacing: 4, spacing: 4,
runSpacing: 4, runSpacing: 4,
children: selectedIds children: selectedIds.map((id) {
.map((id) => Chip( return Chip(
label: MyText(idToName[id] ?? "", color: Colors.black87), label: MyText(idToName[id] ?? "", color: Colors.black87),
deleteIcon: const Icon(Icons.close, size: 16), deleteIcon: const Icon(Icons.close, size: 16),
onDeleted: () => onRemoved(id), onDeleted: () => onRemoved(id),
backgroundColor: Colors.blue.shade50, backgroundColor: Colors.blue.shade50,
)) );
.toList(), }).toList(),
); );
} }
// ------------------------------
// EXPANDABLE FILTER UI
// ------------------------------
Widget _buildExpandableFilterSection({ Widget _buildExpandableFilterSection({
required String title, required String title,
required RxBool expanded, required RxBool expanded,
@ -146,7 +163,7 @@ class _DirectoryFilterBottomSheetState
required Function(String) onToggle, required Function(String) onToggle,
}) { }) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 8),
child: Column( child: Column(
children: [ children: [
GestureDetector( GestureDetector(
@ -159,28 +176,27 @@ class _DirectoryFilterBottomSheetState
: Icons.keyboard_arrow_right, : Icons.keyboard_arrow_right,
size: 20, size: 20,
), ),
const SizedBox(width: 4), const SizedBox(width: 6),
MyText( MyText(title, fontWeight: 600, fontSize: 16),
"$title",
fontWeight: 600,
fontSize: 16,
),
], ],
), ),
), ),
if (expanded.value) if (expanded.value)
_buildFilterSection( _buildFilterSection(
title: title,
searchQuery: searchQuery, searchQuery: searchQuery,
allItems: allItems, allItems: allItems,
selectedItems: selectedItems, selectedItems: selectedItems,
onToggle: onToggle, onToggle: onToggle,
title: title,
), ),
], ],
), ),
); );
} }
// ------------------------------
// FILTER LIST + SEARCH
// ------------------------------
Widget _buildFilterSection({ Widget _buildFilterSection({
required String title, required String title,
required RxString searchQuery, required RxString searchQuery,
@ -189,14 +205,16 @@ class _DirectoryFilterBottomSheetState
required Function(String) onToggle, required Function(String) onToggle,
}) { }) {
final filteredList = allItems.where((item) { final filteredList = allItems.where((item) {
if (searchQuery.isEmpty) return true; if (searchQuery.value.isEmpty) return true;
return item.name.toLowerCase().contains(searchQuery.value.toLowerCase()); return item.name.toLowerCase().contains(searchQuery.value.toLowerCase());
}).toList(); }).toList();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 6), const SizedBox(height: 8),
// SEARCH BOX
TextField( TextField(
onChanged: (value) => searchQuery.value = value, onChanged: (value) => searchQuery.value = value,
style: const TextStyle(fontSize: 13), style: const TextStyle(fontSize: 13),
@ -215,7 +233,10 @@ class _DirectoryFilterBottomSheetState
fillColor: Colors.grey.shade100, fillColor: Colors.grey.shade100,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// NO RESULTS
if (filteredList.isEmpty) if (filteredList.isEmpty)
Row( Row(
children: [ children: [
@ -227,7 +248,7 @@ class _DirectoryFilterBottomSheetState
) )
else else
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 230), constraints: const BoxConstraints(maxHeight: 260),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
shrinkWrap: true, shrinkWrap: true,
@ -238,7 +259,7 @@ class _DirectoryFilterBottomSheetState
return Obx(() { return Obx(() {
final isSelected = selectedItems.contains(item.id); final isSelected = selectedItems.contains(item.id);
return GestureDetector( return InkWell(
onTap: () => onToggle(item.id), onTap: () => onToggle(item.id),
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -271,7 +292,7 @@ class _DirectoryFilterBottomSheetState
}); });
}, },
), ),
) ),
], ],
); );
} }

View File

@ -48,116 +48,133 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
onCancel: () => Navigator.pop(context), onCancel: () => Navigator.pop(context),
onSubmit: _handleAssign, onSubmit: _handleAssign,
submitText: "Assign", submitText: "Assign",
child: Obx(() {
if (assignController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projects = assignController.allProjects; /// 🔥 MAKE BODY SCROLLABLE (fix for landscape)
if (projects.isEmpty) { child: LayoutBuilder(
return const Center(child: Text('No projects available.')); builder: (context, constraints) {
} return Obx(() {
if (assignController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return Column( final projects = assignController.allProjects;
crossAxisAlignment: CrossAxisAlignment.start, if (projects.isEmpty) {
children: [ return const Center(child: Text('No projects available.'));
MyText.bodySmall( }
'Select the projects to assign this employee.',
color: Colors.grey[600],
),
MySpacing.height(8),
// Select All return ConstrainedBox(
Row( constraints: BoxConstraints(
mainAxisAlignment: MainAxisAlignment.spaceBetween, /// 🔥 Always allow enough height for scroll
children: [ maxHeight: constraints.maxHeight,
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<Color>(
(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,
),
),
);
});
},
), ),
), 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<Color>(
(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,
),
),
);
});
},
),
],
),
),
);
});
},
),
); );
}, },
); );

View File

@ -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/controller/tenant/tenant_switch_controller.dart';
import 'package:marco/helpers/theme/theme_editor_widget.dart'; import 'package:marco/helpers/theme/theme_editor_widget.dart';
class UserProfileBar extends StatefulWidget { class UserProfileBar extends StatefulWidget {
final bool isCondensed; final bool isCondensed;
const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key); const UserProfileBar({Key? key, this.isCondensed = false}) : super(key: key);
@ -46,6 +45,7 @@ class _UserProfileBarState extends State<UserProfileBar>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isCondensed = widget.isCondensed; final bool isCondensed = widget.isCondensed;
return Padding( return Padding(
padding: const EdgeInsets.only(left: 14), padding: const EdgeInsets.only(left: 14),
child: ClipRRect( child: ClipRRect(
@ -79,55 +79,74 @@ class _UserProfileBarState extends State<UserProfileBar>
), ),
), ),
child: SafeArea( child: SafeArea(
bottom: true, bottom: true,
child: Stack( child: Stack(
children: [ children: [
Offstage( // ======================= MAIN PROFILE SIDEBAR =======================
offstage: _isThemeEditorVisible, Offstage(
child: Column( offstage: _isThemeEditorVisible,
crossAxisAlignment: CrossAxisAlignment.stretch, child: LayoutBuilder(
children: [ builder: (context, constraints) {
_isLoading return SingleChildScrollView(
? const _LoadingSection() physics: const ClampingScrollPhysics(),
: _userProfileSection(isCondensed), child: ConstrainedBox(
if (!_isLoading && !isCondensed) _switchTenantRow(), constraints: BoxConstraints(
MySpacing.height(12), minHeight: constraints.maxHeight),
Divider( child: IntrinsicHeight(
indent: 18, child: Column(
endIndent: 18, crossAxisAlignment: CrossAxisAlignment.stretch,
thickness: 0.7, children: [
color: Colors.grey.withOpacity(0.25), _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( // ======================= THEME EDITOR VIEW =======================
onClose: () { Offstage(
setState(() => _isThemeEditorVisible = false); offstage: !_isThemeEditorVisible,
}, child: ThemeEditorWidget(
), onClose: () {
setState(() => _isThemeEditorVisible = false);
},
), ),
], ),
)), ],
),
),
), ),
), ),
), ),
); );
} }
// ==================== CONTINUE EXISTING CODE ===================== // ==================== EXISTING CODE (UNCHANGED) =====================
Widget _switchTenantRow() { Widget _switchTenantRow() {
final TenantSwitchController tenantSwitchController = final TenantSwitchController tenantSwitchController =
Get.put(TenantSwitchController()); Get.put(TenantSwitchController());
@ -231,17 +250,25 @@ class _UserProfileBarState extends State<UserProfileBar>
child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), child: const Center(child: CircularProgressIndicator(strokeWidth: 2)),
); );
// FIXED YOUR ORIGINAL INTENT, COMPLETED PROPERLY
Widget _noTenantContainer() => Container( Widget _noTenantContainer() => Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade50, color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12), 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( child: const Center(
"No tenants available", child: Text(
color: Colors.blueAccent, "No organizations available",
fontWeight: 600, style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
), ),
); );
@ -443,24 +470,20 @@ class _UserProfileBarState extends State<UserProfileBar>
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Top icon
Icon(LucideIcons.log_out, size: 56, color: primaryColor), Icon(LucideIcons.log_out, size: 56, color: primaryColor),
MySpacing.height(18), MySpacing.height(18),
// Title
MyText.titleLarge( MyText.titleLarge(
"Logout Confirmation", "Logout Confirmation",
fontWeight: 700, fontWeight: 700,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
MySpacing.height(14), MySpacing.height(14),
// Subtitle
MyText.bodyMedium( MyText.bodyMedium(
"Are you sure you want to logout?\nYou will need to login again to continue.", "Are you sure you want to logout?\nYou will need to login again to continue.",
color: Colors.grey[700], color: Colors.grey[700],
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
MySpacing.height(30), MySpacing.height(30),
// Buttons
Row( Row(
children: [ children: [
Expanded( Expanded(

View File

@ -22,11 +22,11 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
final ServiceProjectController controller = final ServiceProjectController controller =
Get.put(ServiceProjectController()); Get.put(ServiceProjectController());
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Fetch projects safely after first frame
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
controller.fetchProjects(); controller.fetchProjects();
}); });
@ -42,35 +42,29 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
Widget _buildProjectCard(ProjectItem project) { Widget _buildProjectCard(ProjectItem project) {
return Card( return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
shadowColor: Colors.indigo.withOpacity(0.10), shadowColor: Colors.indigo.withOpacity(0.10),
color: Colors.white, color: Colors.white,
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(5),
onTap: () { onTap: () {
// Navigate to ServiceProjectDetailsScreen
Get.to(() => ServiceProjectDetailsScreen(projectId: project.id)); Get.to(() => ServiceProjectDetailsScreen(projectId: project.id));
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
/// Project Header
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: Column( child: MyText.titleMedium(
crossAxisAlignment: CrossAxisAlignment.start, project.name,
children: [ fontWeight: 700,
MyText.titleMedium( maxLines: 2,
project.name, overflow: TextOverflow.ellipsis,
fontWeight: 700,
),
MySpacing.height(4),
],
), ),
), ),
if (project.status?.status.isNotEmpty ?? false) if (project.status?.status.isNotEmpty ?? false)
@ -89,47 +83,32 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
), ),
], ],
), ),
MySpacing.height(8),
MySpacing.height(10),
/// Assigned Date
_buildDetailRow( _buildDetailRow(
Icons.date_range_outlined, Icons.date_range_outlined,
Colors.teal, Colors.teal,
"Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}", "Assigned: ${DateTimeUtils.convertUtcToLocal(project.assignedDate.toIso8601String(), format: DateTimeUtils.defaultFormat)}",
fontSize: 13,
), ),
MySpacing.height(6),
MySpacing.height(8),
/// Client Info
if (project.client != null) if (project.client != null)
_buildDetailRow( _buildDetailRow(
Icons.account_circle_outlined, Icons.account_circle_outlined,
Colors.indigo, Colors.indigo,
"Client: ${project.client!.name} (${project.client!.contactPerson})", "Client: ${project.client!.name} (${project.client!.contactPerson})",
fontSize: 13,
), ),
MySpacing.height(6),
MySpacing.height(8),
/// Contact Info
_buildDetailRow( _buildDetailRow(
Icons.phone, Icons.phone,
Colors.green, Colors.green,
"Contact: ${project.contactName} (${project.contactPhone})", "Contact: ${project.contactName} (${project.contactPhone})",
fontSize: 13,
), ),
MySpacing.height(10),
MySpacing.height(12),
/// Services List
if (project.services.isNotEmpty) if (project.services.isNotEmpty)
Wrap( Wrap(
spacing: 6, spacing: 6,
runSpacing: 4, runSpacing: 4,
children: project.services children: project.services
.map((service) => _buildServiceChip(service.name)) .map((e) => _buildServiceChip(e.name))
.toList(), .toList(),
), ),
], ],
@ -145,7 +124,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
color: Colors.orange.withOpacity(0.1), color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: MyText.labelSmall( child: MyText.labelSmall(
name, name,
color: Colors.orange[800], color: Colors.orange[800],
@ -154,19 +133,18 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
); );
} }
Widget _buildDetailRow(IconData icon, Color iconColor, String value, Widget _buildDetailRow(IconData icon, Color color, String value) {
{double fontSize = 12}) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Icon(icon, size: 18, color: iconColor), Icon(icon, size: 18, color: color),
MySpacing.width(8), MySpacing.width(8),
Flexible( Expanded(
child: MyText.bodySmall( child: MyText.bodySmall(
value, value,
color: Colors.grey[900], maxLines: 2,
overflow: TextOverflow.ellipsis,
fontWeight: 500, fontWeight: 500,
fontSize: fontSize, color: Colors.grey[900],
), ),
), ),
], ],
@ -181,7 +159,7 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
const Icon(Icons.work_outline, size: 60, color: Colors.grey), const Icon(Icons.work_outline, size: 60, color: Colors.grey),
MySpacing.height(18), MySpacing.height(18),
MyText.titleMedium('No matching projects found.', MyText.titleMedium('No matching projects found.',
fontWeight: 600, color: Colors.grey), color: Colors.grey, fontWeight: 600),
MySpacing.height(10), MySpacing.height(10),
MyText.bodySmall('Try adjusting your filters or refresh.', MyText.bodySmall('Try adjusting your filters or refresh.',
color: Colors.grey), color: Colors.grey),
@ -192,146 +170,150 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return SafeArea(
backgroundColor: const Color(0xFFF5F5F5), child: Scaffold(
appBar: CustomAppBar( backgroundColor: const Color(0xFFF5F5F5),
title: "Service Projects", appBar: CustomAppBar(
onBackPressed: () => Get.toNamed('/dashboard'), title: "Service Projects",
), onBackPressed: () => Get.toNamed('/dashboard'),
body: Column( ),
children: [ body: LayoutBuilder(
/// Search bar and actions builder: (context, constraints) {
Padding( return Column(
padding: MySpacing.xy(8, 8),
child: Row(
children: [ children: [
Expanded( /// SEARCH BAR AREA
child: SizedBox( Padding(
height: 35, padding: const EdgeInsets.all(8),
child: TextField( child: Row(
controller: searchController, children: [
decoration: InputDecoration( Expanded(
contentPadding: child: SizedBox(
const EdgeInsets.symmetric(horizontal: 12), height: 38,
prefixIcon: const Icon(Icons.search, child: TextField(
size: 20, color: Colors.grey), controller: searchController,
suffixIcon: ValueListenableBuilder<TextEditingValue>( decoration: InputDecoration(
valueListenable: searchController, contentPadding:
builder: (context, value, _) { const EdgeInsets.symmetric(horizontal: 12),
if (value.text.isEmpty) { prefixIcon: const Icon(Icons.search,
return const SizedBox.shrink();
}
return IconButton(
icon: const Icon(Icons.clear,
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
onPressed: () { suffixIcon:
searchController.clear(); ValueListenableBuilder<TextEditingValue>(
controller.updateSearch(''); valueListenable: searchController,
}, builder: (context, value, _) {
); return value.text.isNotEmpty
}, ? IconButton(
), icon: const Icon(Icons.clear,
hintText: 'Search projects...', size: 20, color: Colors.grey),
filled: true, onPressed: () {
fillColor: Colors.white, searchController.clear();
border: OutlineInputBorder( controller.updateSearch('');
borderRadius: BorderRadius.circular(5), },
borderSide: BorderSide(color: Colors.grey.shade300), )
), : const SizedBox.shrink();
enabledBorder: OutlineInputBorder( },
borderRadius: BorderRadius.circular(5), ),
borderSide: BorderSide(color: Colors.grey.shade300), hintText: 'Search projects...',
), fillColor: Colors.white,
), filled: true,
), border: OutlineInputBorder(
), borderRadius: BorderRadius.circular(5),
), borderSide:
MySpacing.width(8), BorderSide(color: Colors.grey.shade300),
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<int>(
padding: EdgeInsets.zero,
icon: const Icon(Icons.more_vert,
size: 20, color: Colors.black87),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)),
itemBuilder: (context) => [
const PopupMenuItem<int>(
enabled: false,
height: 30,
child: Text(
"Actions",
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.grey),
),
),
const PopupMenuItem<int>(
value: 1,
child: Row(
children: [
SizedBox(width: 10),
Expanded(child: Text("Manage Projects")),
Icon(Icons.chevron_right,
size: 20, color: Colors.indigo),
],
), ),
), ),
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<int>(
padding: EdgeInsets.zero,
icon: const Icon(Icons.more_vert, size: 20, color: Colors.black87),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
itemBuilder: (context) => [
const PopupMenuItem<int>(
enabled: false,
height: 30,
child: Text("Actions",
style:
TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
),
const PopupMenuItem<int>(
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]),
),
);
}),
),
], ],
), ),
); );