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,17 +60,19 @@ 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(
padding: const EdgeInsets.only(bottom: 16, left: 4, right: 4),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
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(); if (!hasSelections) return const SizedBox.shrink();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -77,11 +80,14 @@ class _DirectoryFilterBottomSheetState
const SizedBox(height: 4), const SizedBox(height: 4),
_buildChips(_tempSelectedCategories, _buildChips(_tempSelectedCategories,
controller.contactCategories, _toggleCategory), controller.contactCategories, _toggleCategory),
_buildChips(_tempSelectedBuckets, controller.contactBuckets, _buildChips(_tempSelectedBuckets,
_toggleBucket), controller.contactBuckets, _toggleBucket),
const SizedBox(height: 10),
], ],
); );
}), }),
// RESET BUTTON
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
@ -89,12 +95,11 @@ class _DirectoryFilterBottomSheetState
onPressed: _resetFilters, onPressed: _resetFilters,
icon: const Icon(Icons.restart_alt, size: 18), icon: const Icon(Icons.restart_alt, size: 18),
label: MyText("Reset All", color: Colors.red), label: MyText("Reset All", color: Colors.red),
style: TextButton.styleFrom(
foregroundColor: Colors.red.shade400,
),
), ),
], ],
), ),
// CATEGORIES
if (controller.contactCategories.isNotEmpty) if (controller.contactCategories.isNotEmpty)
Obx(() => _buildExpandableFilterSection( Obx(() => _buildExpandableFilterSection(
title: "Categories", title: "Categories",
@ -104,6 +109,8 @@ class _DirectoryFilterBottomSheetState
selectedItems: _tempSelectedCategories, selectedItems: _tempSelectedCategories,
onToggle: _toggleCategory, onToggle: _toggleCategory,
)), )),
// BUCKETS
if (controller.contactBuckets.isNotEmpty) if (controller.contactBuckets.isNotEmpty)
Obx(() => _buildExpandableFilterSection( Obx(() => _buildExpandableFilterSection(
title: "Buckets", title: "Buckets",
@ -113,30 +120,40 @@ class _DirectoryFilterBottomSheetState
selectedItems: _tempSelectedBuckets, selectedItems: _tempSelectedBuckets,
onToggle: _toggleBucket, 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,7 +48,11 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
onCancel: () => Navigator.pop(context), onCancel: () => Navigator.pop(context),
onSubmit: _handleAssign, onSubmit: _handleAssign,
submitText: "Assign", submitText: "Assign",
child: Obx(() {
/// 🔥 MAKE BODY SCROLLABLE (fix for landscape)
child: LayoutBuilder(
builder: (context, constraints) {
return Obx(() {
if (assignController.isLoading.value) { if (assignController.isLoading.value) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@ -58,7 +62,15 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
return const Center(child: Text('No projects available.')); return const Center(child: Text('No projects available.'));
} }
return Column( 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, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MyText.bodySmall( MyText.bodySmall(
@ -67,7 +79,7 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
), ),
MySpacing.height(8), MySpacing.height(8),
// Select All // Header Row
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -97,11 +109,10 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
], ],
), ),
// List of Projects /// 🔥 List auto grows and scrolls no fixed height
SizedBox( ListView.builder(
height: 300, shrinkWrap: true,
child: ListView.builder( physics: const NeverScrollableScrollPhysics(),
controller: _scrollController,
itemCount: projects.length, itemCount: projects.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final GlobalProjectModel project = projects[index]; final GlobalProjectModel project = projects[index];
@ -113,8 +124,10 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
return Theme( return Theme(
data: Theme.of(context).copyWith( data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData( checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith<Color>( fillColor:
(states) => states.contains(WidgetState.selected) WidgetStateProperty.resolveWith<Color>(
(states) =>
states.contains(WidgetState.selected)
? Colors.blueAccent ? Colors.blueAccent
: Colors.white, : Colors.white,
), ),
@ -143,7 +156,8 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
); );
}, },
activeColor: Colors.blueAccent, activeColor: Colors.blueAccent,
controlAffinity: ListTileControlAffinity.leading, controlAffinity:
ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
visualDensity: const VisualDensity( visualDensity: const VisualDensity(
horizontal: -4, horizontal: -4,
@ -154,10 +168,13 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
}); });
}, },
), ),
),
], ],
),
),
); );
}), });
},
),
); );
}, },
); );

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(
@ -82,15 +82,25 @@ 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: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_isLoading _isLoading
? const _LoadingSection() ? const _LoadingSection()
: _userProfileSection(isCondensed), : _userProfileSection(isCondensed),
if (!_isLoading && !isCondensed) _switchTenantRow(), if (!_isLoading && !isCondensed)
_switchTenantRow(),
MySpacing.height(12), MySpacing.height(12),
Divider( Divider(
indent: 18, indent: 18,
@ -111,6 +121,13 @@ class _UserProfileBarState extends State<UserProfileBar>
], ],
), ),
), ),
),
);
},
),
),
// ======================= THEME EDITOR VIEW =======================
Offstage( Offstage(
offstage: !_isThemeEditorVisible, offstage: !_isThemeEditorVisible,
child: ThemeEditorWidget( child: ThemeEditorWidget(
@ -120,14 +137,16 @@ class _UserProfileBarState extends State<UserProfileBar>
), ),
), ),
], ],
)), ),
),
), ),
), ),
), ),
); );
} }
// ==================== 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: const Center(
child: Text(
"No organizations available",
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.black87,
),
), ),
child: MyText.bodyMedium(
"No tenants available",
color: Colors.blueAccent,
fontWeight: 600,
), ),
); );
@ -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,
children: [
MyText.titleMedium(
project.name, project.name,
fontWeight: 700, fontWeight: 700,
), maxLines: 2,
MySpacing.height(4), overflow: TextOverflow.ellipsis,
],
), ),
), ),
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,22 +170,25 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return SafeArea(
child: Scaffold(
backgroundColor: const Color(0xFFF5F5F5), backgroundColor: const Color(0xFFF5F5F5),
appBar: CustomAppBar( appBar: CustomAppBar(
title: "Service Projects", title: "Service Projects",
onBackPressed: () => Get.toNamed('/dashboard'), onBackPressed: () => Get.toNamed('/dashboard'),
), ),
body: Column( body: LayoutBuilder(
builder: (context, constraints) {
return Column(
children: [ children: [
/// Search bar and actions /// SEARCH BAR AREA
Padding( Padding(
padding: MySpacing.xy(8, 8), padding: const EdgeInsets.all(8),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: SizedBox( child: SizedBox(
height: 35, height: 38,
child: TextField( child: TextField(
controller: searchController, controller: searchController,
decoration: InputDecoration( decoration: InputDecoration(
@ -215,98 +196,48 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
const EdgeInsets.symmetric(horizontal: 12), const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search, prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
suffixIcon: ValueListenableBuilder<TextEditingValue>( suffixIcon:
ValueListenableBuilder<TextEditingValue>(
valueListenable: searchController, valueListenable: searchController,
builder: (context, value, _) { builder: (context, value, _) {
if (value.text.isEmpty) { return value.text.isNotEmpty
return const SizedBox.shrink(); ? IconButton(
}
return IconButton(
icon: const Icon(Icons.clear, icon: const Icon(Icons.clear,
size: 20, color: Colors.grey), size: 20, color: Colors.grey),
onPressed: () { onPressed: () {
searchController.clear(); searchController.clear();
controller.updateSearch(''); controller.updateSearch('');
}, },
); )
: const SizedBox.shrink();
}, },
), ),
hintText: 'Search projects...', hintText: 'Search projects...',
filled: true,
fillColor: Colors.white, fillColor: Colors.white,
filled: true,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300), borderSide:
), BorderSide(color: Colors.grey.shade300),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: BorderSide(color: Colors.grey.shade300),
), ),
), ),
), ),
), ),
), ),
MySpacing.width(8), MySpacing.width(8),
Container(
height: 35, /// FILTER BUTTON
width: 35, _roundIconButton(Icons.tune),
decoration: BoxDecoration(
color: Colors.white, MySpacing.width(8),
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(5), /// ACTION MENU
), _roundMenuButton(),
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),
],
),
),
],
),
),
], ],
), ),
), ),
/// Project List /// LIST AREA
Expanded( Expanded(
child: Obx(() { child: Obx(() {
if (controller.isLoading.value) { if (controller.isLoading.value) {
@ -314,16 +245,16 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
} }
final projects = controller.filteredProjects; final projects = controller.filteredProjects;
return MyRefreshIndicator( return MyRefreshIndicator(
onRefresh: _refreshProjects, onRefresh: _refreshProjects,
backgroundColor: Colors.indigo,
color: Colors.white, color: Colors.white,
backgroundColor: Colors.indigo,
child: projects.isEmpty child: projects.isEmpty
? _buildEmptyState() ? _buildEmptyState()
: ListView.separated( : ListView.separated(
physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only(
padding: MySpacing.only( left: 8, right: 8, top: 4, bottom: 20),
left: 8, right: 8, top: 4, bottom: 80),
itemCount: projects.length, itemCount: projects.length,
separatorBuilder: (_, __) => MySpacing.height(12), separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) => itemBuilder: (_, index) =>
@ -333,6 +264,57 @@ class _ServiceProjectScreenState extends State<ServiceProjectScreen>
}), }),
), ),
], ],
);
},
),
),
);
}
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),
],
),
),
],
), ),
); );
} }