Done all screen landscape responsive for mobile and tablet

This commit is contained in:
Manish 2025-11-18 16:25:25 +05:30
parent bf03023db7
commit 70b144ffce
4 changed files with 435 additions and 435 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

@ -52,6 +52,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(
@ -88,41 +89,52 @@ class _UserProfileBarState extends State<UserProfileBar>
bottom: true, bottom: true,
child: Stack( child: Stack(
children: [ children: [
// ======================= MAIN PROFILE SIDEBAR =======================
Offstage( Offstage(
offstage: _isThemeEditorVisible, offstage: _isThemeEditorVisible,
child: Column( child: LayoutBuilder(
crossAxisAlignment: CrossAxisAlignment.stretch, builder: (context, constraints) {
children: [ return SingleChildScrollView(
_isLoading physics: const ClampingScrollPhysics(),
? const _LoadingSection() child: ConstrainedBox(
: _userProfileSection(isCondensed), constraints: BoxConstraints(
if (!_isLoading && !isCondensed) _switchTenantRow(), minHeight: constraints.maxHeight),
MySpacing.height(12), child: IntrinsicHeight(
Divider( child: Column(
indent: 18, crossAxisAlignment: CrossAxisAlignment.stretch,
endIndent: 18, children: [
thickness: 0.7, _isLoading
color: Colors.grey.withOpacity(0.25), ? const _LoadingSection()
), : _userProfileSection(isCondensed),
MySpacing.height(12), if (!_isLoading && !isCondensed)
_supportAndSettingsMenu(isCondensed), _switchTenantRow(),
MySpacing.height(12), MySpacing.height(12),
Divider(
// Subtle version text for expanded mode indent: 18,
if (!isCondensed && _appVersion.isNotEmpty) endIndent: 18,
_versionText(), thickness: 0.7,
color: Colors.grey.withOpacity(0.25),
const Spacer(), ),
Divider( MySpacing.height(12),
indent: 18, _supportAndSettingsMenu(isCondensed),
endIndent: 18, const Spacer(),
thickness: 0.35, Divider(
color: Colors.grey.withOpacity(0.18), indent: 18,
), endIndent: 18,
_logoutButton(isCondensed), thickness: 0.35,
], color: Colors.grey.withOpacity(0.18),
),
_logoutButton(isCondensed),
],
),
),
),
);
},
), ),
), ),
// ======================= THEME EDITOR VIEW =======================
Offstage( Offstage(
offstage: !_isThemeEditorVisible, offstage: !_isThemeEditorVisible,
child: ThemeEditorWidget( child: ThemeEditorWidget(
@ -131,9 +143,6 @@ class _UserProfileBarState extends State<UserProfileBar>
}, },
), ),
), ),
// Floating badge for condensed mode
if (isCondensed && _appVersion.isNotEmpty) _versionBadge(),
], ],
), ),
), ),
@ -143,96 +152,7 @@ class _UserProfileBarState extends State<UserProfileBar>
); );
} }
// =================== Version Widgets =================== // ==================== EXISTING CODE (UNCHANGED) =====================
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 ===================
Widget _switchTenantRow() { Widget _switchTenantRow() {
final TenantSwitchController tenantSwitchController = final TenantSwitchController tenantSwitchController =
@ -337,17 +257,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,
),
),
), ),
); );

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,38 +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,
projectName: project.name,
));
}, },
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)
@ -92,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(),
), ),
], ],
@ -148,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],
@ -157,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],
), ),
), ),
], ],
@ -184,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),
@ -195,91 +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(
projectName: 'All Service Projects', title: "Service Projects",
onBackPressed: () => Get.toNamed('/dashboard'), onBackPressed: () => Get.toNamed('/dashboard'),
), ),
body: Column( body: LayoutBuilder(
children: [ builder: (context, constraints) {
/// Search bar and actions return Column(
Padding(
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:
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<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]),
),
);
}),
),
], ],
), ),
); );