748 lines
29 KiB
Dart
748 lines
29 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:geocoding/geocoding.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:lottie/lottie.dart';
|
|
import 'package:nearledaily/constants/color_constants.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import '../../Helper/Logger.dart';
|
|
import '../../constants/font_constants.dart';
|
|
import '../../data/tenant/get_tenant_res.dart';
|
|
import '../../domain/provider/authentication/location.dart';
|
|
import '../../domain/provider/tenant/get_tenant_pro.dart';
|
|
import '../../domain/repository/authentication/auth_repository.dart';
|
|
import '../../domain/repository/tenant/get_tenant_repo.dart';
|
|
import '../../modules/authentication/auth.dart';
|
|
import '../../modules/tenant/category.dart';
|
|
import '../../modules/tenant/get_tenant.dart';
|
|
import '../../view/authentication/costomer_create_view.dart';
|
|
import '../../widgets/text_widget.dart';
|
|
import '../tenant_controller /tenant_list.dart';
|
|
|
|
class DashboardController extends GetxController {
|
|
// Loading state
|
|
var isLoading = true.obs;
|
|
List<Authentication> fetchedLocations = [];
|
|
var categories = <Category>[].obs;
|
|
|
|
|
|
var selectedIndex = 0.obs;
|
|
var show = true.obs;
|
|
|
|
|
|
|
|
void fetchCategories() async {
|
|
try {
|
|
isLoading(true);
|
|
|
|
final url = Uri.parse(
|
|
'https://fiesta.nearle.app/live/api/v1/mob/utils/getappcategories');
|
|
|
|
final response = await http.get(url);
|
|
|
|
if (response.statusCode == 200) {
|
|
final data = jsonDecode(response.body);
|
|
|
|
categories.value = (data['details'] as List)
|
|
.map((e) => Category.fromJson(e))
|
|
.toList();
|
|
|
|
print(response.body);
|
|
print('gtot');
|
|
|
|
}
|
|
} catch (e) {
|
|
print("Error: $e");
|
|
} finally {
|
|
isLoading(false);
|
|
}
|
|
}
|
|
|
|
void selectCategory(int index) {
|
|
selectedIndex.value = index;
|
|
}
|
|
|
|
|
|
Future<void> checkMainFlag() async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
|
|
bool firstTime = prefs.getBool("firstTime") ?? true;
|
|
|
|
if (firstTime) {
|
|
show.value = true;
|
|
|
|
// Next time this becomes false
|
|
prefs.setBool("firstTime", false);
|
|
} else {
|
|
show.value = false;
|
|
}
|
|
}
|
|
|
|
void location(){
|
|
_showLocationBottomSheet();
|
|
}
|
|
|
|
// Carousel images
|
|
var carouselImages = <String>[].obs;
|
|
// List<Tenants> getAllTenants = [];
|
|
// Grid items
|
|
var gridItems = <Map<String, String>>[].obs;
|
|
var currentAddress = ''.obs;
|
|
final CustomerLocationProvider locationProvider = CustomerLocationProvider();
|
|
final TenantController tenantController = Get.put(TenantController());
|
|
|
|
@override
|
|
void onInit() {
|
|
// loadTenants();
|
|
_getAndUpdateCurrentLocation();
|
|
_fetchLocations();
|
|
checkMainFlag();
|
|
print("🚀 DashboardController onInit() called");
|
|
// getTenantCustomers();
|
|
// Simulate data loading (e.g., from an API)
|
|
Future.delayed(const Duration(seconds: 2), () {
|
|
// Load carousel images
|
|
carouselImages.addAll([
|
|
'assets/Banner_1.png',
|
|
'assets/Banner_2.png',
|
|
]);
|
|
|
|
|
|
|
|
// Set loading to false after data is loaded
|
|
isLoading.value = false;
|
|
});
|
|
fetchCategories();
|
|
super.onInit();
|
|
}
|
|
Future<void> _fetchLocations() async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
final id = prefs.getInt('customerId');
|
|
|
|
try {
|
|
final locations = await locationProvider.fetchCustomerLocations(id!);
|
|
fetchedLocations = locations;
|
|
print(locations);
|
|
|
|
} catch (e) {
|
|
print('Error fetching locations: $e');
|
|
} finally {
|
|
|
|
}
|
|
}
|
|
void loadTenants() async {
|
|
isLoading.value = true;
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
int? id = prefs.getInt('customerId');
|
|
|
|
try {
|
|
final response = await CustomerTenantsProvider().getCustomerTenants(id!, 1);
|
|
|
|
if (response != null && response.status == true && response.details != null) {
|
|
populateGridFromTenants(response);
|
|
} else {
|
|
gridItems.clear();
|
|
}
|
|
} catch (e) {
|
|
print("⛔ Error fetching tenants: $e");
|
|
gridItems.clear();
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
void populateGridFromTenants(CustomerTenantsResponse response) {
|
|
final tenants = response.details ?? [];
|
|
gridItems.clear();
|
|
|
|
for (var tenant in tenants) {
|
|
gridItems.add({
|
|
'tenantid': (tenant.tenantid ?? 0).toString(),
|
|
'title': tenant.tenantname ?? 'No Name',
|
|
'address': tenant.address ?? '',
|
|
'licenseno': tenant.licenseno ?? '',
|
|
'primaryemail': tenant.primaryemail ?? '',
|
|
'primarycontact': tenant.primarycontact ?? '',
|
|
'pickuplocationid': (tenant.pickuplocationid ?? 0).toString(),
|
|
'applocationid': (tenant.applocationid ?? 0).toString(),
|
|
'suburb': tenant.suburb ?? '',
|
|
'city': tenant.city ?? '',
|
|
'latitude': tenant.latitude ?? '',
|
|
'longitude': tenant.longitude ?? '',
|
|
'postcode': tenant.postcode ?? '',
|
|
'tenantimage': tenant.tenantimage != null && tenant.tenantimage!.isNotEmpty
|
|
? tenant.tenantimage!
|
|
: 'https://via.placeholder.com/150',
|
|
'locationid': (tenant.locationid ?? 0).toString(),
|
|
'locationname': tenant.locationname ?? '',
|
|
'subcategoryid': (tenant.subcategoryid ?? 0).toString(),
|
|
'categoryid': (tenant.categoryid ?? 0).toString(),
|
|
'registrationno': tenant.registrationno ?? '',
|
|
});
|
|
}
|
|
}
|
|
Future<void> _updateProfile({
|
|
required String address,
|
|
required double latitude,
|
|
required double longitude,
|
|
required String city,
|
|
required String state,
|
|
required String suburb,
|
|
}) async {
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
int? id = prefs.getInt('customerId');
|
|
prefs.setDouble('lat', latitude);
|
|
prefs.setDouble('long', longitude);
|
|
String? name = prefs.getString('customerFirstname');
|
|
String? contactNo = prefs.getString('contactno');
|
|
String? fcm = prefs.getString('fcmToken');
|
|
|
|
if (id == null) {
|
|
// Get.snackbar("Error", "Customer ID not found");
|
|
return;
|
|
}
|
|
|
|
final repo = LoginRepository();
|
|
|
|
Map<String, dynamic> data = {
|
|
"customerid": id,
|
|
"configid": 2,
|
|
"address": address.toString(),
|
|
"suburb": suburb.toString(),
|
|
"city": city.toString(),
|
|
"state": state.toString(),
|
|
"latitude": latitude.toString(),
|
|
"longitude": longitude.toString(),
|
|
'customertoken': fcm
|
|
};
|
|
|
|
print("Request Data: $data");
|
|
// 🧾 Print all data in readable format
|
|
print("🚀 Sending Update Profile Request:");
|
|
print("==================================");
|
|
print("🆔 Customer ID: $id");
|
|
print("👤 Name: $name");
|
|
print("📞 Contact: $contactNo");
|
|
print("📍 Address: $address");
|
|
print("🏙️ City: $city");
|
|
print("🌆 State: $state");
|
|
print("🏘️ Suburb: $suburb");
|
|
print("🧭 Latitude: $latitude");
|
|
print("🧭 Longitude: $longitude");
|
|
print("🧭 fcm: $fcm");
|
|
print("==================================");
|
|
|
|
|
|
try {
|
|
final response = await repo.updateProfile(data);
|
|
print("Server Response: $response");
|
|
|
|
if (response != null && response['status'] == true) {
|
|
tenantController.loadTenants();
|
|
// Get.snackbar("Success", "Location updated");
|
|
} else {
|
|
// Get.snackbar("Error", "Something went wrong");
|
|
}
|
|
} catch (e) {
|
|
print("Update Profile Error: $e");
|
|
// Get.snackbar("Error", "Something went wrong");
|
|
}
|
|
}
|
|
Future<void> _getAndUpdateCurrentLocation() async {
|
|
bool serviceEnabled;
|
|
LocationPermission permission;
|
|
|
|
// Check if location service is enabled
|
|
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
if (!serviceEnabled) {
|
|
// Show bottom sheet to add new address
|
|
_showLocationBottomSheet();
|
|
return;
|
|
}
|
|
|
|
// Check location permission
|
|
permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
print("❌ Location permissions are denied.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (permission == LocationPermission.deniedForever) {
|
|
print("❌ Location permissions are permanently denied.");
|
|
return;
|
|
}
|
|
|
|
// Get current position
|
|
Position position = await Geolocator.getCurrentPosition(
|
|
desiredAccuracy: LocationAccuracy.high,
|
|
);
|
|
|
|
// Reverse geocode
|
|
List<Placemark> placemarks =
|
|
await placemarkFromCoordinates(position.latitude, position.longitude);
|
|
|
|
if (placemarks.isNotEmpty) {
|
|
Placemark place = placemarks.first;
|
|
|
|
String fullAddress =
|
|
"${place.name}, ${place.subLocality}, ${place.locality}, ${place.administrativeArea}, ${place.country}, ${place.postalCode}";
|
|
String city = place.locality ?? '';
|
|
String state = place.administrativeArea ?? '';
|
|
String suburb = place.subLocality ?? '';
|
|
|
|
// Auto update profile
|
|
await _updateProfile(
|
|
address: fullAddress,
|
|
latitude: position.latitude,
|
|
longitude: position.longitude,
|
|
city: city,
|
|
state: state,
|
|
suburb: suburb,
|
|
);
|
|
|
|
|
|
} else {
|
|
print("⚠️ No address found for this location.");
|
|
}
|
|
}
|
|
|
|
Future<void> _showLocationBottomSheet() async {
|
|
await _fetchLocations();
|
|
|
|
if (fetchedLocations.isEmpty) return;
|
|
|
|
await Future.delayed(Duration.zero);
|
|
|
|
Get.bottomSheet(
|
|
StatefulBuilder(
|
|
builder: (context, setState) {
|
|
String? selectedAddress;
|
|
|
|
return SafeArea(
|
|
child: Container(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
height: MediaQuery.of(context).size.height * 0.75,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Drag Handle
|
|
Center(
|
|
child: Container(
|
|
margin: const EdgeInsets.only(top: 10, bottom: 16),
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: ColorConstants.primaryColor,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── Header Row ──
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
// Title + subtitle
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ReusableTextWidget(
|
|
text: 'Location & Address',
|
|
color: Colors.black,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
const SizedBox(height: 4),
|
|
ReusableTextWidget(
|
|
text: 'Allow location access for faster delivery',
|
|
color: Colors.grey.shade600,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Map pin illustration box
|
|
Container(
|
|
width: 64,
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEDE7F6),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
const Icon(Icons.location_on,
|
|
color: Color(0xFF662582), size: 32),
|
|
Positioned(
|
|
top: 8,
|
|
left: 8,
|
|
child: Icon(Icons.auto_awesome,
|
|
color: Color(0xFF662582), size: 12),
|
|
),
|
|
Positioned(
|
|
top: 12,
|
|
left: 14,
|
|
child: Icon(Icons.auto_awesome,
|
|
color: Color(0xFF662582), size: 8),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// ── Turn on Location Card ──
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF0EAFB),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Crosshair icon circle
|
|
Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.08),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(Icons.my_location,
|
|
color: Color(0xFF662582), size: 22),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Text
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ReusableTextWidget(
|
|
text: 'Turn on location',
|
|
color: Colors.black,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
const SizedBox(height: 2),
|
|
ReusableTextWidget(
|
|
text: 'Detect your location automatically',
|
|
color: Colors.grey.shade600,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Enable button
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
final result =
|
|
await Get.to(() => const MapPickerPage1());
|
|
if (result != null &&
|
|
result is Map<String, dynamic>) {
|
|
await _updateProfile(
|
|
address: result['address'] ?? '',
|
|
latitude: double.tryParse(
|
|
result['latitude'] ?? '0') ??
|
|
0.0,
|
|
longitude: double.tryParse(
|
|
result['longitude'] ?? '0') ??
|
|
0.0,
|
|
city: result['city'] ?? '',
|
|
state: result['state'] ?? '',
|
|
suburb: result['suburb'] ?? '',
|
|
);
|
|
Navigator.pop(context, result);
|
|
}
|
|
},
|
|
icon: const Icon(Icons.near_me,
|
|
color: Colors.white, size: 16),
|
|
label: ReusableTextWidget(
|
|
text: 'Enable',
|
|
color: Colors.white,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ColorConstants.primaryColor,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 14, vertical: 10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Saved Addresses Header ──
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
ReusableTextWidget(
|
|
text: 'Saved Addresses',
|
|
color: Colors.black,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
GestureDetector(
|
|
onTap: () async {
|
|
final result =
|
|
await Get.to(() => const MapPickerPage1());
|
|
if (result != null &&
|
|
result is Map<String, dynamic>) {
|
|
await _updateProfile(
|
|
address: result['address'] ?? '',
|
|
latitude: double.tryParse(
|
|
result['latitude'] ?? '0') ??
|
|
0.0,
|
|
longitude: double.tryParse(
|
|
result['longitude'] ?? '0') ??
|
|
0.0,
|
|
city: result['city'] ?? '',
|
|
state: result['state'] ?? '',
|
|
suburb: result['suburb'] ?? '',
|
|
);
|
|
Navigator.pop(context, result);
|
|
}
|
|
},
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.add_circle_outline,
|
|
color: ColorConstants.primaryColor, size: 18),
|
|
const SizedBox(width: 4),
|
|
ReusableTextWidget(
|
|
text: 'Add New',
|
|
color: ColorConstants.primaryColor,
|
|
fontFamily: FontConstants.fontFamily,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// ── Address List ──
|
|
Expanded(
|
|
child: fetchedLocations.isNotEmpty
|
|
? ListView.builder(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 16),
|
|
itemCount: fetchedLocations.length,
|
|
itemBuilder: (context, index) {
|
|
final loc = fetchedLocations[index];
|
|
final addr = loc.address ?? '';
|
|
final isSelected = selectedAddress == addr;
|
|
|
|
return GestureDetector(
|
|
onTap: () async {
|
|
setState(() => selectedAddress = addr);
|
|
await _updateProfile(
|
|
address: loc.address ?? '',
|
|
latitude: double.tryParse(
|
|
loc.latitude ?? '0') ??
|
|
0.0,
|
|
longitude: double.tryParse(
|
|
loc.longitude ?? '0') ??
|
|
0.0,
|
|
city: loc.city ?? '',
|
|
state: loc.state ?? '',
|
|
suburb: loc.suburb ?? '',
|
|
);
|
|
Navigator.pop(context, loc);
|
|
},
|
|
child: Container(
|
|
margin:
|
|
const EdgeInsets.only(bottom: 10),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? ColorConstants.primaryColor
|
|
: Colors.grey.shade200,
|
|
width: isSelected ? 1.5 : 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color:
|
|
Colors.black.withOpacity(0.04),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.center,
|
|
children: [
|
|
// Purple circle icon
|
|
Container(
|
|
width: 42,
|
|
height: 42,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEDE7F6),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.location_on,
|
|
color: Color(0xFF662582),
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// Address text
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
ReusableTextWidget(
|
|
text: loc.suburb?.isNotEmpty ==
|
|
true
|
|
? loc.suburb!
|
|
: "Address ${index + 1}",
|
|
color: Colors.black87,
|
|
fontFamily:
|
|
FontConstants.fontFamily,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
const SizedBox(height: 3),
|
|
ReusableTextWidget(
|
|
text: addr,
|
|
color: Colors.grey.shade600,
|
|
fontFamily:
|
|
FontConstants.fontFamily,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
// Current badge or chevron
|
|
if (isSelected)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius:
|
|
BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration:
|
|
const BoxDecoration(
|
|
color: Colors.green,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
ReusableTextWidget(
|
|
text: 'Current',
|
|
color: Colors.green.shade700,
|
|
fontFamily:
|
|
FontConstants.fontFamily,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else
|
|
Icon(Icons.chevron_right,
|
|
color: Colors.grey.shade400,
|
|
size: 22),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
: const Center(
|
|
child: Text(
|
|
"No saved addresses found.",
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
isScrollControlled: true,
|
|
);
|
|
}
|
|
// Add method to update grid dynamically if needed
|
|
void addGridItem(Map<String, String> item) {
|
|
gridItems.add(item);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |