Done all screen landscape responsive for mobile and tablet
This commit is contained in:
parent
bf03023db7
commit
70b144ffce
@ -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
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user