feat(bucket): implement create bucket functionality with UI and API integration
This commit is contained in:
parent
2926bb7216
commit
33d267f18e
@ -1,5 +1,3 @@
|
|||||||
// Updated AddContactController to support multiple emails and phones
|
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|||||||
70
lib/controller/directory/create_bucket_controller.dart
Normal file
70
lib/controller/directory/create_bucket_controller.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
|
class BucketController extends GetxController {
|
||||||
|
RxBool isCreating = false.obs;
|
||||||
|
final RxString name = ''.obs;
|
||||||
|
final RxString description = ''.obs;
|
||||||
|
|
||||||
|
Future<void> createBucket() async {
|
||||||
|
if (name.value.trim().isEmpty) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Missing Name",
|
||||||
|
message: "Bucket name is required.",
|
||||||
|
type: SnackbarType.warning,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreating.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logSafe("Creating bucket: ${name.value}");
|
||||||
|
|
||||||
|
final success = await ApiService.createBucket(
|
||||||
|
name: name.value.trim(),
|
||||||
|
description: description.value.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logSafe("Bucket created successfully");
|
||||||
|
|
||||||
|
Get.back(result: true); // Close bottom sheet/dialog
|
||||||
|
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Bucket has been created successfully.",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logSafe("Bucket creation failed", level: LogLevel.error);
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Creation Failed",
|
||||||
|
message: "Unable to create bucket. Please try again later.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logSafe("Error during bucket creation: $e", level: LogLevel.error);
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Unexpected Error",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
type: SnackbarType.error,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isCreating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateName(String value) {
|
||||||
|
name.value = value;
|
||||||
|
logSafe("Bucket name updated: ${value.trim()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDescription(String value) {
|
||||||
|
description.value = value;
|
||||||
|
logSafe("Bucket description updated: ${value.trim()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -43,4 +43,5 @@ class ApiEndpoints {
|
|||||||
static const String updateContact = "/directory";
|
static const String updateContact = "/directory";
|
||||||
static const String getDirectoryNotes = "/directory/notes";
|
static const String getDirectoryNotes = "/directory/notes";
|
||||||
static const String updateDirectoryNotes = "/directory/note";
|
static const String updateDirectoryNotes = "/directory/note";
|
||||||
|
static const String createBucket = "/directory/bucket";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,7 +163,7 @@ class ApiService {
|
|||||||
|
|
||||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
||||||
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
|
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http
|
final response = await http
|
||||||
@ -243,6 +243,49 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Directory calling the API
|
/// Directory calling the API
|
||||||
|
|
||||||
|
static Future<bool> createBucket({
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
}) async {
|
||||||
|
final payload = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
};
|
||||||
|
|
||||||
|
final endpoint = ApiEndpoints.createBucket;
|
||||||
|
|
||||||
|
logSafe("Creating bucket with payload: $payload");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _postRequest(endpoint, payload);
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
logSafe("Create bucket failed: null response", level: LogLevel.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSafe("Create bucket response status: ${response.statusCode}");
|
||||||
|
logSafe("Create bucket response body: ${response.body}");
|
||||||
|
|
||||||
|
final json = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (json['success'] == true) {
|
||||||
|
logSafe("Bucket created successfully: ${json['data']}");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logSafe("Failed to create bucket: ${json['message']}",
|
||||||
|
level: LogLevel.warning);
|
||||||
|
}
|
||||||
|
} catch (e, stack) {
|
||||||
|
logSafe("Exception during createBucket API: ${e.toString()}",
|
||||||
|
level: LogLevel.error);
|
||||||
|
logSafe("StackTrace: ${stack.toString()}", level: LogLevel.debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>?> getDirectoryNotes({
|
static Future<Map<String, dynamic>?> getDirectoryNotes({
|
||||||
int pageSize = 1000,
|
int pageSize = 1000,
|
||||||
int pageNumber = 1,
|
int pageNumber = 1,
|
||||||
|
|||||||
171
lib/model/directory/create_bucket_bottom_sheet.dart
Normal file
171
lib/model/directory/create_bucket_bottom_sheet.dart
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:marco/controller/directory/create_bucket_controller.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_spacing.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
|
|
||||||
|
class CreateBucketBottomSheet extends StatefulWidget {
|
||||||
|
const CreateBucketBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateBucketBottomSheet> createState() => _CreateBucketBottomSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateBucketBottomSheetState extends State<CreateBucketBottomSheet> {
|
||||||
|
final BucketController _controller = Get.put(BucketController());
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
InputDecoration _inputDecoration(String hint) {
|
||||||
|
return InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade100,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
|
||||||
|
),
|
||||||
|
contentPadding: MySpacing.all(16),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return GetBuilder<BucketController>(
|
||||||
|
builder: (_) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: MediaQuery.of(context).viewInsets,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.cardColor,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
boxShadow: const [
|
||||||
|
BoxShadow(color: Colors.black12, blurRadius: 12, offset: Offset(0, -2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(12),
|
||||||
|
Text("Create New Bucket", style: MyTextStyle.titleLarge(fontWeight: 700)),
|
||||||
|
MySpacing.height(24),
|
||||||
|
Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MyText.labelMedium("Bucket Name"),
|
||||||
|
MySpacing.height(8),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: _controller.name.value,
|
||||||
|
onChanged: _controller.updateName,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return "Bucket name is required";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
decoration: _inputDecoration("e.g., Project Docs"),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
MyText.labelMedium("Description"),
|
||||||
|
MySpacing.height(8),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: _controller.description.value,
|
||||||
|
onChanged: _controller.updateDescription,
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: _inputDecoration("Optional bucket description"),
|
||||||
|
),
|
||||||
|
MySpacing.height(24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
icon: const Icon(Icons.close, color: Colors.red),
|
||||||
|
label: MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Colors.red),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.width(12),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: _controller.isCreating.value
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
await _controller.createBucket();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: _controller.isCreating.value
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.check_circle_outline, color: Colors.white),
|
||||||
|
label: MyText.bodyMedium(
|
||||||
|
_controller.isCreating.value ? "Creating..." : "Create",
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: 600,
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.indigo,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
Get.delete<BucketController>();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
|||||||
import 'package:marco/model/directory/add_contact_bottom_sheet.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/directory_filter_bottom_sheet.dart';
|
||||||
import 'package:marco/view/directory/contact_detail_screen.dart';
|
import 'package:marco/view/directory/contact_detail_screen.dart';
|
||||||
|
import 'package:marco/controller/directory/create_bucket_controller.dart';
|
||||||
|
import 'package:marco/model/directory/create_bucket_bottom_sheet.dart';
|
||||||
|
|
||||||
class DirectoryView extends StatelessWidget {
|
class DirectoryView extends StatelessWidget {
|
||||||
final DirectoryController controller = Get.find();
|
final DirectoryController controller = Get.find();
|
||||||
@ -32,11 +34,104 @@ class DirectoryView extends StatelessWidget {
|
|||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
await controller.fetchBuckets();
|
||||||
|
if (controller.contactBuckets.isEmpty) {
|
||||||
|
final shouldCreate = await showModalBottomSheet<bool>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) {
|
||||||
|
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 don’t 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"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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>();
|
||||||
|
// Proceed to open Add Contact bottom sheet
|
||||||
final result = await Get.bottomSheet(
|
final result = await Get.bottomSheet(
|
||||||
AddContactBottomSheet(),
|
AddContactBottomSheet(),
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
controller.fetchContacts();
|
controller.fetchContacts();
|
||||||
}
|
}
|
||||||
@ -282,8 +377,8 @@ class DirectoryView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
...contact.contactPhones.map((p) => Padding(
|
...contact.contactPhones.map((p) => Padding(
|
||||||
padding:
|
padding: const EdgeInsets.only(
|
||||||
const EdgeInsets.only(bottom: 8,top: 4),
|
bottom: 8, top: 4),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user