feat(bucket): implement create bucket functionality with UI and API integration

This commit is contained in:
Vaibhav Surve 2025-07-11 17:28:22 +05:30
parent 2926bb7216
commit 33d267f18e
6 changed files with 384 additions and 6 deletions

View File

@ -1,5 +1,3 @@
// Updated AddContactController to support multiple emails and phones
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
@ -75,7 +73,7 @@ class AddContactController extends GetxController {
} catch (e) {
logSafe("Failed to fetch buckets: \$e", level: LogLevel.error);
}
}
}
Future<void> fetchOrganizationNames() async {
try {

View 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()}");
}
}

View File

@ -43,4 +43,5 @@ class ApiEndpoints {
static const String updateContact = "/directory";
static const String getDirectoryNotes = "/directory/notes";
static const String updateDirectoryNotes = "/directory/note";
static const String createBucket = "/directory/bucket";
}

View File

@ -163,7 +163,7 @@ class ApiService {
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
);
);
try {
final response = await http
@ -243,6 +243,49 @@ class ApiService {
}
/// 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({
int pageSize = 1000,
int pageNumber = 1,

View 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();
}
}

View File

@ -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/directory_filter_bottom_sheet.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 {
final DirectoryController controller = Get.find();
@ -32,11 +34,104 @@ class DirectoryView extends StatelessWidget {
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.red,
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 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"),
),
),
],
),
],
),
),
);
},
);
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(
AddContactBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.transparent,
);
if (result == true) {
controller.fetchContacts();
}
@ -282,8 +377,8 @@ class DirectoryView extends StatelessWidget {
),
)),
...contact.contactPhones.map((p) => Padding(
padding:
const EdgeInsets.only(bottom: 8,top: 4),
padding: const EdgeInsets.only(
bottom: 8, top: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [