user management functionalities

This commit is contained in:
Venba Team 2025-04-04 09:03:37 +05:30
parent c8f2d798ea
commit 41308b2590
7 changed files with 523 additions and 164 deletions

View File

@ -335,7 +335,6 @@ class _CreateNewPlansState extends State<CreateNewPlan> {
}
void getSelectedPlanFor(){
if (!mounted) return;
@ -590,7 +589,7 @@ class _CreateNewPlansState extends State<CreateNewPlan> {
});
}
Future<void> postPlanData(Map<String, dynamic> planData) async {
Future<void> postPlanData(Map<String, dynamic> planData) async {
final String apiUrldata = '$apiUrl/api/plans/createOrEditPlan';
final token = await getToken(); // Fetch token

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:frontend/Screens/policy/policyCriteria.dart';
import 'package:responsive_builder/responsive_builder.dart';
import '../../routes/custom_appBar.dart';
@ -203,7 +204,8 @@ class _PolicyState extends State<Policy>{
return SizedBox(
width: 180,
height: isDesktop? 65 : 50,
height: isDesktop ? MediaQuery.of(context).size.height * 0.1 : 50,
child:GestureDetector(
onTap :(){
print("Selected Services - $service - $index");
@ -242,45 +244,10 @@ class _PolicyState extends State<Policy>{
Widget _buildPolicyCategory(bool isDesktop){
return Expanded(
child: Container(
color: Colors.brown.shade100,
// color: Colors.white60,
// color: Colors.brown.shade100,
color: Colors.white60,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Container(
child: Text(
"policy Data 1"
),
),
),
],
),
Expanded(
child: Container(
color: Colors.brown.shade200,
child: Row(
children: [
Expanded(
child: Container(
child: Text(
"policy Data 2"
),
),
),
],
),
),
),
],
),
child: PolicyCriteria(isDesktop: isDesktop)
),

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import '../../widgets/custom_text_field.dart';
import '../../widgets/custom_user_form.dart';
class PolicyCriteria extends StatefulWidget{
bool isDesktop;
PolicyCriteria({super.key, required this.isDesktop});
@override
_PolicyCriteriaState createState() => _PolicyCriteriaState();
}
class _PolicyCriteriaState extends State<PolicyCriteria>{
Widget build(BuildContext context){
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text("Selected Policy Criteria For Flight",
style: TextStyle(
fontSize: 16,
color:Colors.grey,
fontWeight: FontWeight.bold
)
,)
],),
SizedBox(height: 15),
Row(
children: [
Expanded(
child: Container(
// color:Colors.grey,
padding: const EdgeInsets.only(top:5,bottom: 5,left:5, right: 5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Class", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
isDesktop: widget.isDesktop,
child: SizedBox(
height: 40,
child: TextField(
style: TextStyle(fontSize: 12),
// controller: controllers["Fname"],
// enabled: !isViewMode,
onChanged: (value) { },
decoration: InputDecoration(
labelText: "Class",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
],
),
SizedBox(height: 10),
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Cost", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
isDesktop: widget.isDesktop,
child: SizedBox(
height: 40,
child: TextField(
style: TextStyle(fontSize: 12),
// controller: controllers["Fname"],
// enabled: !isViewMode,
onChanged: (value) { },
decoration: InputDecoration(
labelText: "Cost",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
],
),
],
),
],
),
),
),
],
),
SizedBox(height: 15),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Color(0xFFEBEBF7),
),
borderRadius: BorderRadius.circular(8),
// color: Colors.brown.shade200,
),
child: Row(
children: [
Expanded(
child: Container(
child: Text(
"policy Data 2"
),
),
),
],
),
),
),
],
);
}
}

View File

