marco.pms.mobileapp/lib/view/directory/directory_view.dart

511 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:marco/controller/directory/directory_controller.dart';
import 'package:marco/controller/directory/create_bucket_controller.dart';
import 'package:marco/helpers/utils/launcher_utils.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/model/directory/add_contact_bottom_sheet.dart';
import 'package:marco/model/directory/directory_filter_bottom_sheet.dart';
import 'package:marco/model/directory/create_bucket_bottom_sheet.dart';
import 'package:marco/view/directory/contact_detail_screen.dart';
import 'package:marco/view/directory/manage_bucket_screen.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
class DirectoryView extends StatefulWidget {
@override
State<DirectoryView> createState() => _DirectoryViewState();
}
class _DirectoryViewState extends State<DirectoryView> {
final DirectoryController controller = Get.find();
final TextEditingController searchController = TextEditingController();
bool isFabExpanded = false;
final PermissionController permissionController =
Get.put(PermissionController());
Future<void> _refreshDirectory() async {
try {
await controller.fetchContacts();
} catch (e, stackTrace) {
debugPrint('Error refreshing directory data: ${e.toString()}');
debugPrintStack(stackTrace: stackTrace);
}
}
void _handleCreateContact() async {
await controller.fetchBuckets();
if (controller.contactBuckets.isEmpty) {
final shouldCreate = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => _buildEmptyBucketPrompt(),
);
if (shouldCreate != true) return;
final created = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const CreateBucketBottomSheet(),
);
if (created == true) {
await controller.fetchBuckets();
} else {
return;
}
}
Get.delete<BucketController>();
final result = await Get.bottomSheet(
AddContactBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.transparent,
);
if (result == true) {
controller.fetchContacts();
}
}
void _handleManageBuckets() async {
await controller.fetchBuckets();
Get.to(
() => ManageBucketsScreen(permissionController: permissionController));
}
Widget _buildEmptyBucketPrompt() {
return SafeArea(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.info_outline, size: 48, color: Colors.indigo),
MySpacing.height(12),
MyText.titleMedium("No Buckets Assigned",
fontWeight: 700, textAlign: TextAlign.center),
MySpacing.height(8),
MyText.bodyMedium(
"You dont have any buckets assigned. Please create a bucket before adding a contact.",
textAlign: TextAlign.center,
color: Colors.grey[700],
),
MySpacing.height(20),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context, false),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[300],
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text("Cancel"),
),
),
MySpacing.width(12),
Expanded(
child: ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text("Create Bucket"),
),
),
],
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isFabExpanded) ...[
Obx(() {
if (permissionController
.hasPermission(Permissions.directoryAdmin) ||
permissionController
.hasPermission(Permissions.directoryManager)) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FloatingActionButton.extended(
heroTag: 'manageBuckets',
backgroundColor: Colors.grey[850],
icon: const Icon(Icons.folder_open_outlined,
color: Colors.white),
label: const Text("Manage Buckets",
style: TextStyle(color: Colors.white)),
onPressed: () {
setState(() => isFabExpanded = false);
_handleManageBuckets();
},
),
);
} else {
return const SizedBox.shrink();
}
}),
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: FloatingActionButton.extended(
heroTag: 'createContact',
backgroundColor: Colors.indigo,
icon: const Icon(Icons.person_add_alt_1, color: Colors.white),
label: const Text("Create Contact",
style: TextStyle(color: Colors.white)),
onPressed: () {
setState(() => isFabExpanded = false);
_handleCreateContact();
},
),
),
],
FloatingActionButton(
heroTag: 'toggleFab',
backgroundColor: Colors.red,
onPressed: () => setState(() => isFabExpanded = !isFabExpanded),
child: Icon(isFabExpanded ? Icons.close : Icons.add,
color: Colors.white),
),
],
),
body: Column(
children: [
Padding(
padding: MySpacing.xy(8, 8),
child: Row(
children: [
Expanded(
child: SizedBox(
height: 35,
child: TextField(
controller: searchController,
onChanged: (value) {
controller.searchQuery.value = value;
controller.applyFilters();
},
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
prefixIcon: const Icon(Icons.search,
size: 20, color: Colors.grey),
hintText: 'Search contacts...',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
),
),
),
MySpacing.width(8),
Tooltip(
message: 'Refresh Data',
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: _refreshDirectory,
child: const Padding(
padding: EdgeInsets.all(0),
child: Icon(Icons.refresh, color: Colors.green, size: 28),
),
),
),
MySpacing.width(8),
Obx(() {
final isFilterActive = controller.hasActiveFilters();
return Stack(
children: [
Container(
height: 35,
width: 35,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: Icon(Icons.filter_alt_outlined,
size: 20,
color: isFilterActive
? Colors.indigo
: Colors.black87),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20)),
),
builder: (_) =>
const DirectoryFilterBottomSheet(),
);
},
),
),
if (isFilterActive)
Positioned(
top: 6,
right: 6,
child: Container(
height: 8,
width: 8,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
),
],
);
}),
MySpacing.width(10),
Container(
height: 35,
width: 35,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10),
),
child: PopupMenuButton<int>(
padding: EdgeInsets.zero,
icon: const Icon(Icons.more_vert,
size: 20, color: Colors.black87),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
itemBuilder: (context) => [
PopupMenuItem<int>(
value: 0,
enabled: false,
child: Obx(() => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodySmall('Show Inactive',
fontWeight: 600),
Switch.adaptive(
value: !controller.isActive.value,
activeColor: Colors.indigo,
onChanged: (val) {
controller.isActive.value = !val;
controller.fetchContacts(active: !val);
Navigator.pop(context);
},
),
],
)),
),
],
),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return ListView.separated(
itemCount: 10,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, __) => SkeletonLoaders.contactSkeletonCard(),
);
}
if (controller.filteredContacts.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.contact_page_outlined,
size: 60, color: Colors.grey),
const SizedBox(height: 12),
MyText.bodyMedium('No contacts found.', fontWeight: 500),
],
),
);
}
return ListView.separated(
padding: MySpacing.only(left: 8, right: 8, top: 4, bottom: 80),
itemCount: controller.filteredContacts.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, index) {
final contact = controller.filteredContacts[index];
final nameParts = contact.name.trim().split(" ");
final firstName = nameParts.first;
final lastName = nameParts.length > 1 ? nameParts.last : "";
final tags = contact.tags.map((tag) => tag.name).toList();
return InkWell(
onTap: () {
Get.to(() => ContactDetailScreen(contact: contact));
},
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(
firstName: firstName,
lastName: lastName,
size: 35),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(contact.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis),
MyText.bodySmall(contact.organization,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis),
MySpacing.height(8),
...contact.contactEmails.map((e) =>
GestureDetector(
onTap: () => LauncherUtils.launchEmail(
e.emailAddress),
onLongPress: () =>
LauncherUtils.copyToClipboard(
e.emailAddress,
typeLabel: 'Email'),
child: Padding(
padding:
const EdgeInsets.only(bottom: 4),
child: Row(
children: [
const Icon(Icons.email_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 180),
child: MyText.labelSmall(
e.emailAddress,
overflow: TextOverflow.ellipsis,
color: Colors.indigo,
decoration:
TextDecoration.underline,
),
),
],
),
),
)),
...contact.contactPhones.map((p) => Padding(
padding: const EdgeInsets.only(
bottom: 8, top: 4),
child: Row(
children: [
GestureDetector(
onTap: () =>
LauncherUtils.launchPhone(
p.phoneNumber),
onLongPress: () =>
LauncherUtils.copyToClipboard(
p.phoneNumber,
typeLabel: 'Phone'),
child: Row(
children: [
const Icon(Icons.phone_outlined,
size: 16,
color: Colors.indigo),
MySpacing.width(4),
ConstrainedBox(
constraints:
const BoxConstraints(
maxWidth: 140),
child: MyText.labelSmall(
p.phoneNumber,
overflow:
TextOverflow.ellipsis,
color: Colors.indigo,
decoration: TextDecoration
.underline,
),
),
],
),
),
MySpacing.width(8),
GestureDetector(
onTap: () =>
LauncherUtils.launchWhatsApp(
p.phoneNumber),
child: const FaIcon(
FontAwesomeIcons.whatsapp,
color: Colors.green,
size: 16),
),
],
),
)),
if (tags.isNotEmpty) ...[
MySpacing.height(2),
MyText.labelSmall(tags.join(', '),
color: Colors.grey[500],
maxLines: 1,
overflow: TextOverflow.ellipsis),
],
],
),
),
Column(
children: [
const Icon(Icons.arrow_forward_ios,
color: Colors.grey, size: 16),
MySpacing.height(8),
],
),
],
),
),
);
},
);
}),
),
],
),
);
}
}