@ -44,8 +44,8 @@ class _CreateUserFormState extends State<CreateUserForm>{
// late final List<dynamic>? apiCountryData ;
late List<dynamic>? apiCountryData;
late List<dynamic>? apiCostData;
late List<dynamic>? apiRoleData;
late List<dynamic>? apiUserData;
Map<String, dynamic>? apiselectedUser;
@ -102,20 +102,20 @@ class _CreateUserFormState extends State<CreateUserForm>{
"user_type" : selectedUserType,
"role_id" : selectedRole,
"department_id": selectedDepartment,
"level_id" : selectedLevel,
"group_id" : selectedLevel,
"first_approver": selectedFirstApprover,
"second_approver": selectedSecondApprover,
"third_approver": selectedThirdApprover,
"passport_number": controllers["passportNumber"]?.text,
"place_of_issue": controllers["placeOfIssue"]?.text,
"passport_document": selectedFileNames,
"passport_document": base64PDF,
"date_of_issue": controllers["dateOfIssue"]?.text,
"date_of_expiry": controllers["dateOfExpiry"]?.text,
"created_by": userId,
"is_active": "1",
"passport_fileData": base64PDF,
// "passport_fileData": base64PDF,
};
return data;
}
@ -145,10 +145,10 @@ class _CreateUserFormState extends State<CreateUserForm>{
controllers["dateOfExpiry"]?.text = apiselectedUser?["date_of_expiry"] ?? "";
selectedCountry = apiselectedUser?["country"]?.toString() ?? "";
selectedCountry = apiselectedUser?["country_code"]?.toString() ?? "";
selectedGender = apiselectedUser?["gender"]?.toString().trim() ?? "";
selectedFileNames = apiselectedUser?["passport_document"]?.toString().trim() ?? "";
base64PDF = apiselectedUser?["passport_document"]?.toString().trim() ?? "";
selectedUserType = apiselectedUser?["user_type"]?.toString().trim() ?? "";
selectedRole = apiselectedUser?["role_id"]?.toString().trim() ?? "";
selectedDepartment = apiselectedUser?["department_id"]?.toString().trim() ?? "";
@ -178,6 +178,8 @@ class _CreateUserFormState extends State<CreateUserForm>{
apiCountryData = null;
apiUserData = null;
apiselectedUser = null;
apiCostData = null;
apiRoleData = null;
// Delay accessing context until the widget is fully initialized
// WidgetsBinding.instance.addPostFrameCallback((_) {
@ -246,7 +248,9 @@ class _CreateUserFormState extends State<CreateUserForm>{
initializeData();
fetchCountries();
fetchDepartment();
fetchUsers();
fetchRoles();
}
@ -261,11 +265,27 @@ class _CreateUserFormState extends State<CreateUserForm>{
}
}
Future<void> fetchDepartment() async {
try {
List<dynamic> department = await apiService.fetchCostCenter();
setState(() {
apiCostData = department;
});
} catch (e) {
print('Error fetching department list: $e');
}
}
Future<void> fetchUsers() async {
try {
List<dynamic> users = await apiService.fetchUsers();
setState(() {
apiUserData = users;
// apiUserData = users;
apiUserData = users.where((user) => user["role_id"] == "3").toList();
print("APIUSerDATa - $apiUserData");
userList = apiUserData ?? [];
userMap = {
@ -280,6 +300,24 @@ class _CreateUserFormState extends State<CreateUserForm>{
}
Future<void> fetchRoles() async {
try {
final response = await apiService.fetchMasterDropdown();
if (response is Map<String, dynamic> && response.containsKey("role")) {
List<dynamic> roleList = response["role"]; // Extract the list
setState(() {
apiRoleData = roleList;
});
print("APIROLEData - $apiRoleData");
} else {
print("Error: 'role' key not found or response is not a Map.");
}
} catch (e) {
print('Error fetching role list: $e');
}
}
void printFormData() {
for (var field in dataHeader) {
@ -346,6 +384,7 @@ class _CreateUserFormState extends State<CreateUserForm>{
Map<String,dynamic> data = userDetials;
if (!isValidData(data)) {
print("USERDETAILS : $userDetials");
print("Validation Failed: Required fields are missing.");
setState(() {});
return; // Stop execution if validation fails
@ -362,7 +401,11 @@ class _CreateUserFormState extends State<CreateUserForm>{
errorMessages.clear(); // Reset errors
// Required fields that must not be empty
List<String> requiredFields = ["first_name", "last_name","email","password","mobile_no"];
List<String> requiredFields = ["first_name", "last_name","email","mobile_no"];
if (apiselectedUser == null) {
requiredFields.add("password");
}
// Check validation for each field
for (String field in requiredFields) {
@ -371,6 +414,7 @@ class _CreateUserFormState extends State<CreateUserForm>{
}
}
// Mobile number validation for both mobile_no and alternate_mobile_no
if (data["mobile_no"] != null && data["mobile_no"].toString().isNotEmpty) {
if (!RegExp(r"^\d{10}$").hasMatch(data["mobile_no"].toString())) {
@ -402,6 +446,8 @@ class _CreateUserFormState extends State<CreateUserForm>{
}
void pickPDFWeb() {
html.FileUploadInputElement uploadInput = html.FileUploadInputElement();
uploadInput.accept = '.pdf';
@ -416,35 +462,87 @@ class _CreateUserFormState extends State<CreateUserForm>{
print('File picked: ${file.name}');
print('File size: ${file.size} bytes');
// Ensure the file is a PDF
if (!file.type.contains("pdf")) {
print("Error: Not a PDF file");
return;
}
// Update the state with the selected file name
setState(() {
selectedFileNames = file.name; // Store only one file name
selectedFileNames = file.name; // Store file name
passportDocumentBytes = reader.result as Uint8List; // Store file data
// 🔹 Convert file to Base64
base64PDF = base64Encode(passportDocumentBytes! as List<int>);
// File size check: Ensure the file size does not exceed 3MB
int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes
// 🔹 Convert to Base64 properly
base64PDF = base64Encode(passportDocumentBytes!);
if (file.size > maxFileSize) {
print('Error: File size exceeds 3MB');
// You can show an error message here if necessary
// For example:
// showError('File size cannot exceed 3MB');
print('Base64 Length: ${base64PDF!.length}');
print('Base64 (first 50 chars): ${base64PDF!.substring(0, 50)}');
// Ensure Base64 starts with "JVBERi0x"
if (!base64PDF!.startsWith("JVBERi0x")) {
print("Error: Base64 does not start with 'JVBERi0x'");
return;
}
print('File size: ${file.size} bytes');
print('Base64 Data: $base64PDF'); // Debugging
// 🔹 File size check: Ensure it does not exceed 3MB
int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes
if (file.size > maxFileSize) {
print('Error: File size exceeds 3MB');
return;
}
});
});
});
}
// void pickPDFWeb() {
// html.FileUploadInputElement uploadInput = html.FileUploadInputElement();
// uploadInput.accept = '.pdf';
// uploadInput.click();
//
// uploadInput.onChange.listen((e) {
// final file = uploadInput.files!.first;
// final reader = html.FileReader();
//
// reader.readAsArrayBuffer(file);
// reader.onLoadEnd.listen((event) {
// print('File picked: ${file.name}');
// print('File size: ${file.size} bytes');
//
//
// // Update the state with the selected file name
// setState(() {
// selectedFileNames = file.name; // Store only one file name
// passportDocumentBytes = reader.result as Uint8List; // Store file data
//
// // 🔹 Convert file to Base64
// base64PDF = base64Encode(passportDocumentBytes! as List<int>);
// // File size check: Ensure the file size does not exceed 3MB
// int maxFileSize = 3 * 1024 * 1024; // 3MB in bytes
//
// if (file.size > maxFileSize) {
// print('Error: File size exceeds 3MB');
// // You can show an error message here if necessary
// // For example:
// // showError('File size cannot exceed 3MB');
// return;
// }
//
// print('File size: ${file.size} bytes');
// print('Base64 Data: $base64PDF'); // Debugging
// });
//
//
//
// });
// });
// }
Future<void> createUserData(Map<String, dynamic> userData) async {
bool isUpdating = apiselectedUser != null && apiselectedUser!.isNotEmpty;
@ -624,7 +722,6 @@ class _CreateUserFormState extends State<CreateUserForm>{
Text("Personal Details", style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold,
// color: Colors.black,
color: Color(0xFF8B8FB2)
),
)
],),
@ -1238,36 +1335,7 @@ class _CreateUserFormState extends State<CreateUserForm>{
children: [
Text("Change Password", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: TextField(
style: TextStyle(fontSize: 12),
controller: controllers["password"],
enabled: !isViewMode,
onChanged: (value) {
_clearError("changePassword");
handleChangePassword(value);
},
decoration: InputDecoration(
labelText: "Change Password",
labelStyle: TextStyle(fontSize: 12, color: Colors.grey),
floatingLabelBehavior: FloatingLabelBehavior.never,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16),
),
),
),
),
if (errorMessages["password"] != null) ...[
SizedBox(height: 5), // Space before error message
Text(
errorMessages["password"]!,
style: TextStyle(color: Colors.red, fontSize: 12),
),
],
],
),
],
@ -1621,6 +1689,7 @@ class _CreateUserFormState extends State<CreateUserForm>{
children: [
Text("Passport Document", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
isDesktop: isDesktop,
@ -1641,6 +1710,101 @@ class _CreateUserFormState extends State<CreateUserForm>{
),
),
),
SizedBox(height: 10),
if(base64PDF != null)
// Centers the text
Container(
color: Colors.white,
width : isDesktop
? MediaQuery.of(context).size.width * 0.25
: MediaQuery.of(context).size.width * 0.8,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
GestureDetector(onTap: (){
print('DOWLOAS- $base64PDF ');
if (base64PDF != null && base64PDF!.isNotEmpty) {
try {
// Step 1: Clean the Base64 string
String cleanedBase64 = base64PDF!
.replaceAll("\n", "") // Remove newlines
.replaceAll("\r", "") // Remove carriage returns
.replaceAll(" ", "") // Remove spaces
.trim(); // Trim any whitespace
// Step 2: Ensure valid Base64 length (multiple of 4)
while (cleanedBase64.length % 4 != 0) {
cleanedBase64 += "_"; // Add '=' padding
}
// Step 3: Decode the cleaned Base64
Uint8List bytes;
try {
bytes = base64Decode(cleanedBase64);
} catch (e) {
print("Base64 decoding failed: $e");
return;
}
// Step 4: Create a Blob for download
final blob = html.Blob([bytes], 'application/pdf');
final url = html.Url.createObjectUrlFromBlob(blob);
// Step 5: Trigger the file download
final anchor = html.AnchorElement(href: url)
..setAttribute("download", selectedFileNames ?? "document.pdf")
..style.display = "none";
html.document.body!.append(anchor);
anchor.click();
// Step 6: Clean up
anchor.remove();
html.Url.revokeObjectUrl(url);
print("Download successful!");
} catch (e) {
print("Error downloading file: $e");
}
} else {
print("No file to download.");
}
} ,
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.green.shade300,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"Download",
style: TextStyle(fontSize: 12, color: Colors.white,fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end, // Ensures text is centered within the Text widget
),
SizedBox(width: 5),
Icon(Icons.download ,size: 13, color: Colors.white,)
],
),
),
)
],
),
),
@ -1933,7 +2097,7 @@ class _CreateUserFormState extends State<CreateUserForm>{
constraints: BoxConstraints(maxHeight: 250),
),
items: ["Admin", "Manager", "HR"],
items: ["Normal", "Privilege",],
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
@ -1952,6 +2116,8 @@ class _CreateUserFormState extends State<CreateUserForm>{
// Find the country_code based on selected country_name
selectedUserType = newValue;
// print("selectedUserType - $selectedUserType");
// if (selectedCountry!.isNotEmpty) {
// errorMessages.remove("country_code");
// }
@ -1989,43 +2155,32 @@ class _CreateUserFormState extends State<CreateUserForm>{
Text("Role ", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
isFocused: false, // Dropdown doesn't use focus
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: DropdownSearch<String>(
selectedItem: selectedRole,
enabled: !isViewMode,
popupProps: PopupProps.menu(
// showSearchBox: true,
fit: FlexFit.loose, // Allows flexible height
constraints: BoxConstraints(maxHeight: 250),
height: 45, // Set appropriate height
child: DropdownButtonFormField<String>(
value: selectedRole,
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding:
EdgeInsets.symmetric(horizontal: 10), // Proper padding
),
items: ["Role 1", "Role 2", "Role 3"],
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1,),
),
),
dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
onChanged: isViewMode ? null : (newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedRole = newValue;
// if (selectedCountry!.isNotEmpty) {
// errorMessages.remove("country_code");
// }
selectedRole = newValue;
});
},
items:apiRoleData?.map<DropdownMenuItem<String>>((item){
return DropdownMenuItem(
value: item['dropdown_key'], // ID as value
child:Text(item['dropdown_value'] ?? "Select")
);
}).toList(),
hint: Text("Select"),
),
),
),
@ -2039,7 +2194,7 @@ class _CreateUserFormState extends State<CreateUserForm>{
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Level", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
Text("Group", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
@ -2097,44 +2252,30 @@ class _CreateUserFormState extends State<CreateUserForm>{
Text("Department", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w200, color: Colors.black)),
SizedBox(height: 5),
CustomTextFieldUserWrapper(
isFocused: false,
isFocused: false, // Dropdown doesn't use focus
isDesktop: isDesktop,
child: SizedBox(
height: 40,
child: DropdownSearch<String>(
selectedItem: selectedDepartment,
enabled: !isViewMode,
popupProps: PopupProps.menu(
// showSearchBox: true,
fit: FlexFit.loose, // Allows flexible height
constraints: BoxConstraints(maxHeight: 250),
height: 45, // Set appropriate height
child: DropdownButtonFormField<String>(
value: selectedDepartment,
style: TextStyle(fontSize: 12),
decoration: InputDecoration(
border: InputBorder.none,
contentPadding:
EdgeInsets.symmetric(horizontal: 10), // Proper padding
),
items: ["Dep1", "Dep2", "Dep3"],
dropdownDecoratorProps: DropDownDecoratorProps(
dropdownSearchDecoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 1,),
),
),
dropdownBuilder: (context, selectedItem) => Align( // Center-align selected item
alignment: Alignment.centerLeft,
child: Text(
selectedItem ?? "Select",
style: TextStyle(fontSize: 12),
),
),
onChanged: (String? newValue) {
onChanged: isViewMode ? null : (newValue) {
setState(() {
// Find the country_code based on selected country_name
selectedDepartment = newValue;
// if (selectedCountry!.isNotEmpty) {
// errorMessages.remove("country_code");
// }
selectedDepartment = newValue;
});
},
items:apiCostData?.map<DropdownMenuItem<String>>((item){
return DropdownMenuItem(
value: item['department_id'], // ID as value
child: Text(item['name'] ?? "Unknown"),
);
}).toList(),
hint: Text("Select"),
),
),
),

View File

@ -388,8 +388,6 @@ class _UserListScreenState extends State<UserListScreen> {
SizedBox(width: 10,),
Text(user['user_type'] ?? '') ],),
],),
],
)
),

View File

@ -69,4 +69,95 @@ class ApiService {
}
Future<List> fetchCostCenter() async {
final String apiUrldata = '$apiUrl/api/getCostCenterMaster';
final token = await getToken();
final userId = await getUserId();
// print("SUSRTRT- $userId");
//
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode == 200) {
try {
final data = json.decode(response.body);
print(data);
if (!data.containsKey('data') || data['data'] is!List) {
throw Exception("Invalid response format: 'data' field is missing or not a List");
}
List <dynamic> plansJson = data['data']; // 'data' is a Map, not a List
// setState(() {
// apiCostData = plansJson; // Store API response in state
// if(apiCostData!.isNotEmpty){
// selectedCostCenterId =apiCostData?.first['department_id'];
// }
// if (apiCostData != null && apiCostData!.isNotEmpty) {
// selectedCostCenterId ??= apiCostData!.first['department_id']?.toString();
// }
// });
print('plansJSON');
return plansJson;
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else {
throw Exception('Failed to load plans');
}
}
Future<Map<String, dynamic>> fetchMasterDropdown() async {
final String apiUrldata = '$apiUrl/api/getDropdownMaster';
final token = await getToken();
if (token == null) {
throw Exception('Token not found. Please log in.');
}
final response = await http.get(
Uri.parse(apiUrldata),
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',},);
if (response.statusCode == 200) {
try {
final data = json.decode(response.body);
print(data);
if (!data.containsKey('data') || data['data'] is! Map) {
throw Exception("Invalid response format: 'data' field is missing or not a Map");
}
Map<String, dynamic> plansJson = data['data']; // 'data' is a Map, not a List
return plansJson;
} catch (e) {
throw Exception('Error parsing response: $e');
}
} else {
throw Exception('Failed to load plans');
}
}
}

View File

@ -4,3 +4,9 @@ Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString("auth_token");
}
Future<String?> getUserId() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('userId');
}