first commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../profile_setup_screen.dart';
|
||||
import '../../success_screen.dart';
|
||||
|
||||
class PhoneAuthRepo {
|
||||
Future<bool> sentOTP({
|
||||
required String phoneNumber,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/send-otp');
|
||||
final body = {
|
||||
'phone': phoneNumber,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final errorData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorData['message'])));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
EasyLoading.showError(errorData['message']);
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.showError('Network error: Please try again');
|
||||
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> submitOTP({
|
||||
required String phoneNumber,
|
||||
required String otp,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/submit-otp');
|
||||
final body = {
|
||||
'phone': phoneNumber,
|
||||
'otp': otp,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
print(response.statusCode);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(data['message'])));
|
||||
// await saveUserData(userData: data);
|
||||
bool isSetup = data['is_setup'] ?? false;
|
||||
if (isSetup) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => SuccessScreen(email: 'phone')));
|
||||
} else {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => ProfileSetup()));
|
||||
}
|
||||
} else {
|
||||
EasyLoading.showError(data['message']);
|
||||
}
|
||||
} catch (error) {
|
||||
print(error);
|
||||
EasyLoading.showError('Network error: Please try again');
|
||||
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
}
|
||||
}
|
||||
211
lib/Screens/Authentication/Phone Auth/phone_OTP_screen.dart
Normal file
211
lib/Screens/Authentication/Phone Auth/phone_OTP_screen.dart
Normal file
@@ -0,0 +1,211 @@
|
||||
// ignore_for_file: file_names
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:mobile_pos/GlobalComponents/button_global.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Phone%20Auth/Repo/phone_auth_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:pinput/pinput.dart';
|
||||
|
||||
class OTPVerify extends StatefulWidget {
|
||||
const OTPVerify({Key? key, required this.phoneNumber}) : super(key: key);
|
||||
|
||||
final String phoneNumber;
|
||||
|
||||
@override
|
||||
State<OTPVerify> createState() => _OTPVerifyState();
|
||||
}
|
||||
|
||||
class _OTPVerifyState extends State<OTPVerify> {
|
||||
String code = '';
|
||||
FocusNode focusNode = FocusNode();
|
||||
int _start = 60; // 2 minutes in seconds
|
||||
late Timer _timer;
|
||||
|
||||
void _startTimer() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
if (_start == 0) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
_start--;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _resendOtp() async {
|
||||
_start = 60;
|
||||
_startTimer();
|
||||
PhoneAuthRepo repo = PhoneAuthRepo();
|
||||
await repo.sentOTP(phoneNumber: widget.phoneNumber, context: context);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
super.dispose();
|
||||
_timer.cancel();
|
||||
focusNode.dispose();
|
||||
}
|
||||
|
||||
final GlobalKey<FormState> _key = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: kWhite,
|
||||
body: Container(
|
||||
margin: const EdgeInsets.only(left: 25, right: 25),
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const NameWithLogo(),
|
||||
const SizedBox(height: 25),
|
||||
Text(
|
||||
lang.S.of(context).phoneVerification,
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
//lang.S.of(context)
|
||||
lang.S.of(context).weSentAnOTPInYourPhoneNumber,
|
||||
// 'We sent an OTP in your phone number',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.phoneNumber,
|
||||
style: const TextStyle(fontSize: 16, color: kMainColor),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Icon(
|
||||
IconlyLight.edit_square,
|
||||
size: 16,
|
||||
color: kMainColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Form(
|
||||
key: _key,
|
||||
child: Pinput(
|
||||
focusNode: focusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
errorPinTheme: PinTheme(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade200, borderRadius: const BorderRadius.all(Radius.circular(8)))),
|
||||
validator: (value) {
|
||||
// if (value.isEmptyOrNull) {
|
||||
// //return 'Please enter the OTP';
|
||||
// return lang.S.of(context).pleaseEnterTheOTP;
|
||||
// }
|
||||
if (value == null || value.isEmpty) {
|
||||
return lang.S.of(context).pleaseEnterTheOTP;
|
||||
}
|
||||
|
||||
if (value!.length < 4) {
|
||||
//return 'Enter a valid OTP';
|
||||
return lang.S.of(context).enterAValidOTP;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
length: 4,
|
||||
showCursor: true,
|
||||
onCompleted: (pin) {
|
||||
code = pin;
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 45,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
||||
onPressed: () async {
|
||||
focusNode.unfocus();
|
||||
|
||||
if (_key.currentState?.validate() ?? false) {
|
||||
EasyLoading.show();
|
||||
|
||||
PhoneAuthRepo repo = PhoneAuthRepo();
|
||||
|
||||
await repo.submitOTP(phoneNumber: widget.phoneNumber, otp: code, context: context);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).verify,
|
||||
// 'Verify',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_start == 0
|
||||
? GestureDetector(
|
||||
onTap: _resendOtp,
|
||||
child: Text(
|
||||
//'Resend OTP',
|
||||
lang.S.of(context).resendOTP,
|
||||
style: const TextStyle(color: kMainColor),
|
||||
))
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).resendIn,
|
||||
//'Resend OTP in '
|
||||
),
|
||||
Text(
|
||||
'${_start.toString()} ${lang.S.of(context).seconds}',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
214
lib/Screens/Authentication/Phone Auth/phone_auth_screen.dart
Normal file
214
lib/Screens/Authentication/Phone Auth/phone_auth_screen.dart
Normal file
@@ -0,0 +1,214 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:intl_phone_field/intl_phone_field.dart';
|
||||
import 'package:mobile_pos/GlobalComponents/button_global.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Phone%20Auth/phone_OTP_screen.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import 'Repo/phone_auth_repo.dart';
|
||||
|
||||
class PhoneAuth extends StatefulWidget {
|
||||
const PhoneAuth({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PhoneAuth> createState() => _PhoneAuthState();
|
||||
}
|
||||
|
||||
class _PhoneAuthState extends State<PhoneAuth> {
|
||||
String? phoneNumber;
|
||||
|
||||
bool phoneFieldValid = true;
|
||||
late StreamSubscription subscription;
|
||||
bool isDeviceConnected = false;
|
||||
bool isAlertSet = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
body: Container(
|
||||
margin: const EdgeInsets.only(left: 25, right: 25),
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const NameWithLogo(),
|
||||
const SizedBox(height: 25),
|
||||
Text(
|
||||
lang.S.of(context).phoneVerification,
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
lang.S.of(context).registerTitle,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
IntlPhoneField(
|
||||
decoration: InputDecoration(
|
||||
//labelText: 'Phone Number',
|
||||
labelText: lang.S.of(context).phoneNumber,
|
||||
border: const OutlineInputBorder(borderSide: BorderSide(), borderRadius: BorderRadius.all(Radius.circular(15))),
|
||||
),
|
||||
initialCountryCode: 'BD',
|
||||
onChanged: (phone) {
|
||||
phoneNumber = phone.completeNumber;
|
||||
},
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Container(
|
||||
// height: 55,
|
||||
// decoration:
|
||||
// BoxDecoration(border: Border.all(width: phoneFieldValid ? 1 : 2, color: phoneFieldValid ? Colors.grey : Colors.red), borderRadius: BorderRadius.circular(10)),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// const SizedBox(width: 10),
|
||||
// SizedBox(
|
||||
// width: 40,
|
||||
// child: TextField(
|
||||
// controller: countryController,
|
||||
// keyboardType: TextInputType.number,
|
||||
// decoration: const InputDecoration(
|
||||
// border: InputBorder.none,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const Text(
|
||||
// "|",
|
||||
// style: TextStyle(fontSize: 33, color: Colors.grey),
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
// Expanded(
|
||||
// child: Form(
|
||||
// key: _key,
|
||||
// child: TextFormField(
|
||||
// controller: phoneNumberController,
|
||||
// inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d'))],
|
||||
// validator: (value) {
|
||||
// if (value.isEmptyOrNull) {
|
||||
// setState(() {
|
||||
// phoneFieldValid = false;
|
||||
// });
|
||||
// return null;
|
||||
// }
|
||||
// if (value!.length < 8) {
|
||||
// setState(() {
|
||||
// phoneFieldValid = false;
|
||||
// });
|
||||
// return null;
|
||||
// } else {
|
||||
// setState(() {
|
||||
// phoneFieldValid = true;
|
||||
// });
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
// },
|
||||
// keyboardType: TextInputType.phone,
|
||||
// decoration: const InputDecoration(
|
||||
// border: InputBorder.none,
|
||||
// hintText: "Phone Number",
|
||||
// ),
|
||||
// ),
|
||||
// ))
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// Visibility(
|
||||
// visible: !phoneFieldValid,
|
||||
// child: const Padding(
|
||||
// padding: EdgeInsets.only(top: 4, left: 2),
|
||||
// child: Text(
|
||||
// 'Enter a valid phone number',
|
||||
// style: TextStyle(color: Colors.red),
|
||||
// ),
|
||||
// )),
|
||||
// const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 45,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: kMainColor, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
||||
// onPressed: () async {
|
||||
// // const OTPVerify().launch(context);
|
||||
// _key.currentState?.validate();
|
||||
//
|
||||
// if (phoneFieldValid) {
|
||||
// EasyLoading.show();
|
||||
// PhoneAuthRepo repo = PhoneAuthRepo();
|
||||
//
|
||||
// if (await repo.sentOTP(phoneNumber: countryController.text + phoneNumberController.text, context: context)) {
|
||||
// OTPVerify(phoneNumber: countryController.text + phoneNumberController.text).launch(context);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
onPressed: () async {
|
||||
if ((phoneNumber?.length ?? 0) > 8) {
|
||||
EasyLoading.show();
|
||||
PhoneAuthRepo repo = PhoneAuthRepo();
|
||||
|
||||
if (await repo.sentOTP(phoneNumber: phoneNumber!, context: context)) {
|
||||
// OTPVerify(phoneNumber: phoneNumber!).launch(context);
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => OTPVerify(phoneNumber: phoneNumber!)));
|
||||
}
|
||||
} else {
|
||||
EasyLoading.showError(
|
||||
lang.S.of(context).pleaseEnterAValidPhoneNumber,
|
||||
//'Enter a valid Phone Number'
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).sendCode,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// TextButton(
|
||||
// onPressed: () {
|
||||
// const LoginForm(isEmailLogin: false).launch(context);
|
||||
// },
|
||||
// child: Text(lang.S.of(context).staffLogin),
|
||||
// ),
|
||||
// Flexible(
|
||||
// child: TextButton(
|
||||
// onPressed: () {
|
||||
// const LoginForm(isEmailLogin: true).launch(context);
|
||||
// },
|
||||
// child: Text(
|
||||
// lang.S.of(context).logInWithMail,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// maxLines: 1,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
lib/Screens/Authentication/Repo/licnese_repo.dart
Normal file
16
lib/Screens/Authentication/Repo/licnese_repo.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io'; // Required for SocketException
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
// import '../../../constant.dart'; // Keep your constant import
|
||||
|
||||
class PurchaseModel {
|
||||
final String validProductCode = '53621221';
|
||||
final String apiToken = 'orZoxiU81Ok7kxsE0FvfraaO0vDW5tiz';
|
||||
|
||||
// Added 'purchaseCode' as a parameter to the function
|
||||
Future<bool> isActiveBuyer(String purchaseCode) async {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
31
lib/Screens/Authentication/Repo/logout_repo.dart
Normal file
31
lib/Screens/Authentication/Repo/logout_repo.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../currency.dart';
|
||||
|
||||
class LogOutRepo {
|
||||
Future<void> signOut() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove("token");
|
||||
await prefs.remove("hasShownExpiredDialog");
|
||||
CurrencyMethods().removeCurrencyFromLocalDatabase();
|
||||
EasyLoading.showSuccess('Successfully Logged Out');
|
||||
Restart.restartApp();
|
||||
}
|
||||
|
||||
Future<void> signOutApi() async {
|
||||
final uri = Uri.parse('${APIConfig.url}/sign-out');
|
||||
|
||||
await http.get(uri, headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
});
|
||||
await signOut();
|
||||
}
|
||||
}
|
||||
48
lib/Screens/Authentication/Repo/otp_settings_repo.dart
Normal file
48
lib/Screens/Authentication/Repo/otp_settings_repo.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
|
||||
class OtpSettingsModel {
|
||||
final String otpStatus;
|
||||
final String otpExpirationTime;
|
||||
final String otpDurationType;
|
||||
|
||||
OtpSettingsModel({
|
||||
required this.otpStatus,
|
||||
required this.otpExpirationTime,
|
||||
required this.otpDurationType,
|
||||
});
|
||||
|
||||
factory OtpSettingsModel.fromJson(Map<String, dynamic> json) {
|
||||
return OtpSettingsModel(
|
||||
otpStatus: json['otp_status'] ?? '',
|
||||
otpExpirationTime: json['otp_expiration_time'] ?? '',
|
||||
otpDurationType: json['otp_duration_type'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OtpSettingsRepo {
|
||||
Future<OtpSettingsModel?> fetchOtpSettings() async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse("${APIConfig.url}/otp-settings"),
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> decoded = jsonDecode(response.body);
|
||||
final data = decoded['data'];
|
||||
return OtpSettingsModel.fromJson(data);
|
||||
} else {
|
||||
throw Exception("Failed to load OTP settings");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error fetching OTP settings: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
class LogInResponseModel {
|
||||
LogInResponseModel({
|
||||
this.message,
|
||||
this.data,
|
||||
});
|
||||
|
||||
LogInResponseModel.fromJson(dynamic json) {
|
||||
message = json['message'];
|
||||
data = json['data'] != null ? Data.fromJson(json['data']) : null;
|
||||
}
|
||||
|
||||
String? message;
|
||||
Data? data;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['message'] = message;
|
||||
if (data != null) {
|
||||
map['data'] = data?.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Data {
|
||||
Data({
|
||||
this.name,
|
||||
this.email,
|
||||
this.isSetupped,
|
||||
this.token,
|
||||
});
|
||||
|
||||
Data.fromJson(dynamic json) {
|
||||
name = json['name'];
|
||||
email = json['email'];
|
||||
isSetupped = json['is_setupped'];
|
||||
token = json['token'];
|
||||
}
|
||||
|
||||
String? name;
|
||||
String? email;
|
||||
bool? isSetupped;
|
||||
String? token;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['name'] = name;
|
||||
map['email'] = email;
|
||||
map['is_setupped'] = isSetupped;
|
||||
map['token'] = token;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
82
lib/Screens/Authentication/Sign In/Repo/sign_in_repo.dart
Normal file
82
lib/Screens/Authentication/Sign In/Repo/sign_in_repo.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../Repository/constant_functions.dart';
|
||||
import '../../../../currency.dart';
|
||||
import '../../../Home/home.dart';
|
||||
import '../../Sign Up/verify_email.dart';
|
||||
import '../../profile_setup_screen.dart';
|
||||
|
||||
class LogInRepo {
|
||||
Future<bool> logIn({
|
||||
required String email,
|
||||
required String password,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/sign-in');
|
||||
|
||||
final body = {
|
||||
'email': email,
|
||||
'password': password,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
print('Signin ${response.statusCode}');
|
||||
print('Signin ${response.body}');
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
bool isSetupDone = responseData['data']['is_setup'];
|
||||
try {
|
||||
await CurrencyMethods()
|
||||
.saveCurrencyDataInLocalDatabase(selectedCurrencySymbol: responseData['data']['currency']['symbol'], selectedCurrencyName: responseData['data']['currency']['name']);
|
||||
} catch (error) {
|
||||
print(error);
|
||||
}
|
||||
if (!isSetupDone) {
|
||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const ProfileSetup()));
|
||||
} else {
|
||||
await saveUserData(
|
||||
token: responseData['data']['token'],
|
||||
);
|
||||
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const Home()));
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (response.statusCode == 201) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VerifyEmail(
|
||||
email: email,
|
||||
isFormForgotPass: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
}
|
||||
} catch (error) {
|
||||
print(error);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $error')));
|
||||
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
419
lib/Screens/Authentication/Sign In/sign_in_screen.dart
Normal file
419
lib/Screens/Authentication/Sign In/sign_in_screen.dart
Normal file
@@ -0,0 +1,419 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/GlobalComponents/button_global.dart';
|
||||
import 'package:mobile_pos/GlobalComponents/glonal_popup.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Sign%20In/webview_login.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../Sign Up/sign_up_screen.dart';
|
||||
import '../forgot password/forgot_password.dart';
|
||||
import 'Repo/sign_in_repo.dart';
|
||||
import '../../../Repository/check_addon_providers.dart';
|
||||
|
||||
class SignIn extends StatefulWidget {
|
||||
const SignIn({super.key});
|
||||
|
||||
@override
|
||||
State<SignIn> createState() => _SignInState();
|
||||
}
|
||||
|
||||
class _SignInState extends State<SignIn> {
|
||||
bool showPassword = true;
|
||||
bool _isChecked = false;
|
||||
|
||||
///__________variables_____________
|
||||
bool isClicked = false;
|
||||
|
||||
final key = GlobalKey<FormState>();
|
||||
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserCredentials();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
emailController.dispose();
|
||||
passwordController.dispose();
|
||||
}
|
||||
|
||||
void _loadUserCredentials() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
_isChecked = prefs.getBool('remember_me') ?? false;
|
||||
if (_isChecked) {
|
||||
emailController.text = prefs.getString('email') ?? '';
|
||||
passwordController.text = prefs.getString('password') ?? '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _saveUserCredentials() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool('remember_me', _isChecked);
|
||||
if (_isChecked) {
|
||||
prefs.setString('email', emailController.text);
|
||||
prefs.setString('password', passwordController.text);
|
||||
} else {
|
||||
prefs.remove('email');
|
||||
prefs.remove('password');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
final _theme = Theme.of(context);
|
||||
return GlobalPopup(
|
||||
child: Consumer(
|
||||
builder: (_, ref, watch) {
|
||||
final socialNetworkProvider = ref.watch(socialLoginCheckProvider);
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
surfaceTintColor: kWhite,
|
||||
centerTitle: false,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: kWhite,
|
||||
titleSpacing: 16,
|
||||
title: Text(
|
||||
// 'Sign in',
|
||||
lang.S.of(context).signIn,
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Form(
|
||||
key: key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const NameWithLogo(),
|
||||
// const SizedBox(height: 24),
|
||||
// Text(
|
||||
// // 'Welcome back!',f
|
||||
// lang.S.of(context).welcomeBack,
|
||||
// style: textTheme.titleMedium?.copyWith(fontSize: 24.0, fontWeight: FontWeight.w600),
|
||||
// ),
|
||||
// Text(
|
||||
// lang.S.of(context).pleaseEnterYourDetails,
|
||||
// //'Please enter your details.',
|
||||
// style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
|
||||
// ),
|
||||
const SizedBox(height: 34.0),
|
||||
TextFormField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: 'Email',
|
||||
labelText: lang.S.of(context).lableEmail,
|
||||
//hintText: 'Enter email address',
|
||||
hintText: lang.S.of(context).hintEmail,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
// return 'Email can\'t be empty';
|
||||
return lang.S.of(context).emailCannotBeEmpty;
|
||||
} else if (!value.contains('@')) {
|
||||
//return 'Please enter a valid email';
|
||||
return lang.S.of(context).pleaseEnterAValidEmail;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showPassword,
|
||||
decoration: InputDecoration(
|
||||
//labelText: 'Password',
|
||||
labelText: lang.S.of(context).lablePassword,
|
||||
//hintText: 'Enter password',
|
||||
hintText: lang.S.of(context).hintPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPassword = !showPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
// return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 6) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
checkColor: Colors.white,
|
||||
activeColor: kMainColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(3.0),
|
||||
),
|
||||
fillColor: WidgetStateProperty.all(_isChecked ? kMainColor : Colors.transparent),
|
||||
visualDensity: const VisualDensity(horizontal: -4),
|
||||
side: const BorderSide(color: kGreyTextColor),
|
||||
value: _isChecked,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_isChecked = newValue!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
Text(
|
||||
lang.S.of(context).rememberMe,
|
||||
//'Remember me',
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ForgotPassword(),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
lang.S.of(context).forgotPassword,
|
||||
//'Forgot password?',
|
||||
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
maximumSize: const Size(double.infinity, 48),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isClicked) {
|
||||
return;
|
||||
}
|
||||
if (key.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
LogInRepo repo = LogInRepo();
|
||||
if (await repo.logIn(
|
||||
email: emailController.text, password: passwordController.text, context: context)) {
|
||||
_saveUserCredentials();
|
||||
EasyLoading.showSuccess(lang.S.of(context).done);
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).logIn,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
highlightColor: kMainColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(3.0),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const SignUpScreen();
|
||||
},
|
||||
));
|
||||
},
|
||||
hoverColor: kMainColor.withValues(alpha: 0.1),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: lang.S.of(context).donNotHaveAnAccount,
|
||||
//'Don’t have an account? ',
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: lang.S.of(context).signUp,
|
||||
// text:'Sign Up',
|
||||
style:
|
||||
textTheme.bodyMedium?.copyWith(color: kMainColor, fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
socialNetworkProvider.when(data: (isEnable) {
|
||||
if (isEnable) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(height: 20),
|
||||
// Divider
|
||||
SizedBox(
|
||||
height: 28,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 6,
|
||||
children: [
|
||||
Expanded(child: Divider()),
|
||||
Text(
|
||||
lang.S.of(context).orContinueWith,
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
),
|
||||
Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox.square(dimension: 30),
|
||||
// Social Login
|
||||
Row(
|
||||
spacing: 16,
|
||||
children: [
|
||||
// Facebook
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
///_________-This is a _ repo________________________
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebViewLogin(
|
||||
loginUrl: "${APIConfig.domain}login/x?platform=app",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
minimumSize: Size(double.infinity, 48),
|
||||
side: const BorderSide(color: kBorder),
|
||||
foregroundColor: _theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
label: Text(
|
||||
lang.S.of(context).loginX,
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
icon: Container(
|
||||
height: 26,
|
||||
width: 26,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image:
|
||||
DecorationImage(fit: BoxFit.cover, image: AssetImage('images/x.png'))),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Google
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WebViewLogin(
|
||||
loginUrl: "${APIConfig.domain}login/google?platform=app",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
minimumSize: Size(double.infinity, 48),
|
||||
side: const BorderSide(color: kBorder),
|
||||
foregroundColor: _theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
label: Text(
|
||||
lang.S.of(context).loginGoogle,
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
icon: SvgPicture.asset(
|
||||
'assets/google.svg',
|
||||
width: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}, error: (e, stack) {
|
||||
return Center(
|
||||
child: Text(e.toString()),
|
||||
);
|
||||
}, loading: () {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
lib/Screens/Authentication/Sign In/webview_login.dart
Normal file
109
lib/Screens/Authentication/Sign In/webview_login.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
|
||||
import '../../../../Repository/constant_functions.dart'; // Adjust path accordingly
|
||||
import '../../../../currency.dart'; // Adjust path accordingly
|
||||
import '../../Home/home.dart';
|
||||
import '../profile_setup_screen.dart'; // Adjust path accordingly
|
||||
|
||||
class WebViewLogin extends StatefulWidget {
|
||||
final String loginUrl;
|
||||
|
||||
const WebViewLogin({super.key, required this.loginUrl});
|
||||
|
||||
@override
|
||||
_WebViewLoginState createState() => _WebViewLoginState();
|
||||
}
|
||||
|
||||
class _WebViewLoginState extends State<WebViewLogin> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
EasyLoading.show(status: l.S.of(context).loading);
|
||||
}
|
||||
|
||||
void _handleRedirect(String url) async {
|
||||
if (url.contains('/app-login-or-signup')) {
|
||||
final uri = Uri.parse(url);
|
||||
final queryParams = uri.queryParameters;
|
||||
|
||||
final token = queryParams['token'];
|
||||
final isSetup = queryParams['is_setup'] == '1';
|
||||
final status = queryParams['status'];
|
||||
final currency = queryParams['currency'] ?? queryParams['currency_id'];
|
||||
if (status == 'success' && token != null) {
|
||||
await saveUserData(token: token); // Save token
|
||||
if (currency != null) {
|
||||
try {
|
||||
await CurrencyMethods().saveCurrencyDataInLocalDatabase(
|
||||
selectedCurrencySymbol: currency,
|
||||
selectedCurrencyName: currency,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error saving currency: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
if (isSetup) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const Home()),
|
||||
);
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ProfileSetup()),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(l.S.of(context).loginFailedPleaseTryAgain),
|
||||
));
|
||||
Navigator.pop(context); // Close WebView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: WebViewWidget(
|
||||
controller: WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
// Intercept all navigation requests and load within WebView
|
||||
onNavigationRequest: (request) {
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageFinished: (url) {
|
||||
EasyLoading.dismiss();
|
||||
},
|
||||
onPageStarted: (url) {
|
||||
_handleRedirect(url);
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l.S.of(context).someThingWithWrongWithTheWebPage)),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
// Set user agent to mimic a browser
|
||||
..setUserAgent(
|
||||
'Mozilla/5.0 (Linux; Android 10; Mobile) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36')
|
||||
..loadRequest(Uri.parse(widget.loginUrl)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/Screens/Authentication/Sign Up/repo/sign_up_repo.dart
Normal file
118
lib/Screens/Authentication/Sign Up/repo/sign_up_repo.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../Repository/constant_functions.dart';
|
||||
import '../../../../currency.dart';
|
||||
|
||||
class SignUpRepo {
|
||||
Future<dynamic> signUp({required String name, required String email, required String password, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/sign-up');
|
||||
|
||||
final body = {
|
||||
'name': name,
|
||||
'email': email,
|
||||
'password': password,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
final token = responseData['token'];
|
||||
if (token != null) {
|
||||
await saveUserData(token: token);
|
||||
return responseData['token'];
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
}
|
||||
} catch (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> verifyOTP({required String email, required String otp, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/submit-otp');
|
||||
|
||||
final body = {
|
||||
'email': email,
|
||||
'otp': otp,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
String? token = responseData['token'];
|
||||
if (responseData['currency'] != null) {
|
||||
await CurrencyMethods()
|
||||
.saveCurrencyDataInLocalDatabase(selectedCurrencySymbol: responseData['currency']['symbol'], selectedCurrencyName: responseData['currency']['name']);
|
||||
}
|
||||
if (token != null) {
|
||||
await saveUserData(token: responseData['token']);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['error'])));
|
||||
}
|
||||
} catch (error) {
|
||||
print('Error: $error');
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $error')));
|
||||
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Server error: Please try again')));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> resendOTP({required String email, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/resend-otp');
|
||||
|
||||
final body = {
|
||||
'email': email,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['error'])));
|
||||
}
|
||||
} catch (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
260
lib/Screens/Authentication/Sign Up/sign_up_screen.dart
Normal file
260
lib/Screens/Authentication/Sign Up/sign_up_screen.dart
Normal file
@@ -0,0 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Sign%20Up/repo/sign_up_repo.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Sign%20Up/verify_email.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../GlobalComponents/button_global.dart';
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../../constant.dart';
|
||||
import '../Wedgets/check_email_for_otp_popup.dart';
|
||||
import '../profile_setup_screen.dart';
|
||||
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
const SignUpScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SignUpScreen> createState() => _SignUpScreenState();
|
||||
}
|
||||
|
||||
class _SignUpScreenState extends State<SignUpScreen> {
|
||||
///__________Variables________________________________
|
||||
bool showPassword = true;
|
||||
bool isClicked = false;
|
||||
|
||||
///________Key_______________________________________
|
||||
GlobalKey<FormState> key = GlobalKey<FormState>();
|
||||
|
||||
///___________Controllers______________________________
|
||||
TextEditingController nameTextController = TextEditingController();
|
||||
TextEditingController passwordTextController = TextEditingController();
|
||||
TextEditingController emailTextController = TextEditingController();
|
||||
|
||||
///________Dispose____________________________________
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
nameTextController.dispose();
|
||||
passwordTextController.dispose();
|
||||
emailTextController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
final _theme = Theme.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
titleSpacing: 16,
|
||||
centerTitle: true,
|
||||
surfaceTintColor: kWhite,
|
||||
title: Text(
|
||||
lang.S.of(context).signUp,
|
||||
//'Sign Up',
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
|
||||
child: Form(
|
||||
key: key,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
const NameWithLogo(),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).createAFreeAccount,
|
||||
//'Create A Free Account',
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 24.0, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).pleaseEnterYourDetails,
|
||||
//'Please enter your details',
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
|
||||
///____________Name______________________________________________
|
||||
TextFormField(
|
||||
controller: nameTextController,
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
//labelText: 'Full Name',
|
||||
labelText: lang.S.of(context).fullName,
|
||||
//hintText: 'Enter your full name',
|
||||
hintText: lang.S.of(context).enterYourFullName,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'name can\'n be empty';
|
||||
return lang.S.of(context).nameCanNotBeEmpty;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
|
||||
///__________Email______________________________________________
|
||||
TextFormField(
|
||||
controller: emailTextController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
// border: OutlineInputBorder(),
|
||||
// labelText: 'email',
|
||||
labelText: lang.S.of(context).lableEmail,
|
||||
//hintText: 'Enter email address',
|
||||
hintText: lang.S.of(context).hintEmail,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Email can\'n be empty';
|
||||
return lang.S.of(context).emailCannotBeEmpty;
|
||||
} else if (!value.contains('@')) {
|
||||
//return 'Please enter a valid email';
|
||||
return lang.S.of(context).pleaseEnterAValidEmail;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
|
||||
///___________Password_____________________________________________
|
||||
TextFormField(
|
||||
controller: passwordTextController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showPassword,
|
||||
decoration: InputDecoration(
|
||||
//labelText: 'Password',
|
||||
labelText: lang.S.of(context).lablePassword,
|
||||
// hintText: 'Enter password',
|
||||
hintText: lang.S.of(context).hintPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPassword = !showPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 6) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
|
||||
///________Button___________________________________________________
|
||||
ElevatedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
maximumSize: const Size(double.infinity, 48),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isClicked) {
|
||||
return;
|
||||
}
|
||||
if (key.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
SignUpRepo repo = SignUpRepo();
|
||||
final result = await repo.signUp(name: nameTextController.text, email: emailTextController.text, password: passwordTextController.text, context: context);
|
||||
if (result is bool && result) {
|
||||
if (result) {
|
||||
if (await checkEmailForCodePupUp(email: emailTextController.text, context: context, textTheme: textTheme)) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VerifyEmail(
|
||||
email: emailTextController.text,
|
||||
isFormForgotPass: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
} else if (result is String) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfileSetup(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).signUp,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
InkWell(
|
||||
highlightColor: kMainColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(3.0),
|
||||
onTap: () => Navigator.pop(context),
|
||||
hoverColor: kMainColor.withOpacity(0.1),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: lang.S.of(context).alreadyHaveAnAccount,
|
||||
//'Already have an account? ',
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: lang.S.of(context).signIn,
|
||||
//'Sign In',
|
||||
style: textTheme.bodyMedium?.copyWith(color: kMainColor, fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
251
lib/Screens/Authentication/Sign Up/verify_email.dart
Normal file
251
lib/Screens/Authentication/Sign Up/verify_email.dart
Normal file
@@ -0,0 +1,251 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Sign%20Up/repo/sign_up_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:pinput/pinput.dart' as p;
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
import '../Repo/otp_settings_repo.dart';
|
||||
import '../forgot password/repo/forgot_pass_repo.dart';
|
||||
import '../forgot password/set_new_password.dart';
|
||||
import '../profile_setup_screen.dart';
|
||||
|
||||
class VerifyEmail extends StatefulWidget {
|
||||
const VerifyEmail({super.key, required this.email, required this.isFormForgotPass});
|
||||
final String email;
|
||||
final bool isFormForgotPass;
|
||||
|
||||
@override
|
||||
State<VerifyEmail> createState() => _VerifyEmailNewState();
|
||||
}
|
||||
|
||||
class _VerifyEmailNewState extends State<VerifyEmail> {
|
||||
bool isClicked = false;
|
||||
|
||||
Timer? _timer;
|
||||
int _start = 180; // default fallback
|
||||
bool _isButtonEnabled = false;
|
||||
|
||||
final pinController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
final _pinputKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadOtpSettings();
|
||||
}
|
||||
|
||||
Future<void> _loadOtpSettings() async {
|
||||
EasyLoading.show(status: lang.S.of(context).loadingOtpSetting);
|
||||
final settings = await OtpSettingsRepo().fetchOtpSettings();
|
||||
print(settings?.otpExpirationTime);
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (settings != null) {
|
||||
int durationInSec = int.parse(settings.otpExpirationTime);
|
||||
|
||||
if (settings.otpDurationType.toLowerCase().contains("minute")) {
|
||||
durationInSec *= 60;
|
||||
} else if (settings.otpDurationType.toLowerCase().contains("hour")) {
|
||||
durationInSec *= 3600;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_start = durationInSec;
|
||||
});
|
||||
}
|
||||
startTimer();
|
||||
}
|
||||
|
||||
void startTimer() {
|
||||
_isButtonEnabled = false;
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
if (_start > 0) {
|
||||
_start--;
|
||||
} else {
|
||||
_isButtonEnabled = true;
|
||||
_timer?.cancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pinController.dispose();
|
||||
focusNode.dispose();
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const focusedBorderColor = kMainColor;
|
||||
static const fillColor = Color(0xFFF3F3F3);
|
||||
final defaultPinTheme = p.PinTheme(
|
||||
width: 45,
|
||||
height: 52,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 20,
|
||||
color: kTitleColor,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: kBorderColor),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
centerTitle: true,
|
||||
titleSpacing: 16,
|
||||
title: Text(lang.S.of(context).verityEmail),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).verityEmail,
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: lang.S.of(context).digits,
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.email,
|
||||
style:
|
||||
textTheme.bodyMedium?.copyWith(color: kTitleColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Form(
|
||||
key: _pinputKey,
|
||||
child: p.Pinput(
|
||||
length: 6,
|
||||
controller: pinController,
|
||||
focusNode: focusNode,
|
||||
defaultPinTheme: defaultPinTheme,
|
||||
separatorBuilder: (index) => const SizedBox(width: 11),
|
||||
validator: (value) {
|
||||
if ((value?.length ?? 0) < 6) {
|
||||
return lang.S.of(context).enterValidOTP;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
focusedPinTheme: defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
color: kMainColor.withOpacity(0.1),
|
||||
border: Border.all(color: focusedBorderColor),
|
||||
),
|
||||
),
|
||||
submittedPinTheme: defaultPinTheme.copyWith(
|
||||
decoration: defaultPinTheme.decoration!.copyWith(
|
||||
color: fillColor,
|
||||
border: Border.all(color: kTitleColor),
|
||||
),
|
||||
),
|
||||
errorPinTheme: defaultPinTheme.copyBorderWith(
|
||||
border: Border.all(color: Colors.redAccent),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 11, bottom: 11),
|
||||
child: Text(
|
||||
_isButtonEnabled
|
||||
? lang.S.of(context).youCanNowResendYourOtp
|
||||
: lang.S.of(context).resendOtpSeconds(_start),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Visibility(
|
||||
visible: _isButtonEnabled,
|
||||
child: TextButton(
|
||||
onPressed: _isButtonEnabled
|
||||
? () async {
|
||||
EasyLoading.show();
|
||||
SignUpRepo repo = SignUpRepo();
|
||||
if (await repo.resendOTP(email: widget.email, context: context)) {
|
||||
_loadOtpSettings();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(
|
||||
lang.S.of(context).resendOTP,
|
||||
style: TextStyle(color: kMainColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
onPressed: widget.isFormForgotPass
|
||||
? () async {
|
||||
if (isClicked) return;
|
||||
focusNode.unfocus();
|
||||
if (_pinputKey.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
ForgotPassRepo repo = ForgotPassRepo();
|
||||
if (await repo.verifyOTPForgotPass(
|
||||
email: widget.email, otp: pinController.text, context: context)) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SetNewPassword(email: widget.email),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
: () async {
|
||||
if (isClicked) return;
|
||||
focusNode.unfocus();
|
||||
if (_pinputKey.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
SignUpRepo repo = SignUpRepo();
|
||||
if (await repo.verifyOTP(email: widget.email, otp: pinController.text, context: context)) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfileSetup(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).continueE),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../constant.dart';
|
||||
|
||||
Future<dynamic> checkEmailForCodePupUp({required String email, required BuildContext context, required TextTheme textTheme}) {
|
||||
return showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext contextPopUp) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||
child: Dialog(
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).verifyYourEmail,
|
||||
// 'Verify Your Email',
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Text(
|
||||
lang.S.of(context).weHaveSentAConfirmationEmailTo,
|
||||
//'We have sent a confirmation email to',
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.normal, color: kGreyTextColor, fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
email,
|
||||
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Text(
|
||||
lang.S.of(context).folder,
|
||||
// 'It May be that the mail ended up in your spam folder.',
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.normal, color: kGreyTextColor, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 17.0),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(contextPopUp, true);
|
||||
},
|
||||
child: Text(lang.S.of(context).gotIt),
|
||||
//'Got It !',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/change%20password/repo/change_pass_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../../constant.dart';
|
||||
|
||||
class ChangePasswordScreen extends StatefulWidget {
|
||||
const ChangePasswordScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChangePasswordScreen> createState() => _ChangePasswordScreenState();
|
||||
}
|
||||
|
||||
class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool isClicked = false;
|
||||
final TextEditingController _oldPasswordController = TextEditingController();
|
||||
final TextEditingController _newPasswordController = TextEditingController();
|
||||
final TextEditingController _confirmPasswordController = TextEditingController();
|
||||
|
||||
bool showOldPassword = true;
|
||||
bool showPassword = true;
|
||||
bool showConfirmPassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_newPasswordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
final _lang = lang.S.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
surfaceTintColor: kWhite,
|
||||
backgroundColor: kWhite,
|
||||
centerTitle: true,
|
||||
titleSpacing: 16,
|
||||
title: Text(
|
||||
lang.S.of(context).changePassword,
|
||||
//'Create New Password',
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 18),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Text(
|
||||
// lang.S.of(context).setUpNewPassword,
|
||||
// // 'Set Up New Password',
|
||||
// style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
|
||||
// ),
|
||||
// const SizedBox(height: 8.0),
|
||||
// Text(
|
||||
// lang.S.of(context).resetPassword,
|
||||
// //'Reset your password to recovery and log in your account',
|
||||
// style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16), textAlign: TextAlign.center,
|
||||
// ),
|
||||
// const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
controller: _oldPasswordController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showOldPassword,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
// border: const OutlineInputBorder(),
|
||||
hintText: '********',
|
||||
labelText: _lang.oldPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showOldPassword = !showOldPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showOldPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return _lang.oldPasswordCanNotBeEmpty;
|
||||
} else if (value.length < 6) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextFormField(
|
||||
controller: _newPasswordController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showPassword,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
// border: const OutlineInputBorder(),
|
||||
hintText: '********',
|
||||
//labelText: 'New Password',
|
||||
labelText: lang.S.of(context).newPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPassword = !showPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 6) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showConfirmPassword,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
border: const OutlineInputBorder(),
|
||||
//labelText: 'Confirm Password',
|
||||
labelText: lang.S.of(context).confirmPassword,
|
||||
hintText: '********',
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showConfirmPassword = !showConfirmPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showConfirmPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value != _newPasswordController.text) {
|
||||
//return 'Passwords do not match';
|
||||
return lang.S.of(context).passwordsDoNotMatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (isClicked) {
|
||||
return;
|
||||
}
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
ChangePassRepo repo = ChangePassRepo();
|
||||
if (await repo.changePass(
|
||||
oldPass: _oldPasswordController.text,
|
||||
newPass: _confirmPasswordController.text,
|
||||
context: context)) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).save),
|
||||
//'Save',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../Repository/constant_functions.dart';
|
||||
|
||||
class ChangePassRepo {
|
||||
Future<bool> changePass({required String oldPass, required String newPass, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/change-password');
|
||||
|
||||
final body = {
|
||||
'current_password': oldPass,
|
||||
'password': newPass,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
print('ChangePass: $responseData');
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
}
|
||||
} catch (error) {
|
||||
print(error);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $error')));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
89
lib/Screens/Authentication/check_email.dart
Normal file
89
lib/Screens/Authentication/check_email.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class CheckEMail extends StatefulWidget {
|
||||
const CheckEMail({super.key});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_CheckEMailState createState() => _CheckEMailState();
|
||||
}
|
||||
|
||||
class _CheckEMailState extends State<CheckEMail> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 100.0,
|
||||
width: 100.0,
|
||||
child: Image(
|
||||
image: AssetImage('images/mailbox.png'),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).gotEmail,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 25,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Text(
|
||||
lang.S.of(context).sendEmail,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'example@johndoe.com',
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: null,
|
||||
child: Text(lang.S.of(context).checkEmail),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/otp');
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).checkEmail,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
lib/Screens/Authentication/forgot password/forgot_password.dart
Normal file
140
lib/Screens/Authentication/forgot password/forgot_password.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/forgot%20password/repo/forgot_pass_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../../constant.dart';
|
||||
import '../Sign Up/verify_email.dart';
|
||||
import '../Wedgets/check_email_for_otp_popup.dart';
|
||||
|
||||
class ForgotPassword extends StatefulWidget {
|
||||
const ForgotPassword({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ForgotPassword> createState() => _ForgotPasswordState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordState extends State<ForgotPassword> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool isClicked = false;
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
titleSpacing: 16,
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
// 'Forgot Password',
|
||||
lang.S.of(context).forgotPassword,
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 18),
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// 'Forgot Password',
|
||||
lang.S.of(context).forgotPassword,
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
//'Reset password by using your email or phone number',
|
||||
lang.S.of(context).reset,
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
// labelText: 'Email',
|
||||
labelText: lang.S.of(context).lableEmail,
|
||||
// hintText: 'Enter email address',
|
||||
hintText: lang.S.of(context).hintEmail,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Email can\'t be empty';
|
||||
return lang.S.of(context).emailCannotBeEmpty;
|
||||
} else if (!value.contains('@')) {
|
||||
// return 'Please enter a valid email';
|
||||
return lang.S.of(context).pleaseEnterAValidEmail;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
maximumSize: const Size(double.infinity, 48),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (isClicked) {
|
||||
return;
|
||||
}
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
ForgotPassRepo repo = ForgotPassRepo();
|
||||
if (await repo.forgotPass(email: _emailController.text, context: context)) {
|
||||
if (await checkEmailForCodePupUp(
|
||||
email: _emailController.text, context: context, textTheme: textTheme)) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => VerifyEmail(
|
||||
email: _emailController.text,
|
||||
isFormForgotPass: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).continueE,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
|
||||
class ForgotPassRepo {
|
||||
Future<bool> forgotPass({required String email, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/send-reset-code');
|
||||
|
||||
final body = {
|
||||
'email': email,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
}
|
||||
} catch (error) {
|
||||
print(error);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> verifyOTPForgotPass({required String email, required String otp, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/verify-reset-code');
|
||||
|
||||
final body = {
|
||||
'email': email,
|
||||
'code': otp,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
print(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['error'])));
|
||||
}
|
||||
} catch (error) {
|
||||
print(error);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> resetPass({required String email, required String password, required BuildContext context}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/password-reset');
|
||||
|
||||
final body = {
|
||||
'email': email,
|
||||
"password": password,
|
||||
};
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: body);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(responseData['message'])));
|
||||
}
|
||||
} catch (error) {
|
||||
print(error);
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Network error: Please try again')));
|
||||
} finally {}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
165
lib/Screens/Authentication/forgot password/set_new_password.dart
Normal file
165
lib/Screens/Authentication/forgot password/set_new_password.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/forgot%20password/repo/forgot_pass_repo.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../../constant.dart';
|
||||
|
||||
class SetNewPassword extends StatefulWidget {
|
||||
const SetNewPassword({super.key, required this.email});
|
||||
|
||||
final String email;
|
||||
|
||||
@override
|
||||
State<SetNewPassword> createState() => _SetNewPasswordState();
|
||||
}
|
||||
|
||||
class _SetNewPasswordState extends State<SetNewPassword> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool isClicked = false;
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
final TextEditingController _confirmPasswordController = TextEditingController();
|
||||
|
||||
bool showPassword = true;
|
||||
bool showConfirmPassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextTheme textTheme = Theme.of(context).textTheme;
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
surfaceTintColor: kWhite,
|
||||
backgroundColor: kWhite,
|
||||
centerTitle: true,
|
||||
titleSpacing: 16,
|
||||
title: Text(
|
||||
lang.S.of(context).createNewPassword,
|
||||
//'Create New Password',
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 18),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).setUpNewPassword,
|
||||
// 'Set Up New Password',
|
||||
style: textTheme.titleMedium?.copyWith(fontSize: 24.0),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
lang.S.of(context).resetPassword,
|
||||
//'Reset your password to recovery and log in your account',
|
||||
style: textTheme.bodyMedium?.copyWith(color: kGreyTextColor, fontSize: 16), textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showPassword,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
// border: const OutlineInputBorder(),
|
||||
hintText: '********',
|
||||
//labelText: 'New Password',
|
||||
labelText: lang.S.of(context).newPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPassword = !showPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 6) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showConfirmPassword,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
border: const OutlineInputBorder(),
|
||||
//labelText: 'Confirm Password',
|
||||
labelText: lang.S.of(context).confirmPassword,
|
||||
hintText: '********',
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showConfirmPassword = !showConfirmPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
showConfirmPassword ? FeatherIcons.eyeOff : FeatherIcons.eye,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value != _passwordController.text) {
|
||||
//return 'Passwords do not match';
|
||||
return lang.S.of(context).passwordsDoNotMatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (isClicked) {
|
||||
return;
|
||||
}
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
isClicked = true;
|
||||
EasyLoading.show();
|
||||
ForgotPassRepo repo = ForgotPassRepo();
|
||||
if (await repo.resetPass(email: widget.email, password: _confirmPasswordController.text, context: context)) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
isClicked = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).save),
|
||||
//'Save',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/Screens/Authentication/forgot_password.dart
Normal file
119
lib/Screens/Authentication/forgot_password.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../constant.dart';
|
||||
|
||||
class ForgotPassword extends StatefulWidget {
|
||||
const ForgotPassword({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_ForgotPasswordState createState() => _ForgotPasswordState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordState extends State<ForgotPassword> {
|
||||
bool showProgress = false;
|
||||
late String email;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).forgotPassword,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
lang.S.of(context).enterEmail,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
email = value;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).email,
|
||||
border: const OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
hintText: 'example@example.com'),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
// onPressed: () async {
|
||||
// setState(() {
|
||||
// showProgress = true;
|
||||
// });
|
||||
// try {
|
||||
// await FirebaseAuth.instance.sendPasswordResetEmail(
|
||||
// email: email,
|
||||
// );
|
||||
// // ScaffoldMessenger.of(context).showSnackBar(
|
||||
// // const SnackBar(
|
||||
// // content: Text('Check your Inbox'),
|
||||
// // duration: Duration(seconds: 3),
|
||||
// // ),
|
||||
// // );
|
||||
// if (!mounted) return;
|
||||
// const LoginForm(
|
||||
// isEmailLogin: true,
|
||||
// ).launch(context);
|
||||
// } on FirebaseAuthException catch (e) {
|
||||
// if (e.code == 'user-not-found') {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(
|
||||
// content: Text('No user found for that email.'),
|
||||
// duration: Duration(seconds: 3),
|
||||
// ),
|
||||
// );
|
||||
// } else if (e.code == 'wrong-password') {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(
|
||||
// content: Text('Wrong password provided for that user.'),
|
||||
// duration: Duration(seconds: 3),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// } catch (e) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text(e.toString()),
|
||||
// duration: const Duration(seconds: 3),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// setState(
|
||||
// () {
|
||||
// showProgress = false;
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
child: Text(lang.S.of(context).sendLink)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
173
lib/Screens/Authentication/login_form.dart
Normal file
173
lib/Screens/Authentication/login_form.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/register_screen.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../constant.dart';
|
||||
import 'forgot_password.dart';
|
||||
|
||||
class LoginForm extends StatefulWidget {
|
||||
const LoginForm({Key? key, required this.isEmailLogin}) : super(key: key);
|
||||
|
||||
final bool isEmailLogin;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_LoginFormState createState() => _LoginFormState();
|
||||
}
|
||||
|
||||
class _LoginFormState extends State<LoginForm> {
|
||||
bool showPassword = true;
|
||||
late String email, password;
|
||||
GlobalKey<FormState> globalKey = GlobalKey<FormState>();
|
||||
|
||||
bool validateAndSave() {
|
||||
final form = globalKey.currentState;
|
||||
if (form!.validate()) {
|
||||
form.save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Consumer(builder: (context, ref, child) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('images/logoandname.png'),
|
||||
const SizedBox(
|
||||
height: 30.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Form(
|
||||
key: globalKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).emailText,
|
||||
hintText: lang.S.of(context).enterYourEmailAddress,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Email can\'n be empty';
|
||||
return lang.S.of(context).emailCannotBeEmpty;
|
||||
} else if (!value.contains('@')) {
|
||||
//return 'Please enter a valid email';
|
||||
return lang.S.of(context).pleaseEnterAValidEmail;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
// loginProvider.email = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showPassword,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).password,
|
||||
hintText: lang.S.of(context).pleaseEnterAPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPassword = !showPassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(showPassword ? Icons.visibility_off : Icons.visibility),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 4) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
// loginProvider.password = value!;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.isEmailLogin,
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const ForgotPassword()));
|
||||
// const ForgotPassword().launch(context);
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).forgotPassword,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
child: Text(lang.S.of(context).logIn),
|
||||
onPressed: () {
|
||||
if (validateAndSave()) {
|
||||
// loginProvider.signIn(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.isEmailLogin,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).noAcc,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: kGreyTextColor),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigator.pushNamed(context, '/signup');
|
||||
// const RegisterScreen().launch(context);
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const RegisterScreen()));
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).register,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
406
lib/Screens/Authentication/profile_setup_screen.dart
Normal file
406
lib/Screens/Authentication/profile_setup_screen.dart
Normal file
@@ -0,0 +1,406 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/shop_category_provider.dart';
|
||||
import '../../Repository/API/business_setup_repo.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../model/business_category_model.dart';
|
||||
import '../../model/lalnguage_model.dart';
|
||||
|
||||
class ProfileSetup extends StatefulWidget {
|
||||
const ProfileSetup({super.key});
|
||||
|
||||
@override
|
||||
State<ProfileSetup> createState() => _ProfileSetupState();
|
||||
}
|
||||
|
||||
class _ProfileSetupState extends State<ProfileSetup> {
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
}
|
||||
|
||||
// Language? selectedLanguage;
|
||||
BusinessCategory? selectedBusinessCategory;
|
||||
List<Language> language = [];
|
||||
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
XFile? pickedImage;
|
||||
TextEditingController addressController = TextEditingController();
|
||||
TextEditingController openingBalanceController = TextEditingController();
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController nameController = TextEditingController();
|
||||
TextEditingController vatGstTitleController = TextEditingController();
|
||||
TextEditingController vatGstNumberController = TextEditingController();
|
||||
|
||||
DropdownButton<BusinessCategory> getCategory({required List<BusinessCategory> list}) {
|
||||
List<DropdownMenuItem<BusinessCategory>> dropDownItems = [];
|
||||
|
||||
for (BusinessCategory category in list) {
|
||||
var item = DropdownMenuItem(
|
||||
value: category,
|
||||
child: Text(category.name),
|
||||
);
|
||||
dropDownItems.add(item);
|
||||
}
|
||||
return DropdownButton(
|
||||
isExpanded: true,
|
||||
hint: Text(lang.S.of(context).selectBusinessCategory
|
||||
//'Select Business Category'
|
||||
),
|
||||
items: dropDownItems,
|
||||
value: selectedBusinessCategory,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedBusinessCategory = value!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final _lang = lang.S.of(context);
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Consumer(builder: (context, ref, __) {
|
||||
final businessCategoryList = ref.watch(businessCategoryProvider);
|
||||
|
||||
return businessCategoryList.when(data: (categoryList) {
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
title: Text(
|
||||
lang.S.of(context).setUpProfile,
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ElevatedButton.icon(
|
||||
iconAlignment: IconAlignment.end,
|
||||
onPressed: () async {
|
||||
if (selectedBusinessCategory != null) {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
try {
|
||||
BusinessSetupRepo businessSetupRepo = BusinessSetupRepo();
|
||||
await businessSetupRepo.businessSetup(
|
||||
context: context,
|
||||
name: nameController.text,
|
||||
phone: phoneController.text,
|
||||
address: addressController.text.isEmptyOrNull ? null : addressController.text,
|
||||
categoryId: selectedBusinessCategory!.id.toString(),
|
||||
image: pickedImage == null ? null : File(pickedImage!.path),
|
||||
vatGstNumber: vatGstNumberController.text,
|
||||
vatGstTitle: vatGstTitleController.text,
|
||||
openingBalance: openingBalanceController.text,
|
||||
);
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Select a Business Category')));
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: Text(lang.S.of(context).continueButton),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
///________Image______________________________
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
// ignore: sized_box_for_whitespace
|
||||
child: Container(
|
||||
height: 200.0,
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
pickedImage = await _picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.photo_library_rounded,
|
||||
size: 60.0,
|
||||
color: kMainColor,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).gallery,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 40.0),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
pickedImage = await _picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.camera,
|
||||
size: 60.0,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).camera,
|
||||
style: theme.textTheme.titleMedium?.copyWith(color: kGreyTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 120,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// border: Border.all(color: Colors.black54, width: 1),
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(120)),
|
||||
image: pickedImage == null
|
||||
? const DecorationImage(
|
||||
image: AssetImage('images/noImage.png'),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: DecorationImage(
|
||||
image: FileImage(File(pickedImage!.path)),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 35,
|
||||
width: 35,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(120)),
|
||||
shape: BoxShape.circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: SizedBox(
|
||||
height: 60.0,
|
||||
child: FormField(
|
||||
builder: (FormFieldState<dynamic> field) {
|
||||
return InputDecorator(
|
||||
decoration: kInputDecoration.copyWith(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).businessCat,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(5.0))),
|
||||
child: DropdownButtonHideUnderline(child: getCategory(list: categoryList)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
///_________Name________________________
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: AppTextField(
|
||||
// Optional
|
||||
textFieldType: TextFieldType.NAME,
|
||||
controller: nameController,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
// return 'Please enter a valid business name';
|
||||
return lang.S.of(context).pleaseEnterAValidBusinessName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
labelText: lang.S.of(context).businessName,
|
||||
border: const OutlineInputBorder(),
|
||||
//hintText: 'Enter Business/Store Name'
|
||||
hintText: lang.S.of(context).enterBusiness,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
///__________Phone_________________________
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: SizedBox(
|
||||
height: 60.0,
|
||||
child: AppTextField(
|
||||
controller: phoneController,
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
textFieldType: TextFieldType.PHONE,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
labelText: lang.S.of(context).phone,
|
||||
hintText: lang.S.of(context).enterYourPhoneNumber,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
///_________Address___________________________
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: AppTextField(
|
||||
// ignore: deprecated_member_use
|
||||
textFieldType: TextFieldType.ADDRESS,
|
||||
controller: addressController,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: kGreyTextColor),
|
||||
),
|
||||
labelText: lang.S.of(context).companyAddress,
|
||||
hintText: lang.S.of(context).enterFullAddress,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
///________Opening_balance_______________________
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: AppTextField(
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
controller: openingBalanceController, // Optional
|
||||
textFieldType: TextFieldType.PHONE,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
//hintText: 'Enter opening balance',
|
||||
hintText: lang.S.of(context).enterOpeningBalance,
|
||||
labelText: lang.S.of(context).openingBalance,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ///_______Gst_number____________________________
|
||||
// Row(
|
||||
// children: [
|
||||
// ///_______title__________________________________
|
||||
// Expanded(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(top: 10, left: 10, bottom: 10),
|
||||
// child: AppTextField(
|
||||
// validator: (value) {
|
||||
// return null;
|
||||
// },
|
||||
// controller: vatGstTitleController,
|
||||
// textFieldType: TextFieldType.NAME,
|
||||
// decoration: kInputDecoration.copyWith(
|
||||
// labelText: _lang.vatGstTitle,
|
||||
// hintText: _lang.enterVatGstTitle,
|
||||
// border: const OutlineInputBorder(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// ///______Vat_and_Gst_Number__________________________________
|
||||
// Expanded(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: AppTextField(
|
||||
// validator: (value) {
|
||||
// return null;
|
||||
// },
|
||||
// controller: vatGstNumberController, // Optional
|
||||
// textFieldType: TextFieldType.NAME,
|
||||
// decoration: kInputDecoration.copyWith(
|
||||
// hintText: _lang.enterVatGstNumber,
|
||||
// labelText: _lang.vatGstNumber,
|
||||
// border: const OutlineInputBorder(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Center(
|
||||
child: Text(e.toString()),
|
||||
);
|
||||
}, loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
221
lib/Screens/Authentication/register_screen.dart
Normal file
221
lib/Screens/Authentication/register_screen.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
// ignore_for_file: curly_braces_in_flow_control_structures
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/Phone%20Auth/phone_auth_screen.dart';
|
||||
import 'package:mobile_pos/Screens/Authentication/profile_setup_screen.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../Repository/API/register_repo.dart';
|
||||
import '../../constant.dart';
|
||||
import 'login_form.dart';
|
||||
|
||||
class RegisterScreen extends StatefulWidget {
|
||||
const RegisterScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<RegisterScreen> createState() => _RegisterScreenState();
|
||||
}
|
||||
|
||||
class _RegisterScreenState extends State<RegisterScreen> {
|
||||
bool showPass1 = true;
|
||||
bool showPass2 = true;
|
||||
GlobalKey<FormState> globalKey = GlobalKey<FormState>();
|
||||
bool passwordShow = false;
|
||||
String? givenPassword;
|
||||
String? givenPassword2;
|
||||
|
||||
late String email;
|
||||
late String password;
|
||||
late String passwordConfirmation;
|
||||
|
||||
bool validateAndSave() {
|
||||
final form = globalKey.currentState;
|
||||
if (form!.validate() && givenPassword == givenPassword2) {
|
||||
form.save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: implement initState
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: Consumer(builder: (context, ref, child) {
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('images/logoandname.png'),
|
||||
const SizedBox(
|
||||
height: 30.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Form(
|
||||
key: globalKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).emailText,
|
||||
hintText: lang.S.of(context).enterYourEmailAddress,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
// return 'Email can\'n be empty';
|
||||
return lang.S.of(context).emailCannotBeEmpty;
|
||||
} else if (!value.contains('@')) {
|
||||
//return 'Please enter a valid email';
|
||||
return lang.S.of(context).pleaseEnterAValidEmail;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
email = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.text,
|
||||
obscureText: showPass1,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).password,
|
||||
hintText: lang.S.of(context).pleaseEnterAPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPass1 = !showPass1;
|
||||
});
|
||||
},
|
||||
icon: Icon(showPass1 ? Icons.visibility_off : Icons.visibility),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
givenPassword = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 4) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
} else if (value.length < 4) {
|
||||
//return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
password = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
obscureText: showPass2,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).confirmPass,
|
||||
hintText: lang.S.of(context).pleaseEnterAConfirmPassword,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showPass2 = !showPass2;
|
||||
});
|
||||
},
|
||||
icon: Icon(showPass2 ? Icons.visibility_off : Icons.visibility),
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
givenPassword2 = value;
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
//return 'Password can\'t be empty';
|
||||
return lang.S.of(context).passwordCannotBeEmpty;
|
||||
} else if (value.length < 4) {
|
||||
// return 'Please enter a bigger password';
|
||||
return lang.S.of(context).pleaseEnterABiggerPassword;
|
||||
} else if (givenPassword != givenPassword2) {
|
||||
//return 'Password Not mach';
|
||||
return lang.S.of(context).passwordsDoNotMatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (validateAndSave()) {
|
||||
RegisterRepo reg = RegisterRepo();
|
||||
if (await reg.registerRepo(email: email, context: context, password: password, confirmPassword: password))
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfileSetup(),
|
||||
));
|
||||
// auth.signUp(context);
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).register),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).haveAcc,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
const LoginForm(
|
||||
isEmailLogin: true,
|
||||
).launch(context);
|
||||
// Navigator.pushNamed(context, '/loginForm');
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).logIn,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: kMainColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
const PhoneAuth().launch(context);
|
||||
},
|
||||
child: Text(lang.S.of(context).loginWithPhone),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
lib/Screens/Authentication/success_screen.dart
Normal file
79
lib/Screens/Authentication/success_screen.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
// ignore_for_file: unused_result
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../Home/home.dart';
|
||||
|
||||
class SuccessScreen extends StatelessWidget {
|
||||
const SuccessScreen({Key? key, required this.email}) : super(key: key);
|
||||
|
||||
final String? email;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Consumer(builder: (context, ref, _) {
|
||||
final userRoleData = ref.watch(businessInfoProvider);
|
||||
ref.watch(getExpireDateProvider(ref));
|
||||
return userRoleData.when(data: (data) {
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Image(image: AssetImage('images/success.png')),
|
||||
const SizedBox(height: 40.0),
|
||||
Text(
|
||||
lang.S.of(context).congratulation,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
lang.S.of(context).loremIpsumDolorSitAmetConsecteturElitInterdumCons,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
const Home().launch(context);
|
||||
// Navigator.pushNamed(context, '/home');
|
||||
},
|
||||
child: Text(lang.S.of(context).continueButton),
|
||||
),
|
||||
)
|
||||
],
|
||||
// ),
|
||||
// bottomNavigationBar: ButtonGlobalWithoutIcon(
|
||||
// buttontext: lang.S.of(context).continueButton,
|
||||
// buttonDecoration: kButtonDecoration.copyWith(color: kMainColor),
|
||||
// onPressed: () {
|
||||
// const Home().launch(context);
|
||||
// // Navigator.pushNamed(context, '/home');
|
||||
// },
|
||||
// buttonTextColor: Colors.white,
|
||||
// ),
|
||||
)),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
53
lib/Screens/Currency/Model/currency_model.dart
Normal file
53
lib/Screens/Currency/Model/currency_model.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
class CurrencyModel {
|
||||
CurrencyModel({
|
||||
this.id,
|
||||
this.name,
|
||||
this.countryName,
|
||||
this.code,
|
||||
this.symbol,
|
||||
this.position,
|
||||
this.status,
|
||||
this.isDefault,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
CurrencyModel.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
countryName = json['country_name'];
|
||||
code = json['code'];
|
||||
symbol = json['symbol'];
|
||||
position = json['position'];
|
||||
status = json['status'];
|
||||
isDefault = json['is_default'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? name;
|
||||
dynamic countryName;
|
||||
String? code;
|
||||
String? symbol;
|
||||
dynamic position;
|
||||
bool? status;
|
||||
bool? isDefault;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['name'] = name;
|
||||
map['country_name'] = countryName;
|
||||
map['code'] = code;
|
||||
map['symbol'] = symbol;
|
||||
map['position'] = position;
|
||||
map['status'] = status;
|
||||
map['is_default'] = isDefault;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
7
lib/Screens/Currency/Provider/currency_provider.dart
Normal file
7
lib/Screens/Currency/Provider/currency_provider.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../Model/currency_model.dart';
|
||||
import '../Repo/currency_repo.dart';
|
||||
|
||||
CurrencyRepo repo = CurrencyRepo();
|
||||
final currencyProvider = FutureProvider.autoDispose<List<CurrencyModel>>((ref) => repo.fetchAllCurrency());
|
||||
63
lib/Screens/Currency/Repo/currency_repo.dart
Normal file
63
lib/Screens/Currency/Repo/currency_repo.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../Model/currency_model.dart';
|
||||
|
||||
class CurrencyRepo {
|
||||
Future<List<CurrencyModel>> fetchAllCurrency() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/currencies');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final partyList = parsedData['data'] as List<dynamic>;
|
||||
|
||||
// Filter and map the list
|
||||
return partyList
|
||||
.where((category) => category['status'] == true) // Filter by status
|
||||
.map((category) => CurrencyModel.fromJson(category))
|
||||
.toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch Currency');
|
||||
}
|
||||
}
|
||||
|
||||
// Future<List<CurrencyModel>> fetchAllCurrency() async {
|
||||
// final uri = Uri.parse('${APIConfig.url}/currencies');
|
||||
//
|
||||
// final response = await http.get(uri, headers: {
|
||||
// 'Accept': 'application/json',
|
||||
// 'Authorization': await getAuthToken(),
|
||||
// });
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
//
|
||||
// final partyList = parsedData['data'] as List<dynamic>;
|
||||
// return partyList.map((category) => CurrencyModel.fromJson(category)).toList();
|
||||
// // Parse into Party objects
|
||||
// } else {
|
||||
// throw Exception('Failed to fetch Currency');
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<bool> setDefaultCurrency({required num id}) async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/currencies/$id');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
lib/Screens/Currency/currency_screen.dart
Normal file
129
lib/Screens/Currency/currency_screen.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Currency/Provider/currency_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../currency.dart';
|
||||
import 'Model/currency_model.dart';
|
||||
import 'Repo/currency_repo.dart';
|
||||
|
||||
class CurrencyScreen extends StatefulWidget {
|
||||
const CurrencyScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CurrencyScreen> createState() => _CurrencyScreenState();
|
||||
}
|
||||
|
||||
class _CurrencyScreenState extends State<CurrencyScreen> {
|
||||
CurrencyModel selectedCurrency = CurrencyModel(name: currencyName, symbol: currency);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(builder: (context, ref, __) {
|
||||
final currencyData = ref.watch(currencyProvider);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).currency,
|
||||
//'Currency',
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: currencyData.when(
|
||||
data: (currencyList) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: currencyList.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 15),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: selectedCurrency.name == currencyList[index].name ? kMainColor : kWhite,
|
||||
boxShadow: [
|
||||
BoxShadow(color: const Color(0xff0C1A4B).withValues(alpha: 0.24), blurRadius: 1),
|
||||
BoxShadow(color: const Color(0xff473232).withValues(alpha: 0.05), offset: const Offset(0, 3), spreadRadius: -1, blurRadius: 8)
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
selected: selectedCurrency.name == currencyList[index].name,
|
||||
selectedColor: Colors.white,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedCurrency = currencyList[index];
|
||||
});
|
||||
},
|
||||
title: Text('${currencyList[index].name} - ${currencyList[index].symbol}'),
|
||||
trailing: const Icon(
|
||||
(Icons.arrow_forward_ios),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
return null;
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
try {
|
||||
EasyLoading.show();
|
||||
|
||||
final isSet = await CurrencyRepo().setDefaultCurrency(id: selectedCurrency.id!);
|
||||
if (isSet) {
|
||||
await CurrencyMethods().saveCurrencyDataInLocalDatabase(
|
||||
selectedCurrencyName: selectedCurrency.name,
|
||||
selectedCurrencySymbol: selectedCurrency.symbol,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
EasyLoading.showError('Something went wrong');
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.showError('An error occurred: $e');
|
||||
} finally {
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: 50,
|
||||
decoration: const BoxDecoration(
|
||||
color: kMainColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
lang.S.of(context).save,
|
||||
style: const TextStyle(fontSize: 18, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
263
lib/Screens/Customers/Model/parties_model.dart
Normal file
263
lib/Screens/Customers/Model/parties_model.dart
Normal file
@@ -0,0 +1,263 @@
|
||||
import 'package:mobile_pos/model/sale_transaction_model.dart';
|
||||
|
||||
class Party {
|
||||
Party({
|
||||
this.id,
|
||||
this.name,
|
||||
this.businessId,
|
||||
this.email,
|
||||
this.branchId,
|
||||
this.type,
|
||||
this.phone,
|
||||
this.due,
|
||||
this.openingBalanceType,
|
||||
this.openingBalance,
|
||||
this.wallet,
|
||||
this.loyaltyPoints,
|
||||
this.creditLimit,
|
||||
this.address,
|
||||
this.image,
|
||||
this.status,
|
||||
this.meta,
|
||||
this.sales,
|
||||
this.shippingAddress,
|
||||
this.billingAddress,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
Party.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
businessId = json['business_id'];
|
||||
email = json['email'];
|
||||
type = json['type'];
|
||||
phone = json['phone'];
|
||||
branchId = json['branch_id'];
|
||||
due = json['due'];
|
||||
saleCount = json['sales_count'];
|
||||
purchaseCount = json['purchases_count'];
|
||||
totalSaleAmount = json['total_sale_amount'];
|
||||
totalSalePaid = json['total_sale_paid'];
|
||||
totalPurchaseAmount = json['total_purchase_amount'];
|
||||
totalPurchasePaid = json['total_purchase_paid'];
|
||||
totalSaleProfit = json['total_sale_profit'];
|
||||
totalSaleLoss = json['total_sale_loss'];
|
||||
openingBalanceType = json['opening_balance_type'];
|
||||
openingBalance = json['opening_balance'];
|
||||
wallet = json['wallet'];
|
||||
loyaltyPoints = json['loyalty_points'];
|
||||
creditLimit = json['credit_limit'];
|
||||
address = json['address'];
|
||||
image = json['image'];
|
||||
status = json['status'];
|
||||
meta = json['meta'];
|
||||
shippingAddress = json['shipping_address'] != null ? ShippingAddress.fromJson(json['shipping_address']) : null;
|
||||
if (json['sales'] != null) {
|
||||
sales = [];
|
||||
json['sales'].forEach((v) {
|
||||
sales!.add(SalesTransactionModel.fromJson(v));
|
||||
});
|
||||
}
|
||||
billingAddress = json['billing_address'] != null ? BillingAddress.fromJson(json['billing_address']) : null;
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? name;
|
||||
num? businessId;
|
||||
String? email;
|
||||
String? type;
|
||||
String? phone;
|
||||
num? branchId;
|
||||
num? due;
|
||||
num? saleCount;
|
||||
num? purchaseCount;
|
||||
num? totalSaleAmount;
|
||||
num? totalSalePaid;
|
||||
num? totalPurchaseAmount;
|
||||
num? totalPurchasePaid;
|
||||
// num? totalSaleLossProfit;
|
||||
num? totalSaleProfit;
|
||||
num? totalSaleLoss;
|
||||
String? openingBalanceType;
|
||||
num? openingBalance;
|
||||
num? wallet;
|
||||
num? loyaltyPoints;
|
||||
num? creditLimit;
|
||||
String? address;
|
||||
String? image;
|
||||
num? status;
|
||||
dynamic meta;
|
||||
ShippingAddress? shippingAddress;
|
||||
BillingAddress? billingAddress;
|
||||
List<SalesTransactionModel>? sales;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['branch_id'] = branchId;
|
||||
map['name'] = name;
|
||||
map['business_id'] = businessId;
|
||||
map['email'] = email;
|
||||
map['type'] = type;
|
||||
map['phone'] = phone;
|
||||
map['due'] = due;
|
||||
map['sales_count'] = saleCount;
|
||||
map['purchases_count'] = purchaseCount;
|
||||
map['total_sale_amount'] = totalSaleAmount;
|
||||
map['total_sale_paid'] = totalSalePaid;
|
||||
map['total_purchase_amount'] = totalPurchaseAmount;
|
||||
map['total_purchase_paid'] = totalPurchasePaid;
|
||||
map['total_sale_profit'] = totalSaleProfit;
|
||||
map['total_sale_loss'] = totalSaleLoss;
|
||||
map['opening_balance_type'] = openingBalanceType;
|
||||
map['opening_balance'] = openingBalance;
|
||||
map['wallet'] = wallet;
|
||||
map['loyalty_points'] = loyaltyPoints;
|
||||
map['credit_limit'] = creditLimit;
|
||||
map['address'] = address;
|
||||
map['image'] = image;
|
||||
map['status'] = status;
|
||||
map['meta'] = meta;
|
||||
map['sales'] = sales;
|
||||
if (shippingAddress != null) {
|
||||
map['shipping_address'] = shippingAddress?.toJson();
|
||||
}
|
||||
if (billingAddress != null) {
|
||||
map['billing_address'] = billingAddress?.toJson();
|
||||
}
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class BillingAddress {
|
||||
BillingAddress({
|
||||
this.address,
|
||||
this.city,
|
||||
this.state,
|
||||
this.zipCode,
|
||||
this.country,
|
||||
});
|
||||
|
||||
BillingAddress.fromJson(dynamic json) {
|
||||
address = json['address'];
|
||||
city = json['city'];
|
||||
state = json['state'];
|
||||
zipCode = json['zip_code'];
|
||||
country = json['country'];
|
||||
}
|
||||
String? address;
|
||||
String? city;
|
||||
String? state;
|
||||
String? zipCode;
|
||||
String? country;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['address'] = address;
|
||||
map['city'] = city;
|
||||
map['state'] = state;
|
||||
map['zip_code'] = zipCode;
|
||||
map['country'] = country;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class ShippingAddress {
|
||||
ShippingAddress({
|
||||
this.address,
|
||||
this.city,
|
||||
this.state,
|
||||
this.zipCode,
|
||||
this.country,
|
||||
});
|
||||
|
||||
ShippingAddress.fromJson(dynamic json) {
|
||||
address = json['address'];
|
||||
city = json['city'];
|
||||
state = json['state'];
|
||||
zipCode = json['zip_code'];
|
||||
country = json['country'];
|
||||
}
|
||||
String? address;
|
||||
String? city;
|
||||
String? state;
|
||||
String? zipCode;
|
||||
String? country;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['address'] = address;
|
||||
map['city'] = city;
|
||||
map['state'] = state;
|
||||
map['zip_code'] = zipCode;
|
||||
map['country'] = country;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
extension PartyListExt on List<Party> {
|
||||
List<Party> getTopFiveCustomers() {
|
||||
final _customerTypes = {'customer', 'dealer', 'wholesaler', 'retailer'};
|
||||
|
||||
final _customers = where((p) => _customerTypes.contains(p.type?.trim().toLowerCase())).toList();
|
||||
|
||||
if (_customers.isEmpty) return const <Party>[];
|
||||
|
||||
final _hasSaleAmount = _customers.any((p) => (p.totalSaleAmount ?? 0) > 0);
|
||||
|
||||
final _filteredList = _customers.where((p) {
|
||||
if (_hasSaleAmount) {
|
||||
return (p.totalSaleAmount ?? 0) > 0;
|
||||
}
|
||||
|
||||
return (p.saleCount ?? 0) > 0;
|
||||
}).toList();
|
||||
|
||||
if (_filteredList.isEmpty) return const <Party>[];
|
||||
|
||||
_filteredList.sort((a, b) {
|
||||
if (_hasSaleAmount) {
|
||||
return (b.totalSaleAmount ?? 0).compareTo(a.totalSaleAmount ?? 0);
|
||||
}
|
||||
|
||||
return (b.saleCount ?? 0).compareTo(a.saleCount ?? 0);
|
||||
});
|
||||
|
||||
return _filteredList.length > 5 ? _filteredList.sublist(0, 5) : _filteredList;
|
||||
}
|
||||
|
||||
List<Party> getTopFiveSuppliers() {
|
||||
final _suppliers = where((p) => p.type?.trim().toLowerCase() == 'supplier').toList();
|
||||
|
||||
if (_suppliers.isEmpty) return const <Party>[];
|
||||
|
||||
final _hasPurchaseAmount = _suppliers.any((p) => (p.totalPurchaseAmount ?? 0) > 0);
|
||||
|
||||
final _filteredList = _suppliers.where((p) {
|
||||
if (_hasPurchaseAmount) {
|
||||
return (p.totalPurchaseAmount ?? 0) > 0;
|
||||
}
|
||||
|
||||
return (p.purchaseCount ?? 0) > 0;
|
||||
}).toList();
|
||||
|
||||
if (_filteredList.isEmpty) return const <Party>[];
|
||||
|
||||
_filteredList.sort((a, b) {
|
||||
if (_hasPurchaseAmount) {
|
||||
return (b.totalPurchaseAmount ?? 0).compareTo(a.totalPurchaseAmount ?? 0);
|
||||
}
|
||||
|
||||
return (b.purchaseCount ?? 0).compareTo(a.purchaseCount ?? 0);
|
||||
});
|
||||
|
||||
return _filteredList.length > 5 ? _filteredList.sublist(0, 5) : _filteredList;
|
||||
}
|
||||
}
|
||||
7
lib/Screens/Customers/Provider/customer_provider.dart
Normal file
7
lib/Screens/Customers/Provider/customer_provider.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
|
||||
|
||||
import '../Repo/parties_repo.dart';
|
||||
|
||||
PartyRepository partiesRepo = PartyRepository();
|
||||
final partiesProvider = FutureProvider.autoDispose<List<Party>>((ref) => partiesRepo.fetchAllParties());
|
||||
268
lib/Screens/Customers/Repo/parties_repo.dart
Normal file
268
lib/Screens/Customers/Repo/parties_repo.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
//ignore_for_file: avoid_print,unused_local_variable
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../Model/parties_model.dart';
|
||||
import '../Provider/customer_provider.dart';
|
||||
import '../add_customer.dart';
|
||||
|
||||
class PartyRepository {
|
||||
Future<List<Party>> fetchAllParties() async {
|
||||
final uri = Uri.parse('${APIConfig.url}/parties');
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final partyList = parsedData['data'] as List<dynamic>;
|
||||
return partyList.map((category) => Party.fromJson(category)).toList();
|
||||
// Parse into Party objects
|
||||
} else {
|
||||
throw Exception('Failed to fetch parties');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addParty({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required Customer customer,
|
||||
}) async {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
final uri = Uri.parse('${APIConfig.url}/parties');
|
||||
|
||||
var request = http.MultipartRequest('POST', uri)
|
||||
..headers['Accept'] = 'application/json'
|
||||
..headers['Authorization'] = await getAuthToken();
|
||||
|
||||
void addField(String key, String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
request.fields[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
addField('name', customer.name);
|
||||
addField('phone', customer.phone);
|
||||
addField('type', customer.customerType);
|
||||
addField('email', customer.email);
|
||||
addField('address', customer.address);
|
||||
addField('opening_balance_type', customer.openingBalanceType);
|
||||
addField('opening_balance', customer.openingBalance?.toString());
|
||||
addField('credit_limit', customer.creditLimit?.toString());
|
||||
|
||||
// Send billing and shipping address fields directly
|
||||
addField('billing_address[address]', customer.billingAddress);
|
||||
addField('billing_address[city]', customer.billingCity);
|
||||
addField('billing_address[state]', customer.billingState);
|
||||
addField('billing_address[zip_code]', customer.billingZipcode);
|
||||
addField('billing_address[country]', customer.billingCountry);
|
||||
|
||||
addField('shipping_address[address]', customer.shippingAddress);
|
||||
addField('shipping_address[city]', customer.shippingCity);
|
||||
addField('shipping_address[state]', customer.shippingState);
|
||||
addField('shipping_address[zip_code]', customer.shippingZipcode);
|
||||
addField('shipping_address[country]', customer.shippingCountry);
|
||||
|
||||
print('Party Data: ${request.fields}');
|
||||
|
||||
final response = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
fileFieldName: 'image',
|
||||
file: customer.image,
|
||||
fields: request.fields,
|
||||
);
|
||||
|
||||
final responseData = await response.stream.bytesToString();
|
||||
print('${responseData}');
|
||||
final parsedData = jsonDecode(responseData);
|
||||
print('Party Added Response: $parsedData');
|
||||
request.fields.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successfully!')));
|
||||
ref.refresh(partiesProvider); // Refresh party list
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Party creation failed: ${parsedData['message']}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateParty({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required Customer customer,
|
||||
}) async {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
final uri = Uri.parse('${APIConfig.url}/parties/${customer.id}');
|
||||
|
||||
var request = http.MultipartRequest('POST', uri)
|
||||
..headers['Accept'] = 'application/json'
|
||||
..headers['Authorization'] = await getAuthToken();
|
||||
|
||||
void addField(String key, String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
request.fields[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
request.fields['_method'] = 'put';
|
||||
addField('name', customer.name);
|
||||
addField('phone', customer.phone);
|
||||
addField('type', customer.customerType);
|
||||
addField('email', customer.email);
|
||||
addField('address', customer.address);
|
||||
addField('opening_balance_type', customer.openingBalanceType);
|
||||
addField('opening_balance', customer.openingBalance?.toString());
|
||||
addField('credit_limit', customer.creditLimit?.toString());
|
||||
|
||||
// Send billing and shipping address fields directly
|
||||
addField('billing_address[address]', customer.billingAddress);
|
||||
addField('billing_address[city]', customer.billingCity);
|
||||
addField('billing_address[state]', customer.billingState);
|
||||
addField('billing_address[zip_code]', customer.billingZipcode);
|
||||
addField('billing_address[country]', customer.billingCountry);
|
||||
|
||||
addField('shipping_address[address]', customer.shippingAddress);
|
||||
addField('shipping_address[city]', customer.shippingCity);
|
||||
addField('shipping_address[state]', customer.shippingState);
|
||||
addField('shipping_address[zip_code]', customer.shippingZipcode);
|
||||
addField('shipping_address[country]', customer.shippingCountry);
|
||||
|
||||
if (customer.image != null) {
|
||||
request.files.add(await http.MultipartFile.fromPath('image', customer.image!.path));
|
||||
}
|
||||
|
||||
final response = await customHttpClient.uploadFile(
|
||||
url: uri,
|
||||
fileFieldName: 'image',
|
||||
file: customer.image,
|
||||
fields: request.fields,
|
||||
);
|
||||
|
||||
final responseData = await response.stream.bytesToString();
|
||||
final parsedData = jsonDecode(responseData);
|
||||
print('--- Sending Party Data ---');
|
||||
request.fields.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
if (customer.image != null) {
|
||||
print('Image path: ${customer.image!.path}');
|
||||
} else {
|
||||
print('No image selected');
|
||||
}
|
||||
print('---------------------------');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successfully!')));
|
||||
ref.refresh(partiesProvider); // Refresh party list
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Party creation failed: ${parsedData['message']}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Future<void> updateParty({
|
||||
// required String id,
|
||||
// required WidgetRef ref,
|
||||
// required BuildContext context,
|
||||
// required String name,
|
||||
// required String phone,
|
||||
// required String type,
|
||||
// File? image,
|
||||
// String? email,
|
||||
// String? address,
|
||||
// String? due,
|
||||
// }) async {
|
||||
// final uri = Uri.parse('${APIConfig.url}/parties/$id');
|
||||
// CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
//
|
||||
// var request = http.MultipartRequest('POST', uri)
|
||||
// ..headers['Accept'] = 'application/json'
|
||||
// ..headers['Authorization'] = await getAuthToken();
|
||||
//
|
||||
// request.fields['_method'] = 'put';
|
||||
// request.fields['name'] = name;
|
||||
// request.fields['phone'] = phone;
|
||||
// request.fields['type'] = type;
|
||||
// if (email != null) request.fields['email'] = email;
|
||||
// if (address != null) request.fields['address'] = address;
|
||||
// if (due != null) request.fields['due'] = due; // Convert due to string
|
||||
// if (image != null) {
|
||||
// request.files.add(http.MultipartFile.fromBytes('image', image.readAsBytesSync(), filename: image.path));
|
||||
// }
|
||||
//
|
||||
// // final response = await request.send();
|
||||
// final response = await customHttpClient.uploadFile(url: uri, fields: request.fields, file: image, fileFieldName: 'image');
|
||||
// final responseData = await response.stream.bytesToString();
|
||||
//
|
||||
// final parsedData = jsonDecode(responseData);
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Updated Successfully!')));
|
||||
// var data1 = ref.refresh(partiesProvider);
|
||||
//
|
||||
// Navigator.pop(context);
|
||||
// Navigator.pop(context);
|
||||
// } else {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Party Update failed: ${parsedData['message']}')));
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> deleteParty({
|
||||
required String id,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
final String apiUrl = '${APIConfig.url}/parties/$id';
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(
|
||||
url: Uri.parse(apiUrl),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Party deleted successfully')));
|
||||
|
||||
var data1 = ref.refresh(partiesProvider);
|
||||
|
||||
Navigator.pop(context); // Assuming you want to close the screen after deletion
|
||||
// Navigator.pop(context); // Assuming you want to close the screen after deletion
|
||||
} else {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to delete party: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendCustomerUdeSms({required num id, required BuildContext context}) async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/parties/$id');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
EasyLoading.dismiss();
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(jsonDecode(response.body)['message'])));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: ${jsonDecode((response.body))['message']}')));
|
||||
}
|
||||
}
|
||||
}
|
||||
981
lib/Screens/Customers/add_customer.dart
Normal file
981
lib/Screens/Customers/add_customer.dart
Normal file
@@ -0,0 +1,981 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl_phone_field/intl_phone_field.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../model/country_model.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import 'Provider/customer_provider.dart';
|
||||
import 'Repo/parties_repo.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
|
||||
|
||||
class AddParty extends StatefulWidget {
|
||||
const AddParty({super.key, this.customerModel});
|
||||
final Party? customerModel;
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_AddPartyState createState() => _AddPartyState();
|
||||
}
|
||||
|
||||
class _AddPartyState extends State<AddParty> {
|
||||
String groupValue = 'Retailer';
|
||||
String advanced = 'advance';
|
||||
String due = 'due';
|
||||
String openingBalanceType = 'due';
|
||||
bool expanded = false;
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
bool showProgress = false;
|
||||
XFile? pickedImage;
|
||||
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController nameController = TextEditingController();
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController addressController = TextEditingController();
|
||||
final creditLimitController = TextEditingController();
|
||||
final billingAddressController = TextEditingController();
|
||||
final billingCityController = TextEditingController();
|
||||
final billingStateController = TextEditingController();
|
||||
final shippingAddressController = TextEditingController();
|
||||
final shippingCityController = TextEditingController();
|
||||
final shippingStateController = TextEditingController();
|
||||
final billingZipCodeCountryController = TextEditingController();
|
||||
final shippingZipCodeCountryController = TextEditingController();
|
||||
final openingBalanceController = TextEditingController();
|
||||
|
||||
final GlobalKey<FormState> _formKay = GlobalKey();
|
||||
FocusNode focusNode = FocusNode();
|
||||
|
||||
List<Country> _countries = [];
|
||||
Country? _selectedBillingCountry;
|
||||
Country? _selectedShippingCountry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadCountries();
|
||||
}
|
||||
|
||||
void _initializeFields() {
|
||||
final party = widget.customerModel;
|
||||
if (party != null) {
|
||||
nameController.text = party.name ?? '';
|
||||
emailController.text = party.email ?? '';
|
||||
addressController.text = party.address ?? '';
|
||||
// dueController.text = party.due?.toString() ?? '';
|
||||
creditLimitController.text = party.creditLimit?.toString() ?? '';
|
||||
openingBalanceController.text = party.openingBalance?.toString() ?? '';
|
||||
openingBalanceType = party.openingBalanceType ?? 'due';
|
||||
groupValue = party.type ?? 'Retailer';
|
||||
phoneController.text = party.phone ?? '';
|
||||
|
||||
// Initialize billing address fields
|
||||
billingAddressController.text = party.billingAddress?.address ?? '';
|
||||
billingCityController.text = party.billingAddress?.city ?? '';
|
||||
billingStateController.text = party.billingAddress?.state ?? '';
|
||||
billingZipCodeCountryController.text = party.billingAddress?.zipCode ?? '';
|
||||
if (party.billingAddress?.country != null) {
|
||||
_selectedBillingCountry = _countries.firstWhere(
|
||||
(c) => c.name == party.billingAddress!.country,
|
||||
);
|
||||
}
|
||||
shippingAddressController.text = party.shippingAddress?.address ?? '';
|
||||
shippingCityController.text = party.shippingAddress?.city ?? '';
|
||||
shippingStateController.text = party.shippingAddress?.state ?? '';
|
||||
shippingZipCodeCountryController.text = party.shippingAddress?.zipCode ?? '';
|
||||
if (party.shippingAddress?.country != null) {
|
||||
_selectedShippingCountry = _countries.firstWhere(
|
||||
(c) => c.name == party.shippingAddress!.country,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCountries() async {
|
||||
try {
|
||||
final String response = await rootBundle.loadString('assets/countrylist.json');
|
||||
final List<dynamic> data = json.decode(response);
|
||||
setState(() {
|
||||
_countries = data.map((json) => Country.fromJson(json)).toList();
|
||||
});
|
||||
|
||||
// Now that countries are loaded, initialize fields
|
||||
_initializeFields();
|
||||
} catch (e) {
|
||||
print('Error loading countries: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Consumer(builder: (context, ref, __) {
|
||||
final providerData = ref.watch(partiesProvider);
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
bool isReadOnly = (widget.customerModel?.branchId != businessInfo.value?.data?.user?.activeBranchId) &&
|
||||
widget.customerModel != null;
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
surfaceTintColor: kWhite,
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).addParty,
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
)),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKay,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return lang.S.of(context).pleaseEnterAValidPhoneNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).phone,
|
||||
hintText: lang.S.of(context).enterYourPhoneNumber,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
///_________Name_______________________
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
// return 'Please enter a valid Name';
|
||||
return lang.S.of(context).pleaseEnterAValidName;
|
||||
}
|
||||
// You can add more validation logic as needed
|
||||
return null;
|
||||
},
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).name,
|
||||
hintText: lang.S.of(context).enterYourName,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
|
||||
///_________opening balance_______________________
|
||||
///
|
||||
TextFormField(
|
||||
controller: openingBalanceController,
|
||||
// 2. Use the variable here
|
||||
readOnly: isReadOnly,
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).balance,
|
||||
hintText: lang.S.of(context).enterOpeningBalance,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF7F7F7),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(4),
|
||||
bottomRight: Radius.circular(4),
|
||||
)),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
// Optional: Change icon color if disabled
|
||||
color: isReadOnly ? Colors.grey : kPeraColor,
|
||||
),
|
||||
// items: ['Advance', 'Due'].map((entry) {
|
||||
// final valueToStore = entry.toLowerCase();
|
||||
// return DropdownMenuItem<String>(
|
||||
// value: valueToStore,
|
||||
// child: Text(
|
||||
// entry,
|
||||
// style: theme.textTheme.bodyLarge?.copyWith(color: kTitleColor),
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
items: [
|
||||
DropdownMenuItem<String>(
|
||||
value: advanced,
|
||||
child: Text(
|
||||
lang.S.of(context).advance,
|
||||
),
|
||||
),
|
||||
DropdownMenuItem<String>(
|
||||
value: due,
|
||||
child: Text(
|
||||
lang.S.of(context).due,
|
||||
),
|
||||
),
|
||||
],
|
||||
value: openingBalanceType,
|
||||
// 3. LOGIC APPLIED HERE:
|
||||
// If isReadOnly is true, set onChanged to null (disables it).
|
||||
// If false, allow the function to run.
|
||||
onChanged: isReadOnly
|
||||
? null
|
||||
: (String? value) {
|
||||
setState(() {
|
||||
openingBalanceType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// TextFormField(
|
||||
// controller: openingBalanceController,
|
||||
// keyboardType: TextInputType.name,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: lang.S.of(context).balance,
|
||||
// hintText: lang.S.of(context).enterOpeningBalance,
|
||||
// suffixIcon: Padding(
|
||||
// padding: const EdgeInsets.all(1.0),
|
||||
// child: Container(
|
||||
// padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Color(0xffF7F7F7),
|
||||
// borderRadius: BorderRadius.only(
|
||||
// topRight: Radius.circular(4),
|
||||
// bottomRight: Radius.circular(4),
|
||||
// )),
|
||||
// child: DropdownButtonHideUnderline(
|
||||
// child: DropdownButton<String>(
|
||||
// icon: Icon(
|
||||
// Icons.keyboard_arrow_down,
|
||||
// color: kPeraColor,
|
||||
// ),
|
||||
// items: ['Advance', 'Due'].map((entry) {
|
||||
// final valueToStore = entry.toLowerCase(); // 'advanced', 'due'
|
||||
// return DropdownMenuItem<String>(
|
||||
// value: valueToStore,
|
||||
// child: Text(
|
||||
// entry, // show capitalized
|
||||
// style: theme.textTheme.bodyLarge?.copyWith(color: kTitleColor),
|
||||
// ),
|
||||
// );
|
||||
// }).toList(),
|
||||
// value: openingBalanceType,
|
||||
// onChanged: (String? value) {
|
||||
// setState(() {
|
||||
// openingBalanceType = value!;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
SizedBox(height: 20),
|
||||
|
||||
///_______Type___________________________
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
fillColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return kMainColor;
|
||||
}
|
||||
return kPeraColor;
|
||||
},
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
groupValue: groupValue,
|
||||
title: Text(
|
||||
lang.S.of(context).customer,
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
value: 'Retailer',
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
groupValue = value.toString();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RadioListTile(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
fillColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return kMainColor;
|
||||
}
|
||||
return kPeraColor;
|
||||
},
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
groupValue: groupValue,
|
||||
title: Text(
|
||||
lang.S.of(context).dealer,
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
value: 'Dealer',
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
groupValue = value.toString();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RadioListTile(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
fillColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return kMainColor;
|
||||
}
|
||||
return kPeraColor;
|
||||
},
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
activeColor: kMainColor,
|
||||
groupValue: groupValue,
|
||||
title: Text(
|
||||
lang.S.of(context).wholesaler,
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
value: 'Wholesaler',
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
groupValue = value.toString();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RadioListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
activeColor: kMainColor,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
fillColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return kMainColor;
|
||||
}
|
||||
return kPeraColor;
|
||||
},
|
||||
),
|
||||
groupValue: groupValue,
|
||||
title: Text(
|
||||
lang.S.of(context).supplier,
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
value: 'Supplier',
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
groupValue = value.toString();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Visibility(
|
||||
visible: showProgress,
|
||||
child: const CircularProgressIndicator(
|
||||
color: kMainColor,
|
||||
strokeWidth: 5.0,
|
||||
),
|
||||
),
|
||||
ExpansionPanelList(
|
||||
expandIconColor: Colors.transparent,
|
||||
expandedHeaderPadding: EdgeInsets.zero,
|
||||
expansionCallback: (int index, bool isExpanded) {
|
||||
setState(() {
|
||||
expanded == false ? expanded = true : expanded = false;
|
||||
});
|
||||
},
|
||||
animationDuration: const Duration(milliseconds: 500),
|
||||
elevation: 0,
|
||||
dividerColor: Colors.white,
|
||||
children: [
|
||||
ExpansionPanel(
|
||||
backgroundColor: kWhite,
|
||||
headerBuilder: (BuildContext context, bool isExpanded) {
|
||||
return TextButton.icon(
|
||||
style: ButtonStyle(
|
||||
alignment: Alignment.center,
|
||||
backgroundColor: WidgetStateColor.transparent,
|
||||
overlayColor: WidgetStateColor.transparent,
|
||||
surfaceTintColor: WidgetStateColor.transparent,
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.only(left: 70),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
expanded == false ? expanded = true : expanded = false;
|
||||
});
|
||||
},
|
||||
label: Text(
|
||||
lang.S.of(context).moreInfo,
|
||||
style: theme.textTheme.titleSmall?.copyWith(color: Colors.red),
|
||||
),
|
||||
icon: Icon(Icons.keyboard_arrow_down_outlined),
|
||||
iconAlignment: IconAlignment.end,
|
||||
);
|
||||
},
|
||||
body: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: kWhite,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
// ignore: sized_box_for_whitespace
|
||||
child: Container(
|
||||
height: 200.0,
|
||||
width: MediaQuery.of(context).size.width - 80,
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
pickedImage = await _picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.photo_library_rounded,
|
||||
size: 60.0,
|
||||
color: kMainColor,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).gallery,
|
||||
//'Gallery',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 40.0,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
pickedImage = await _picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.camera,
|
||||
size: 60.0,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).camera,
|
||||
//'Camera',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 120,
|
||||
width: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: pickedImage == null
|
||||
? const DecorationImage(
|
||||
image: AssetImage('images/no_shop_image.png'),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: DecorationImage(
|
||||
image: FileImage(File(pickedImage!.path)),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 35,
|
||||
width: 35,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(120)),
|
||||
color: kMainColor,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.camera_alt_outlined,
|
||||
size: 20,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///__________email__________________________
|
||||
TextFormField(
|
||||
controller: emailController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).email,
|
||||
//hintText: 'Enter your email address',
|
||||
hintText: lang.S.of(context).hintEmail),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: addressController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).address,
|
||||
//hintText: 'Enter your address'
|
||||
hintText: lang.S.of(context).hintEmail),
|
||||
),
|
||||
// SizedBox(height: 20),
|
||||
// TextFormField(
|
||||
// controller: dueController,
|
||||
// inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
// keyboardType: TextInputType.number,
|
||||
// decoration: InputDecoration(
|
||||
// border: const OutlineInputBorder(),
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).previousDue,
|
||||
// hintText: lang.S.of(context).amount,
|
||||
// ),
|
||||
// ),
|
||||
SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: creditLimitController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).creditLimit,
|
||||
hintText: 'Ex: 800'),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
collapsedIconColor: kGreyTextColor,
|
||||
visualDensity: VisualDensity(vertical: -2, horizontal: -4),
|
||||
tilePadding: EdgeInsets.zero,
|
||||
trailing: SizedBox.shrink(),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
FeatherIcons.plus,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).billingAddress,
|
||||
style: theme.textTheme.titleMedium,
|
||||
)
|
||||
],
|
||||
),
|
||||
children: [
|
||||
SizedBox(height: 10),
|
||||
//___________Billing Address________________
|
||||
TextFormField(
|
||||
controller: billingAddressController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).address,
|
||||
hintText: lang.S.of(context).enterAddress,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
//--------------billing city------------------------
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: billingCityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).city,
|
||||
hintText: lang.S.of(context).cityName,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: billingStateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).state,
|
||||
hintText: lang.S.of(context).stateName,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
//--------------billing state------------------------
|
||||
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
//--------------billing zip code------------------------
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: billingZipCodeCountryController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).zip,
|
||||
hintText: lang.S.of(context).zipCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
//--------------billing country------------------------
|
||||
Flexible(
|
||||
child: DropdownButtonFormField<Country>(
|
||||
value: _selectedBillingCountry,
|
||||
hint: Text(lang.S.of(context).chooseCountry),
|
||||
onChanged: (Country? newValue) {
|
||||
setState(() {
|
||||
_selectedBillingCountry = newValue;
|
||||
});
|
||||
if (newValue != null) {
|
||||
print('Selected: ${newValue.name} (${newValue.code})');
|
||||
}
|
||||
},
|
||||
items: _countries.map<DropdownMenuItem<Country>>((Country country) {
|
||||
return DropdownMenuItem<Country>(
|
||||
value: country,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(country.emoji),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
country.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
isExpanded: true,
|
||||
dropdownColor: Colors.white,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
labelText: lang.S.of(context).country,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dividerColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
collapsedIconColor: kGreyTextColor,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -2),
|
||||
trailing: SizedBox.shrink(),
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(FeatherIcons.plus, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).shippingAddress,
|
||||
style: theme.textTheme.titleMedium,
|
||||
)
|
||||
],
|
||||
),
|
||||
children: [
|
||||
SizedBox(height: 10),
|
||||
//___________Billing Address________________
|
||||
TextFormField(
|
||||
controller: shippingAddressController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).address,
|
||||
hintText: lang.S.of(context).enterAddress,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
//--------------billing city------------------------
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: shippingCityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).city,
|
||||
hintText: lang.S.of(context).cityName,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: shippingStateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).state,
|
||||
hintText: lang.S.of(context).stateName,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
//--------------billing state------------------------
|
||||
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
//--------------billing zip code------------------------
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: shippingZipCodeCountryController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).zip,
|
||||
hintText: lang.S.of(context).zipCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
//--------------billing country------------------------
|
||||
Flexible(
|
||||
child: DropdownButtonFormField<Country>(
|
||||
value: _selectedShippingCountry,
|
||||
hint: Text(lang.S.of(context).chooseCountry),
|
||||
onChanged: (Country? newValue) {
|
||||
setState(() {
|
||||
_selectedShippingCountry = newValue;
|
||||
});
|
||||
if (newValue != null) {
|
||||
print('Selected: ${newValue.name} (${newValue.code})');
|
||||
}
|
||||
},
|
||||
items: _countries.map<DropdownMenuItem<Country>>((Country country) {
|
||||
return DropdownMenuItem<Country>(
|
||||
value: country,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(country.emoji),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
country.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
isExpanded: true,
|
||||
dropdownColor: Colors.white,
|
||||
decoration: kInputDecoration.copyWith(
|
||||
labelText: lang.S.of(context).country,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
isExpanded: expanded,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (!permissionService.hasPermission(Permit.partiesCreate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).partyCreateWarn),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
num parseOrZero(String? input) {
|
||||
if (input == null || input.isEmpty) return 0;
|
||||
return num.tryParse(input) ?? 0;
|
||||
}
|
||||
|
||||
Customer customer = Customer(
|
||||
id: widget.customerModel?.id.toString() ?? '',
|
||||
name: nameController.text,
|
||||
phone: phoneController.text ?? '',
|
||||
customerType: groupValue,
|
||||
image: pickedImage != null ? File(pickedImage!.path) : null,
|
||||
email: emailController.text,
|
||||
address: addressController.text,
|
||||
openingBalanceType: openingBalanceType.toString(),
|
||||
openingBalance: parseOrZero(openingBalanceController.text),
|
||||
creditLimit: parseOrZero(creditLimitController.text),
|
||||
billingAddress: billingAddressController.text,
|
||||
billingCity: billingCityController.text,
|
||||
billingState: billingStateController.text,
|
||||
billingZipcode: billingZipCodeCountryController.text,
|
||||
billingCountry: _selectedBillingCountry?.name.toString() ?? '',
|
||||
shippingAddress: shippingAddressController.text,
|
||||
shippingCity: shippingCityController.text,
|
||||
shippingState: shippingStateController.text,
|
||||
shippingZipcode: shippingZipCodeCountryController.text,
|
||||
shippingCountry: _selectedShippingCountry?.name.toString() ?? '',
|
||||
);
|
||||
|
||||
final partyRepo = PartyRepository();
|
||||
if (widget.customerModel == null) {
|
||||
// Add new
|
||||
await partyRepo.addParty(
|
||||
ref: ref,
|
||||
context: context,
|
||||
customer: customer,
|
||||
);
|
||||
} else {
|
||||
await partyRepo.updateParty(
|
||||
ref: ref,
|
||||
context: context,
|
||||
customer: customer,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).save),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Customer {
|
||||
String? id;
|
||||
String name;
|
||||
String? phone;
|
||||
String? customerType;
|
||||
File? image;
|
||||
String? email;
|
||||
String? address;
|
||||
String? openingBalanceType;
|
||||
num? openingBalance;
|
||||
num? creditLimit;
|
||||
String? billingAddress;
|
||||
String? billingCity;
|
||||
String? billingState;
|
||||
String? billingZipcode;
|
||||
String? billingCountry;
|
||||
String? shippingAddress;
|
||||
String? shippingCity;
|
||||
String? shippingState;
|
||||
String? shippingZipcode;
|
||||
String? shippingCountry;
|
||||
|
||||
Customer({
|
||||
this.id,
|
||||
required this.name,
|
||||
this.phone,
|
||||
this.customerType,
|
||||
this.image,
|
||||
this.email,
|
||||
this.address,
|
||||
this.openingBalanceType,
|
||||
this.openingBalance,
|
||||
this.creditLimit,
|
||||
this.billingAddress,
|
||||
this.billingCity,
|
||||
this.billingState,
|
||||
this.billingZipcode,
|
||||
this.billingCountry,
|
||||
this.shippingAddress,
|
||||
this.shippingCity,
|
||||
this.shippingState,
|
||||
this.shippingZipcode,
|
||||
this.shippingCountry,
|
||||
});
|
||||
}
|
||||
721
lib/Screens/Customers/customer_details.dart
Normal file
721
lib/Screens/Customers/customer_details.dart
Normal file
@@ -0,0 +1,721 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/GlobalComponents/url_lanuncer.dart';
|
||||
import 'package:mobile_pos/Provider/transactions_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/edit_customer.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/sms_sent_confirmation.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/widgets/empty_widget/_empty_widget.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../GlobalComponents/sales_transaction_widget.dart';
|
||||
import '../../PDF Invoice/purchase_invoice_pdf.dart';
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
import '../../thermal priting invoices/model/print_transaction_model.dart';
|
||||
import '../../thermal priting invoices/provider/print_thermal_invoice_provider.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../invoice_details/purchase_invoice_details.dart';
|
||||
import 'Model/parties_model.dart';
|
||||
import 'Repo/parties_repo.dart';
|
||||
import 'add_customer.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class CustomerDetails extends ConsumerStatefulWidget {
|
||||
CustomerDetails({super.key, required this.party});
|
||||
|
||||
Party party;
|
||||
|
||||
@override
|
||||
ConsumerState<CustomerDetails> createState() => _CustomerDetailsState();
|
||||
}
|
||||
|
||||
class _CustomerDetailsState extends ConsumerState<CustomerDetails> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<void> showDeleteConfirmationAlert({
|
||||
required BuildContext context,
|
||||
required String id,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context1) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
lang.S.of(context).confirmPassword,
|
||||
//'Confirm Delete'
|
||||
),
|
||||
content: Text(
|
||||
lang.S.of(context).areYouSureYouWant,
|
||||
//'Are you sure you want to delete this party?'
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
lang.S.of(context).cancel,
|
||||
//'Cancel'
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
final party = PartyRepository();
|
||||
await party.deleteParty(id: id, context: context, ref: ref);
|
||||
},
|
||||
child: Text(lang.S.of(context).delete,
|
||||
// 'Delete',
|
||||
style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int selectedIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(builder: (context, cRef, __) {
|
||||
final providerData = cRef.watch(salesTransactionProvider);
|
||||
final purchaseList = cRef.watch(purchaseTransactionProvider);
|
||||
final printerData = cRef.watch(thermalPrinterProvider);
|
||||
final businessInfo = cRef.watch(businessInfoProvider);
|
||||
final permissionService = PermissionService(cRef);
|
||||
final _theme = Theme.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
surfaceTintColor: kWhite,
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
widget.party.type != 'Supplier' ? lang.S.of(context).CustomerDetails : lang.S.of(context).supplierDetails,
|
||||
),
|
||||
actions: [
|
||||
businessInfo.when(data: (details) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!permissionService.hasPermission(Permit.partiesUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).updatePartyWarn),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
AddParty(customerModel: widget.party).launch(context);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
FeatherIcons.edit2,
|
||||
color: Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!permissionService.hasPermission(Permit.partiesDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).deletePartyWarn),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
await showDeleteConfirmationAlert(
|
||||
context: context, id: widget.party.id.toString(), ref: cRef);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
FeatherIcons.trash2,
|
||||
color: Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
})
|
||||
],
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (permissionService.hasPermission(Permit.partiesRead.value)) ...{
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
widget.party.image == null
|
||||
? Center(
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _theme.colorScheme.primary,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
(widget.party.name != null && widget.party.name!.length >= 2)
|
||||
? widget.party.name!.substring(0, 2)
|
||||
: (widget.party.name != null ? widget.party.name! : ''),
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 21,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: widget.party.image == null
|
||||
? const DecorationImage(
|
||||
image: AssetImage('images/no_shop_image.png'),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: DecorationImage(
|
||||
image: NetworkImage('${APIConfig.domain}${widget.party.image!}'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
// 'Personal Info:',
|
||||
lang.S.of(context).personalInfo,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
...{
|
||||
lang.S.of(context).name: widget.party.name,
|
||||
lang.S.of(context).type: widget.party.type,
|
||||
lang.S.of(context).phoneNumber: widget.party.phone,
|
||||
lang.S.of(context).email: widget.party.email ?? "n/a",
|
||||
lang.S.of(context).dueBalance: "$currency${(widget.party.due ?? "0")}",
|
||||
lang.S.of(context).walletBalance: "$currency${(widget.party.wallet ?? "0")}",
|
||||
lang.S.of(context).address: widget.party.address ?? "n/a",
|
||||
// "Party Credit Limit": widget.party.creditLimit ?? "0",
|
||||
// "Party GST": widget.party.creditLimit ?? "0",
|
||||
}.entries.map((entry) {
|
||||
return keyValueWidget(title: entry.key, value: entry.value.toString(), context: context);
|
||||
}),
|
||||
SizedBox(height: 19),
|
||||
Text(
|
||||
// 'Billing Address:',
|
||||
lang.S.of(context).billingAddress,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
() {
|
||||
final parts = [
|
||||
widget.party.billingAddress?.address,
|
||||
widget.party.billingAddress?.city,
|
||||
widget.party.billingAddress?.state,
|
||||
widget.party.billingAddress?.zipCode,
|
||||
widget.party.billingAddress?.country,
|
||||
].where((part) => part != null && part.isNotEmpty).toList();
|
||||
|
||||
return parts.isEmpty ? 'n/a' : parts.join(', ');
|
||||
}(),
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
// 'Shipping Address:',
|
||||
lang.S.of(context).shippingAddress,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
() {
|
||||
final parts = [
|
||||
widget.party.shippingAddress?.address,
|
||||
widget.party.shippingAddress?.city,
|
||||
widget.party.shippingAddress?.state,
|
||||
widget.party.shippingAddress?.zipCode,
|
||||
widget.party.shippingAddress?.country,
|
||||
].where((part) => part != null && part.isNotEmpty).toList();
|
||||
|
||||
return parts.isEmpty ? 'n/a' : parts.join(', ');
|
||||
}(),
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: DAppColors.kDividerColor,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
lang.S.of(context).recentTransaction,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// const SizedBox(height: 8),
|
||||
widget.party.type != 'Supplier'
|
||||
? providerData.when(data: (transaction) {
|
||||
final filteredTransactions =
|
||||
transaction.where((t) => t.party?.id == widget.party.id).toList();
|
||||
return filteredTransactions.isNotEmpty
|
||||
? ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredTransactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final currentTransaction = filteredTransactions[index];
|
||||
return salesTransactionWidget(
|
||||
context: context,
|
||||
ref: cRef,
|
||||
businessInfo: businessInfo.value!,
|
||||
sale: currentTransaction,
|
||||
advancePermission: true,
|
||||
showProductQTY: true,
|
||||
);
|
||||
},
|
||||
)
|
||||
: EmptyWidget(
|
||||
message: TextSpan(text: lang.S.of(context).noTransaction),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
})
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: purchaseList.when(data: (pTransaction) {
|
||||
final filteredTransactions =
|
||||
pTransaction.where((t) => t.party?.id == widget.party.id).toList();
|
||||
|
||||
return filteredTransactions.isNotEmpty
|
||||
? ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: filteredTransactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final currentTransaction = filteredTransactions[index];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
PurchaseInvoiceDetails(
|
||||
transitionModel: currentTransaction,
|
||||
businessInfo: businessInfo.value!,
|
||||
).launch(context);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: context.width(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${lang.S.of(context).totalProduct} : ${currentTransaction.details!.length.toString()}",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Text('#${currentTransaction.invoiceNumber}'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: currentTransaction.dueAmount! <= 0
|
||||
? const Color(0xff0dbf7d).withValues(alpha: 0.1)
|
||||
: const Color(0xFFED1A3B).withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(2)),
|
||||
),
|
||||
child: Text(
|
||||
currentTransaction.dueAmount! <= 0
|
||||
? lang.S.of(context).paid
|
||||
: lang.S.of(context).unPaid,
|
||||
style: TextStyle(
|
||||
color: currentTransaction.dueAmount! <= 0
|
||||
? const Color(0xff0dbf7d)
|
||||
: const Color(0xFFED1A3B),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(currentTransaction.purchaseDate!.substring(0, 10),
|
||||
style: _theme.textTheme.bodyMedium
|
||||
?.copyWith(color: DAppColors.kSecondary)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'${lang.S.of(context).total} : $currency${currentTransaction.totalAmount.toString()}',
|
||||
style: _theme.textTheme.bodyMedium
|
||||
?.copyWith(color: DAppColors.kSecondary),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${lang.S.of(context).due}: $currency${currentTransaction.dueAmount.toString()}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
businessInfo.when(data: (data) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
PrintPurchaseTransactionModel model =
|
||||
PrintPurchaseTransactionModel(
|
||||
purchaseTransitionModel: currentTransaction,
|
||||
personalInformationModel: data,
|
||||
);
|
||||
|
||||
await printerData.printPurchaseThermalInvoiceNow(
|
||||
transaction: model,
|
||||
productList: model.purchaseTransitionModel!.details,
|
||||
invoiceSize: businessInfo.value?.data?.invoiceSize,
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
FeatherIcons.printer,
|
||||
color: Colors.grey,
|
||||
),
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
businessInfo.when(data: (business) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
)),
|
||||
onPressed: () =>
|
||||
PurchaseInvoicePDF.generatePurchaseDocument(
|
||||
currentTransaction,
|
||||
data,
|
||||
context,
|
||||
showPreview: true,
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.picture_as_pdf,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
)),
|
||||
onPressed: () =>
|
||||
PurchaseInvoicePDF.generatePurchaseDocument(
|
||||
currentTransaction, data, context,
|
||||
isShare: true),
|
||||
icon: const Icon(
|
||||
Icons.share_outlined,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return Text(lang.S.of(context).loading);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 15,
|
||||
color: kBorderColor,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: EmptyWidget(
|
||||
message: TextSpan(text: lang.S.of(context).noTransaction),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}),
|
||||
),
|
||||
} else
|
||||
Center(child: PermitDenyWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
// bottomNavigationBar: ButtonGlobal(
|
||||
// iconWidget: null,
|
||||
// buttontext: lang.S.of(context).viewAll,
|
||||
// iconColor: Colors.white,
|
||||
// buttonDecoration: kButtonDecoration.copyWith(color: kMainColor),
|
||||
// onPressed: () {
|
||||
// Navigator.push(context, MaterialPageRoute(builder: (context)=>const CustomerAllTransactionScreen()));
|
||||
// },
|
||||
// ),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ContactOptionsRow extends StatefulWidget {
|
||||
final Party party;
|
||||
|
||||
const ContactOptionsRow({super.key, required this.party});
|
||||
|
||||
@override
|
||||
State<ContactOptionsRow> createState() => _ContactOptionsRowState();
|
||||
}
|
||||
|
||||
class _ContactOptionsRowState extends State<ContactOptionsRow> {
|
||||
int selectedIndex = -1;
|
||||
|
||||
void _onButtonTap(int index) async {
|
||||
setState(() {
|
||||
selectedIndex = index;
|
||||
});
|
||||
|
||||
if (index == 0) {
|
||||
// Call functionality
|
||||
if (widget.party.phone == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).phoneNotAvail)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final Uri url = Uri.parse('tel:${widget.party.phone}');
|
||||
bool t = await launchUrl(url);
|
||||
if (!t) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).notLaunch)),
|
||||
);
|
||||
}
|
||||
} else if (index == 1) {
|
||||
// SMS functionality
|
||||
if (widget.party.type != 'Supplier') {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context1) {
|
||||
return SmsConfirmationPopup(
|
||||
customerName: widget.party.name ?? '',
|
||||
phoneNumber: widget.party.phone ?? '',
|
||||
onCancel: () {
|
||||
Navigator.pop(context1);
|
||||
},
|
||||
onSendSms: () {
|
||||
UrlLauncher.handleLaunchURL(context, 'sms:${widget.party.phone}', false);
|
||||
// EasyLoading.show(status: 'SMS Sending..');
|
||||
// PartyRepository repo = PartyRepository();
|
||||
// await repo.sendCustomerUdeSms(id: widget.party.id!, context: context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (widget.party.phone == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).phoneNotAvail)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
UrlLauncher.handleLaunchURL(
|
||||
context,
|
||||
'sms:${widget.party.phone}',
|
||||
false,
|
||||
);
|
||||
}
|
||||
} else if (index == 2) {
|
||||
// Email functionality
|
||||
if (widget.party.email == null || !RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(widget.party.email!)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invalid email address.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
UrlLauncher.handleLaunchURL(context, 'mailto:${widget.party.email}', true);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildContactButton(int index, IconData icon, String label) {
|
||||
final _theme = Theme.of(context);
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _onButtonTap(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: selectedIndex == index ? kMainColor : kMainColor.withValues(alpha: 0.10),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: selectedIndex == index ? kWhite : Colors.black,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
color: selectedIndex == index ? kWhite : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildContactButton(0, FeatherIcons.phone, 'Call'),
|
||||
const SizedBox(width: 18),
|
||||
_buildContactButton(1, FeatherIcons.messageSquare, 'Message'),
|
||||
const SizedBox(width: 18),
|
||||
_buildContactButton(2, FeatherIcons.mail, 'Email'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget keyValueWidget({required String title, required String value, required BuildContext context}) {
|
||||
final _theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 3,
|
||||
child: Text(
|
||||
'$title ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: DAppColors.kNeutral700,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 4,
|
||||
child: Text(
|
||||
': $value',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
// fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
775
lib/Screens/Customers/edit_customer.dart
Normal file
775
lib/Screens/Customers/edit_customer.dart
Normal file
@@ -0,0 +1,775 @@
|
||||
// // ignore: import_of_legacy_library_into_null_safe
|
||||
// // ignore_for_file: unused_result
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
// import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
// import 'package:image_picker/image_picker.dart';
|
||||
// import 'package:mobile_pos/Const/api_config.dart';
|
||||
// import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
|
||||
// import 'package:mobile_pos/constant.dart';
|
||||
// import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// import 'package:nb_utils/nb_utils.dart';
|
||||
//
|
||||
// import '../../GlobalComponents/glonal_popup.dart';
|
||||
// import '../../http_client/custome_http_client.dart';
|
||||
// import '../User Roles/Provider/check_user_role_permission_provider.dart';
|
||||
// import 'Provider/customer_provider.dart';
|
||||
// import 'Repo/parties_repo.dart';
|
||||
//
|
||||
// // ignore: must_be_immutable
|
||||
// class EditCustomer extends StatefulWidget {
|
||||
// EditCustomer({super.key, required this.customerModel});
|
||||
//
|
||||
// Party customerModel;
|
||||
//
|
||||
// @override
|
||||
// // ignore: library_private_types_in_public_api
|
||||
// _EditCustomerState createState() => _EditCustomerState();
|
||||
// }
|
||||
//
|
||||
// class _EditCustomerState extends State<EditCustomer> {
|
||||
// String groupValue = '';
|
||||
// bool expanded = false;
|
||||
// final ImagePicker _picker = ImagePicker();
|
||||
// bool showProgress = false;
|
||||
// XFile? pickedImage;
|
||||
//
|
||||
// @override
|
||||
// void initState() {
|
||||
// phoneController.text = widget.customerModel.phone ?? '';
|
||||
// nameController.text = widget.customerModel.name ?? '';
|
||||
// emailController.text = widget.customerModel.email ?? '';
|
||||
// dueController.text = (widget.customerModel.due ?? 0).toString();
|
||||
// addressController.text = widget.customerModel.address ?? '';
|
||||
// groupValue = widget.customerModel.type ?? '';
|
||||
// super.initState();
|
||||
// }
|
||||
//
|
||||
// final GlobalKey<FormState> _formKay = GlobalKey();
|
||||
//
|
||||
// TextEditingController phoneController = TextEditingController();
|
||||
// TextEditingController nameController = TextEditingController();
|
||||
// TextEditingController emailController = TextEditingController();
|
||||
// TextEditingController dueController = TextEditingController();
|
||||
// TextEditingController addressController = TextEditingController();
|
||||
//
|
||||
// final partyCreditLimitController = TextEditingController();
|
||||
// final partyGstController = TextEditingController();
|
||||
// final billingAddressController = TextEditingController();
|
||||
// final billingCityController = TextEditingController();
|
||||
// final billingStateController = TextEditingController();
|
||||
// final billingCountryController = TextEditingController();
|
||||
// final shippingAddressController = TextEditingController();
|
||||
// final shippingCityController = TextEditingController();
|
||||
// final shippingStateController = TextEditingController();
|
||||
// final shippingCountryController = TextEditingController();
|
||||
// final billingZipCodeCountryController = TextEditingController();
|
||||
// final shippingZipCodeCountryController = TextEditingController();
|
||||
// final openingBalanceController = TextEditingController();
|
||||
//
|
||||
// FocusNode focusNode = FocusNode();
|
||||
// String? selectedBillingCountry;
|
||||
// String? selectedDShippingCountry;
|
||||
// String? selectedBalanceType;
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final theme = Theme.of(context);
|
||||
//
|
||||
// return Consumer(builder: (context, cRef, __) {
|
||||
// final permissionService = PermissionService(cRef);
|
||||
// return GlobalPopup(
|
||||
// child: Scaffold(
|
||||
// backgroundColor: Colors.white,
|
||||
// appBar: AppBar(
|
||||
// backgroundColor: Colors.white,
|
||||
// title: Text(
|
||||
// lang.S.of(context).updateContact,
|
||||
// ),
|
||||
// centerTitle: true,
|
||||
// iconTheme: const IconThemeData(color: Colors.black),
|
||||
// elevation: 0.0,
|
||||
// ),
|
||||
// body: Consumer(builder: (context, ref, __) {
|
||||
// // ignore: unused_local_variable
|
||||
// final customerData = ref.watch(partiesProvider);
|
||||
//
|
||||
// return SingleChildScrollView(
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(16.0),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Form(
|
||||
// key: _formKay,
|
||||
// child: Column(
|
||||
// children: [
|
||||
// ///_________Phone_______________________
|
||||
// TextFormField(
|
||||
// controller: phoneController,
|
||||
// validator: (value) {
|
||||
// if (value == null || value.isEmpty) {
|
||||
// // return 'Please enter a valid phone number';
|
||||
// return lang.S.of(context).pleaseEnterAValidPhoneNumber;
|
||||
// }
|
||||
// return null;
|
||||
// },
|
||||
// decoration: InputDecoration(
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).phone,
|
||||
// hintText: lang.S.of(context).enterYourPhoneNumber,
|
||||
// border: const OutlineInputBorder(),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(height: 20),
|
||||
//
|
||||
// ///_________Name_______________________
|
||||
// TextFormField(
|
||||
// controller: nameController,
|
||||
// validator: (value) {
|
||||
// if (value == null || value.isEmpty) {
|
||||
// // return 'Please enter a valid Name';
|
||||
// return lang.S.of(context).pleaseEnterAValidName;
|
||||
// }
|
||||
// // You can add more validation logic as needed
|
||||
// return null;
|
||||
// },
|
||||
// decoration: InputDecoration(
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).name,
|
||||
// hintText: lang.S.of(context).enterYourName,
|
||||
// border: const OutlineInputBorder(),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(height: 20),
|
||||
//
|
||||
// ///_________opening balance_______________________
|
||||
// // TextFormField(
|
||||
// // controller: openingBalanceController,
|
||||
// // keyboardType: TextInputType.name,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: lang.S.of(context).openingBalance,
|
||||
// // hintText: lang.S.of(context).enterOpeningBalance,
|
||||
// // suffixIcon: Padding(
|
||||
// // padding: const EdgeInsets.all(1.0),
|
||||
// // child: Container(
|
||||
// // padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// // decoration: BoxDecoration(
|
||||
// // color: kBackgroundColor,
|
||||
// // borderRadius: BorderRadius.only(
|
||||
// // topRight: Radius.circular(4),
|
||||
// // bottomRight: Radius.circular(4),
|
||||
// // )),
|
||||
// // child: DropdownButtonHideUnderline(
|
||||
// // child: DropdownButton(
|
||||
// // icon: Icon(
|
||||
// // Icons.keyboard_arrow_down,
|
||||
// // color: kPeraColor,
|
||||
// // ),
|
||||
// // items: ['Advanced', 'Due'].map((entry) {
|
||||
// // return DropdownMenuItem(value: entry, child: Text(entry, style: theme.textTheme.bodyLarge?.copyWith(color: kTitleColor)));
|
||||
// // }).toList(),
|
||||
// // value: selectedBalanceType ?? 'Advanced',
|
||||
// // onChanged: (String? value) {
|
||||
// // setState(() {
|
||||
// // selectedBalanceType = value;
|
||||
// // });
|
||||
// // }),
|
||||
// // ),
|
||||
// // ),
|
||||
// // )),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: RadioListTile(
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// fillColor: WidgetStateProperty.resolveWith(
|
||||
// (states) {
|
||||
// if (states.contains(WidgetState.selected)) {
|
||||
// return kMainColor;
|
||||
// }
|
||||
// return kPeraColor;
|
||||
// },
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// groupValue: groupValue,
|
||||
// title: Text(
|
||||
// lang.S.of(context).retailer,
|
||||
// maxLines: 1,
|
||||
// style: theme.textTheme.bodySmall,
|
||||
// ),
|
||||
// value: 'Retailer',
|
||||
// onChanged: (value) {
|
||||
// if (widget.customerModel.type != 'Supplier') {
|
||||
// setState(() {
|
||||
// groupValue = value.toString();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// // Change the color to indicate it's not selectable
|
||||
// activeColor: widget.customerModel.type == 'Supplier' ? Colors.grey : kMainColor,
|
||||
// ),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: RadioListTile(
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// fillColor: WidgetStateProperty.resolveWith(
|
||||
// (states) {
|
||||
// if (states.contains(WidgetState.selected)) {
|
||||
// return kMainColor;
|
||||
// }
|
||||
// return kPeraColor;
|
||||
// },
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// groupValue: groupValue,
|
||||
// title: Text(
|
||||
// lang.S.of(context).dealer,
|
||||
// maxLines: 1,
|
||||
// style: theme.textTheme.bodySmall,
|
||||
// ),
|
||||
// value: 'Dealer',
|
||||
// onChanged: (value) {
|
||||
// if (widget.customerModel.type != 'Supplier') {
|
||||
// setState(() {
|
||||
// groupValue = value.toString();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// activeColor: widget.customerModel.type == 'Supplier' ? Colors.grey : kMainColor,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: RadioListTile(
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// fillColor: WidgetStateProperty.resolveWith(
|
||||
// (states) {
|
||||
// if (states.contains(WidgetState.selected)) {
|
||||
// return kMainColor;
|
||||
// }
|
||||
// return kPeraColor;
|
||||
// },
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// activeColor: kMainColor,
|
||||
// groupValue: groupValue,
|
||||
// title: Text(
|
||||
// lang.S.of(context).wholesaler,
|
||||
// maxLines: 1,
|
||||
// style: theme.textTheme.bodySmall,
|
||||
// ),
|
||||
// value: 'Wholesaler',
|
||||
// onChanged: (value) {
|
||||
// if (widget.customerModel.type != 'Supplier') {
|
||||
// setState(() {
|
||||
// groupValue = value.toString();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: RadioListTile(
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// fillColor: WidgetStateProperty.resolveWith(
|
||||
// (states) {
|
||||
// if (states.contains(WidgetState.selected)) {
|
||||
// return kMainColor;
|
||||
// }
|
||||
// return kPeraColor;
|
||||
// },
|
||||
// ),
|
||||
// contentPadding: EdgeInsets.zero,
|
||||
// activeColor: kMainColor,
|
||||
// groupValue: groupValue,
|
||||
// title: Text(
|
||||
// lang.S.of(context).supplier,
|
||||
// maxLines: 1,
|
||||
// style: theme.textTheme.bodySmall,
|
||||
// ),
|
||||
// value: 'Supplier',
|
||||
// onChanged: (value) {
|
||||
// if (widget.customerModel.type != 'Retailer' && widget.customerModel.type != 'Dealer' && widget.customerModel.type != 'Wholesaler') {
|
||||
// setState(() {
|
||||
// groupValue = value.toString();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// Visibility(
|
||||
// visible: showProgress,
|
||||
// child: const CircularProgressIndicator(
|
||||
// color: kMainColor,
|
||||
// strokeWidth: 5.0,
|
||||
// ),
|
||||
// ),
|
||||
// ExpansionPanelList(
|
||||
// expandIconColor: Colors.red,
|
||||
// expansionCallback: (int index, bool isExpanded) {},
|
||||
// animationDuration: const Duration(seconds: 1),
|
||||
// elevation: 0,
|
||||
// dividerColor: Colors.white,
|
||||
// children: [
|
||||
// ExpansionPanel(
|
||||
// backgroundColor: kWhite,
|
||||
// headerBuilder: (BuildContext context, bool isExpanded) {
|
||||
// return Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// TextButton(
|
||||
// child: Text(
|
||||
// lang.S.of(context).moreInfo,
|
||||
// style: theme.textTheme.titleLarge?.copyWith(
|
||||
// color: kMainColor,
|
||||
// ),
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// setState(() {
|
||||
// expanded == false ? expanded = true : expanded = false;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// },
|
||||
// body: Column(
|
||||
// children: [
|
||||
// GestureDetector(
|
||||
// onTap: () {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
// return Dialog(
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(12.0),
|
||||
// ),
|
||||
// // ignore: sized_box_for_whitespace
|
||||
// child: Container(
|
||||
// height: 200.0,
|
||||
// width: MediaQuery.of(context).size.width - 80,
|
||||
// child: Center(
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// GestureDetector(
|
||||
// onTap: () async {
|
||||
// pickedImage = await _picker.pickImage(source: ImageSource.gallery);
|
||||
// setState(() {});
|
||||
// Navigator.pop(context);
|
||||
// },
|
||||
// child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// const Icon(
|
||||
// Icons.photo_library_rounded,
|
||||
// size: 60.0,
|
||||
// color: kMainColor,
|
||||
// ),
|
||||
// Text(lang.S.of(context).gallery, style: theme.textTheme.titleLarge?.copyWith(color: kMainColor)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// width: 40.0,
|
||||
// ),
|
||||
// GestureDetector(
|
||||
// onTap: () async {
|
||||
// pickedImage = await _picker.pickImage(source: ImageSource.camera);
|
||||
// setState(() {});
|
||||
// Navigator.pop(context);
|
||||
// },
|
||||
// child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// const Icon(
|
||||
// Icons.camera,
|
||||
// size: 60.0,
|
||||
// color: kGreyTextColor,
|
||||
// ),
|
||||
// Text(
|
||||
// lang.S.of(context).camera,
|
||||
// style: theme.textTheme.titleLarge?.copyWith(
|
||||
// color: kGreyTextColor,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// },
|
||||
// child: Stack(
|
||||
// children: [
|
||||
// Container(
|
||||
// height: 120,
|
||||
// width: 120,
|
||||
// decoration: BoxDecoration(
|
||||
// border: Border.all(color: Colors.black54, width: 1),
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(120)),
|
||||
// image: pickedImage == null
|
||||
// ? widget.customerModel.image.isEmptyOrNull
|
||||
// ? const DecorationImage(
|
||||
// image: AssetImage('images/no_shop_image.png'),
|
||||
// fit: BoxFit.cover,
|
||||
// )
|
||||
// : DecorationImage(
|
||||
// image: NetworkImage('${APIConfig.domain}${widget.customerModel.image!}'),
|
||||
// fit: BoxFit.cover,
|
||||
// )
|
||||
// : DecorationImage(
|
||||
// image: FileImage(File(pickedImage!.path)),
|
||||
// fit: BoxFit.cover,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Positioned(
|
||||
// bottom: 0,
|
||||
// right: 0,
|
||||
// child: Container(
|
||||
// height: 35,
|
||||
// width: 35,
|
||||
// decoration: BoxDecoration(
|
||||
// border: Border.all(color: Colors.white, width: 2),
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(120)),
|
||||
// color: kMainColor,
|
||||
// ),
|
||||
// child: const Icon(
|
||||
// Icons.camera_alt_outlined,
|
||||
// size: 20,
|
||||
// color: Colors.white,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 20),
|
||||
// TextFormField(
|
||||
// controller: emailController,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: lang.S.of(context).email,
|
||||
// hintText: lang.S.of(context).hintEmail,
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(height: 20),
|
||||
// TextFormField(
|
||||
// controller: addressController,
|
||||
// decoration: InputDecoration(
|
||||
// labelText: lang.S.of(context).address,
|
||||
// hintText: lang.S.of(context).enterFullAddress,
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(height: 20),
|
||||
// TextFormField(
|
||||
// readOnly: true,
|
||||
// controller: dueController,
|
||||
// decoration: InputDecoration(
|
||||
// border: const OutlineInputBorder(),
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).previousDue,
|
||||
// ),
|
||||
// ),
|
||||
// // TextFormField(
|
||||
// // readOnly: true,
|
||||
// // controller: dueController,
|
||||
// // decoration: InputDecoration(
|
||||
// // border: const OutlineInputBorder(),
|
||||
// // floatingLabelBehavior:
|
||||
// // FloatingLabelBehavior.always,
|
||||
// // labelText: lang.S.of(context).previousDue,
|
||||
// // ),
|
||||
// // ),
|
||||
// // Row(
|
||||
// // children: [
|
||||
// // Expanded(
|
||||
// // child: TextFormField(
|
||||
// // controller: partyCreditLimitController,
|
||||
// // decoration: InputDecoration(
|
||||
// // border: const OutlineInputBorder(),
|
||||
// // floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// // labelText: 'Party Credit Limit',
|
||||
// // //hintText: 'Enter your address'
|
||||
// // hintText: 'Ex: 800'),
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(width: 20),
|
||||
// // Expanded(
|
||||
// // child: TextFormField(
|
||||
// // controller: partyGstController,
|
||||
// // decoration: InputDecoration(
|
||||
// // border: const OutlineInputBorder(),
|
||||
// // floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// // labelText: 'Party Gst',
|
||||
// // //hintText: 'Enter your address'
|
||||
// // hintText: 'Ex: 800'),
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // SizedBox(height: 4),
|
||||
// // Theme(
|
||||
// // data: Theme.of(context).copyWith(
|
||||
// // dividerColor: Colors.transparent,
|
||||
// // ),
|
||||
// // child: ExpansionTile(
|
||||
// // visualDensity: VisualDensity(vertical: -2, horizontal: -4),
|
||||
// // tilePadding: EdgeInsets.zero,
|
||||
// // trailing: SizedBox.shrink(),
|
||||
// // title: Row(
|
||||
// // crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// // children: [
|
||||
// // Icon(FeatherIcons.minus, size: 20, color: Colors.red),
|
||||
// // SizedBox(width: 8),
|
||||
// // Text(
|
||||
// // 'Billing Address',
|
||||
// // style: theme.textTheme.titleMedium?.copyWith(
|
||||
// // color: kMainColor,
|
||||
// // ),
|
||||
// // )
|
||||
// // ],
|
||||
// // ),
|
||||
// // children: [
|
||||
// // SizedBox(height: 10),
|
||||
// // //___________Billing Address________________
|
||||
// // TextFormField(
|
||||
// // controller: billingAddressController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'Address',
|
||||
// // hintText: 'Enter Address',
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// // //--------------billing city------------------------
|
||||
// // TextFormField(
|
||||
// // controller: billingCityController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'City',
|
||||
// // hintText: 'Enter city',
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// // //--------------billing state------------------------
|
||||
// // TextFormField(
|
||||
// // controller: billingStateController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'State',
|
||||
// // hintText: 'Enter state',
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// // Row(
|
||||
// // children: [
|
||||
// // //--------------billing zip code------------------------
|
||||
// // Expanded(
|
||||
// // child: TextFormField(
|
||||
// // controller: billingZipCodeCountryController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'Zip Code',
|
||||
// // hintText: 'Enter zip code',
|
||||
// // ),
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(width: 20),
|
||||
// // //--------------billing country------------------------
|
||||
// // Expanded(
|
||||
// // child: DropdownButtonFormField(
|
||||
// // isExpanded: true,
|
||||
// // hint: Text(
|
||||
// // 'Select Country',
|
||||
// // maxLines: 1,
|
||||
// // style: theme.textTheme.bodyMedium?.copyWith(
|
||||
// // color: kPeraColor,
|
||||
// // ),
|
||||
// // overflow: TextOverflow.ellipsis,
|
||||
// // ),
|
||||
// // icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor),
|
||||
// // items: ['Bangladesh', 'Pakisthan', 'Iran'].map((entry) {
|
||||
// // return DropdownMenuItem(
|
||||
// // value: entry,
|
||||
// // child: Text(
|
||||
// // entry,
|
||||
// // style: theme.textTheme.bodyMedium?.copyWith(color: kPeraColor),
|
||||
// // ),
|
||||
// // );
|
||||
// // }).toList(),
|
||||
// // value: selectedDShippingCountry,
|
||||
// // onChanged: (String? value) {
|
||||
// // setState(() {
|
||||
// // selectedBillingCountry = value;
|
||||
// // });
|
||||
// // }),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ),
|
||||
// // Theme(
|
||||
// // data: Theme.of(context).copyWith(
|
||||
// // dividerColor: Colors.transparent,
|
||||
// // ),
|
||||
// // child: ExpansionTile(
|
||||
// // tilePadding: EdgeInsets.zero,
|
||||
// // visualDensity: VisualDensity(horizontal: -4, vertical: -2),
|
||||
// // trailing: SizedBox.shrink(),
|
||||
// // title: Row(
|
||||
// // crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// // children: [
|
||||
// // Icon(FeatherIcons.plus, size: 20),
|
||||
// // SizedBox(width: 8),
|
||||
// // Text(
|
||||
// // 'Shipping Address',
|
||||
// // style: theme.textTheme.titleMedium,
|
||||
// // )
|
||||
// // ],
|
||||
// // ),
|
||||
// // children: [
|
||||
// // SizedBox(height: 10),
|
||||
// // //___________Billing Address________________
|
||||
// // TextFormField(
|
||||
// // controller: billingAddressController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'Address',
|
||||
// // hintText: 'Enter Address',
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// // //--------------billing city------------------------
|
||||
// // TextFormField(
|
||||
// // controller: billingCityController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'City',
|
||||
// // hintText: 'Enter city',
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// // //--------------billing state------------------------
|
||||
// // TextFormField(
|
||||
// // controller: billingStateController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'State',
|
||||
// // hintText: 'Enter state',
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(height: 20),
|
||||
// // Row(
|
||||
// // children: [
|
||||
// // //--------------billing zip code------------------------
|
||||
// // Expanded(
|
||||
// // child: TextFormField(
|
||||
// // controller: billingZipCodeCountryController,
|
||||
// // decoration: InputDecoration(
|
||||
// // labelText: 'Zip Code',
|
||||
// // hintText: 'Enter zip code',
|
||||
// // ),
|
||||
// // ),
|
||||
// // ),
|
||||
// // SizedBox(width: 20),
|
||||
// // //--------------billing country------------------------
|
||||
// // Expanded(
|
||||
// // child: DropdownButtonFormField(
|
||||
// // isExpanded: true,
|
||||
// // hint: Text(
|
||||
// // 'Select Country',
|
||||
// // maxLines: 1,
|
||||
// // style: theme.textTheme.bodyMedium?.copyWith(
|
||||
// // color: kPeraColor,
|
||||
// // ),
|
||||
// // overflow: TextOverflow.ellipsis,
|
||||
// // ),
|
||||
// // icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor),
|
||||
// // items: ['Bangladesh', 'Pakisthan', 'Iran'].map((entry) {
|
||||
// // return DropdownMenuItem(
|
||||
// // value: entry,
|
||||
// // child: Text(
|
||||
// // entry,
|
||||
// // style: theme.textTheme.bodyMedium?.copyWith(color: kPeraColor),
|
||||
// // ),
|
||||
// // );
|
||||
// // }).toList(),
|
||||
// // value: selectedDShippingCountry,
|
||||
// // onChanged: (String? value) {
|
||||
// // setState(() {
|
||||
// // selectedBillingCountry = value;
|
||||
// // });
|
||||
// // }),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// // )
|
||||
// ],
|
||||
// ),
|
||||
// isExpanded: expanded,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// SizedBox(height: 20),
|
||||
// ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// if (!permissionService.hasPermission(Permit.partiesCreate.value)) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// backgroundColor: Colors.red,
|
||||
// content: Text('You do not have permission to update Party.'),
|
||||
// ),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// if (_formKay.currentState!.validate()) {
|
||||
// try {
|
||||
// EasyLoading.show(
|
||||
// status: lang.S.of(context).updating,
|
||||
// // 'Updating...'
|
||||
// );
|
||||
// final party = PartyRepository();
|
||||
// await party.updateParty(
|
||||
// id: widget.customerModel.id.toString(),
|
||||
// // Assuming id is a property in customerModel
|
||||
// ref: ref,
|
||||
// context: context,
|
||||
// name: nameController.text,
|
||||
// phone: phoneController.text,
|
||||
// type: groupValue,
|
||||
// image: pickedImage != null ? File(pickedImage!.path) : null,
|
||||
// email: emailController.text,
|
||||
// address: addressController.text,
|
||||
// due: dueController.text,
|
||||
// );
|
||||
// EasyLoading.dismiss();
|
||||
// } catch (e) {
|
||||
// EasyLoading.dismiss();
|
||||
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// child: Text(lang.S.of(context).update)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
587
lib/Screens/Customers/party_list_screen.dart
Normal file
587
lib/Screens/Customers/party_list_screen.dart
Normal file
@@ -0,0 +1,587 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Screens/Sales/provider/sales_cart_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/Provider/customer_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/add_customer.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/customer_details.dart';
|
||||
import 'package:mobile_pos/Screens/Sales/add_sales.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/widgets/empty_widget/_empty_widget.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import 'Repo/parties_repo.dart';
|
||||
|
||||
// 1. Combine the screens into a single class with a parameter for mode
|
||||
class PartyListScreen extends StatefulWidget {
|
||||
// Use a boolean to determine the screen's purpose
|
||||
final bool isSelectionMode;
|
||||
|
||||
const PartyListScreen({super.key, this.isSelectionMode = false});
|
||||
|
||||
@override
|
||||
State<PartyListScreen> createState() => _PartyListScreenState();
|
||||
}
|
||||
|
||||
class _PartyListScreenState extends State<PartyListScreen> {
|
||||
late Color color;
|
||||
bool _isRefreshing = false;
|
||||
bool _isSearching = false;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
Future<void> refreshData(WidgetRef ref) async {
|
||||
if (_isRefreshing) return;
|
||||
_isRefreshing = true;
|
||||
|
||||
ref.refresh(partiesProvider);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
String? partyType;
|
||||
|
||||
// Define party types based on the mode
|
||||
List<String> get availablePartyTypes {
|
||||
if (widget.isSelectionMode) {
|
||||
// For Sales/Selection mode, exclude 'Supplier'
|
||||
return [
|
||||
PartyType.customer,
|
||||
PartyType.dealer,
|
||||
PartyType.wholesaler,
|
||||
];
|
||||
} else {
|
||||
// For General List/Management mode, include all
|
||||
return [
|
||||
PartyType.customer,
|
||||
PartyType.supplier,
|
||||
PartyType.dealer,
|
||||
PartyType.wholesaler,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showDeleteConfirmationAlert({
|
||||
required BuildContext context,
|
||||
required String id,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context1) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
lang.S.of(context).confirmPassword,
|
||||
//'Confirm Delete'
|
||||
),
|
||||
content: Text(
|
||||
lang.S.of(context).areYouSureYouWant,
|
||||
//'Are you sure you want to delete this party?'
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
lang.S.of(context).cancel,
|
||||
//'Cancel'
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
final party = PartyRepository();
|
||||
await party.deleteParty(id: id, context: context, ref: ref);
|
||||
},
|
||||
child: Text(lang.S.of(context).delete,
|
||||
// 'Delete',
|
||||
style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Consumer(
|
||||
builder: (context, ref, __) {
|
||||
final providerData = ref.watch(partiesProvider);
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
|
||||
// Determine App Bar Title based on mode
|
||||
final appBarTitle = widget.isSelectionMode
|
||||
? lang.S.of(context).chooseCustomer // Sales title
|
||||
: lang.S.of(context).partyList; // Management title
|
||||
|
||||
return businessInfo.when(data: (details) {
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
title: Text(
|
||||
appBarTitle,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(color: Colors.black),
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator.adaptive(
|
||||
onRefresh: () => refreshData(ref),
|
||||
child: providerData.when(data: (partyList) {
|
||||
// Permission check only required for the management view
|
||||
if (!widget.isSelectionMode && !permissionService.hasPermission(Permit.partiesRead.value)) {
|
||||
return const Center(child: PermitDenyWidget());
|
||||
}
|
||||
final filteredParties = partyList.where((c) {
|
||||
final normalizedType = (c.type ?? '').toLowerCase();
|
||||
|
||||
// Filter out suppliers ONLY if in selection mode
|
||||
if (widget.isSelectionMode && normalizedType == 'supplier') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final nameMatches = !_isSearching || _searchController.text.isEmpty
|
||||
? true
|
||||
: (c.name ?? '').toLowerCase().contains(_searchController.text.toLowerCase());
|
||||
|
||||
final effectiveType = normalizedType == 'retailer' ? 'customer' : normalizedType;
|
||||
|
||||
final typeMatches = partyType == null || partyType!.isEmpty ? true : effectiveType == partyType;
|
||||
|
||||
return nameMatches && typeMatches;
|
||||
}).toList();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4),
|
||||
child: TextFormField(
|
||||
controller: _searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: lang.S.of(context).search,
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(color: Colors.grey[600]),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF7F7F7),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(8),
|
||||
bottomRight: Radius.circular(8),
|
||||
)),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
hint: Text(lang.S.of(context).selectType),
|
||||
icon: partyType != null
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: kMainColor,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
partyType = null;
|
||||
});
|
||||
},
|
||||
)
|
||||
: const Icon(Icons.keyboard_arrow_down, color: kPeraColor),
|
||||
value: partyType,
|
||||
onChanged: (String? value) {
|
||||
setState(() {
|
||||
partyType = value;
|
||||
});
|
||||
},
|
||||
// Use the list defined by the mode
|
||||
items: availablePartyTypes.map((entry) {
|
||||
final valueToStore = entry.toLowerCase();
|
||||
return DropdownMenuItem<String>(
|
||||
value: valueToStore,
|
||||
child: Text(
|
||||
getPartyTypeLabel(context, valueToStore),
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(color: kTitleColor),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isSearching = value.isNotEmpty;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 3. Show Walk-In Customer ONLY in selection mode
|
||||
if (widget.isSelectionMode)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onTap: () {
|
||||
AddSalesScreen(customerModel: null).launch(context);
|
||||
ref.refresh(cartNotifier);
|
||||
},
|
||||
leading: SizedBox(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'images/no_shop_image.png',
|
||||
fit: BoxFit.cover,
|
||||
width: 120.0,
|
||||
height: 120.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
lang.S.of(context).walkInCustomer,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
lang.S.of(context).guest,
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 18,
|
||||
color: Color(0xff4B5563),
|
||||
),
|
||||
),
|
||||
filteredParties.isNotEmpty
|
||||
? Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: filteredParties.length,
|
||||
shrinkWrap: true,
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(), // Use AlwaysScrollableScrollPhysics for the main list
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemBuilder: (_, index) {
|
||||
final item = filteredParties[index];
|
||||
final normalizedType = (item.type ?? '').toLowerCase();
|
||||
|
||||
// Color logic (unchanged)
|
||||
color = Colors.white;
|
||||
if (normalizedType == 'retailer' || normalizedType == 'customer') {
|
||||
color = const Color(0xFF56da87);
|
||||
}
|
||||
if (normalizedType == 'wholesaler') color = const Color(0xFF25a9e0);
|
||||
if (normalizedType == 'dealer') color = const Color(0xFFff5f00);
|
||||
if (normalizedType == 'supplier') color = const Color(0xFFA569BD);
|
||||
|
||||
// final effectiveDisplayType = normalizedType == 'retailer'
|
||||
// ? 'Customer'
|
||||
// : normalizedType == 'wholesaler'
|
||||
// ? lang.S.of(context).wholesaler
|
||||
// : normalizedType == 'dealer'
|
||||
// ? lang.S.of(context).dealer
|
||||
// : normalizedType == 'supplier'
|
||||
// ? lang.S.of(context).supplier
|
||||
// : item.type ?? '';
|
||||
|
||||
String effectiveDisplayType;
|
||||
|
||||
if (normalizedType == 'retailer') {
|
||||
effectiveDisplayType = lang.S.of(context).customer;
|
||||
} else if (normalizedType == 'wholesaler') {
|
||||
effectiveDisplayType = lang.S.of(context).wholesaler;
|
||||
} else if (normalizedType == 'dealer') {
|
||||
effectiveDisplayType = lang.S.of(context).dealer;
|
||||
} else if (normalizedType == 'supplier') {
|
||||
effectiveDisplayType = lang.S.of(context).supplier;
|
||||
} else {
|
||||
effectiveDisplayType = item.type ?? '';
|
||||
}
|
||||
|
||||
// Due/Advance/No Due Logic (from previous step)
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
num? statusAmount;
|
||||
|
||||
if (item.due != null && item.due! > 0) {
|
||||
statusText = lang.S.of(context).due;
|
||||
statusColor = const Color(0xFFff5f00);
|
||||
statusAmount = item.due;
|
||||
} else if (item.openingBalanceType?.toLowerCase() == 'advance' &&
|
||||
item.wallet != null &&
|
||||
item.wallet! > 0) {
|
||||
statusText = lang.S.of(context).advance;
|
||||
statusColor = DAppColors.kSecondary;
|
||||
statusAmount = item.wallet;
|
||||
} else {
|
||||
statusText = lang.S.of(context).noDue;
|
||||
statusColor = DAppColors.kSecondary;
|
||||
statusAmount = null;
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -2),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
// 4. OnTap action based on mode
|
||||
if (widget.isSelectionMode) {
|
||||
// Selection Mode: Go to AddSalesScreen
|
||||
AddSalesScreen(customerModel: item).launch(context);
|
||||
ref.refresh(cartNotifier);
|
||||
} else {
|
||||
// Management Mode: Go to CustomerDetails
|
||||
CustomerDetails(party: item).launch(context);
|
||||
}
|
||||
},
|
||||
leading: item.image != null
|
||||
? Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: DAppColors.kBorder, width: 0.3),
|
||||
image: DecorationImage(
|
||||
image: NetworkImage('${APIConfig.domain}${item.image ?? ''}'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircleAvatarWidget(name: item.name),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
statusAmount != null ? '$currency${statusAmount.toStringAsFixed(2)}' : '',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(fontSize: 16.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
effectiveDisplayType,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: color,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
statusText,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: statusColor,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton(
|
||||
offset: const Offset(0, 30),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (BuildContext bc) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CustomerDetails(party: item),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.remove_red_eye,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
lang.S.of(context).view,
|
||||
style: TextStyle(color: kGreyTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!permissionService.hasPermission(Permit.partiesUpdate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).updatePartyWarn),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
AddParty(customerModel: item).launch(context);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
IconlyBold.edit,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: TextStyle(color: kGreyTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!permissionService.hasPermission(Permit.partiesDelete.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).deletePartyWarn),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
await showDeleteConfirmationAlert(
|
||||
context: context, id: item.id.toString(), ref: ref);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
IconlyBold.delete,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Text(
|
||||
lang.S.of(context).delete,
|
||||
style: TextStyle(color: kGreyTextColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
Navigator.pushNamed(context, '$value');
|
||||
},
|
||||
child: const Icon(
|
||||
FeatherIcons.moreVertical,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: EmptyWidget(
|
||||
message: TextSpan(text: lang.S.of(context).noParty),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: ElevatedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
maximumSize: const Size(double.infinity, 48),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
disabledBackgroundColor: _theme.colorScheme.primary.withAlpha(15),
|
||||
disabledForegroundColor: const Color(0xff567DF4).withOpacity(0.05),
|
||||
),
|
||||
onPressed: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
// Check logic based on business info (kept original logic)
|
||||
if (result) {
|
||||
if (details.data?.subscriptionDate != null && details.data?.enrolledPlan != null) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const AddParty()));
|
||||
} else if (!widget.isSelectionMode) {
|
||||
// Allow navigation if not in selection mode and subscription check fails (or fix subscription check)
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const AddParty()));
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
iconAlignment: IconAlignment.start,
|
||||
label: Text(
|
||||
lang.S.of(context).addCustomer,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
119
lib/Screens/Customers/sms_sent_confirmation.dart
Normal file
119
lib/Screens/Customers/sms_sent_confirmation.dart
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class SmsConfirmationPopup extends StatefulWidget {
|
||||
final String customerName;
|
||||
final String phoneNumber;
|
||||
final Function onSendSms;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const SmsConfirmationPopup({
|
||||
super.key,
|
||||
required this.customerName,
|
||||
required this.phoneNumber,
|
||||
required this.onSendSms,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
_SmsConfirmationPopupState createState() => _SmsConfirmationPopupState();
|
||||
}
|
||||
|
||||
class _SmsConfirmationPopupState extends State<SmsConfirmationPopup> with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
final scale = _animationController.value;
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
// 'Confirm SMS to ${widget.customerName}',
|
||||
'${lang.S.of(context).confirmSMSTo} ${widget.customerName}',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
//'An SMS will be sent to the following number: ${widget.phoneNumber}',
|
||||
'${lang.S.of(context).anSMSWillBeSentToTheFollowingNumber} ${widget.phoneNumber}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
onPressed: widget.onCancel,
|
||||
child: Text(
|
||||
lang.S.of(context).cancel,
|
||||
//'Cancel'
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 15),
|
||||
Flexible(
|
||||
child: ElevatedButton(
|
||||
style: const ButtonStyle(backgroundColor: MaterialStatePropertyAll(kMainColor)),
|
||||
onPressed: () {
|
||||
widget.onSendSms();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).sendSMS,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// 'Send SMS',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
175
lib/Screens/Customers/transaction_screen.dart
Normal file
175
lib/Screens/Customers/transaction_screen.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../currency.dart';
|
||||
|
||||
class CustomerAllTransactionScreen extends StatefulWidget {
|
||||
const CustomerAllTransactionScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CustomerAllTransactionScreen> createState() => _CustomerAllTransactionScreenState();
|
||||
}
|
||||
|
||||
class _CustomerAllTransactionScreenState extends State<CustomerAllTransactionScreen> {
|
||||
int currentIndex = 0;
|
||||
bool isSearch = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
elevation: 2.0,
|
||||
surfaceTintColor: kWhite,
|
||||
automaticallyImplyLeading: isSearch ? false : true,
|
||||
backgroundColor: kWhite,
|
||||
title: isSearch
|
||||
? TextFormField(
|
||||
decoration: kInputDecoration.copyWith(
|
||||
contentPadding: const EdgeInsets.only(left: 12, right: 5),
|
||||
//hintText: 'Search Here.....',
|
||||
hintText: lang.S.of(context).searchH,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
lang.S.of(context).transactions,
|
||||
// 'Transactions'
|
||||
),
|
||||
actions: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isSearch = true;
|
||||
});
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(15.0),
|
||||
child: Icon(
|
||||
FeatherIcons.search,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// SalesInvoiceDetails(
|
||||
// businessInfo: personalData.value!,
|
||||
// saleTransaction: transaction[index],
|
||||
// ).launch(context);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
// padding: const EdgeInsets.all(20),
|
||||
width: context.width(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).sale,
|
||||
//"Sale",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
const Text('#2145'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
// padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(color: const Color(0xff0dbf7d).withOpacity(0.1), borderRadius: const BorderRadius.all(Radius.circular(10))),
|
||||
child: Text(
|
||||
lang.S.of(context).paid,
|
||||
style: const TextStyle(color: Color(0xff0dbf7d)),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'30/08/2021',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'${lang.S.of(context).total} : $currency 20000',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${lang.S.of(context).due}: $currency 3000',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
FeatherIcons.printer,
|
||||
color: Colors.grey,
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
Icons.picture_as_pdf,
|
||||
color: Colors.grey,
|
||||
)),
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(
|
||||
// FeatherIcons.share,
|
||||
// color: Colors.grey,
|
||||
// ),
|
||||
// ),
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(
|
||||
// FeatherIcons.moreVertical,
|
||||
// color: Colors.grey,
|
||||
// )),
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 0.5,
|
||||
width: context.width(),
|
||||
color: Colors.grey,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/Screens/DashBoard/chart_data.dart
Normal file
7
lib/Screens/DashBoard/chart_data.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
class ChartData {
|
||||
ChartData(this.x, this.y, this.y1);
|
||||
|
||||
final String x;
|
||||
final double y;
|
||||
final double y1;
|
||||
}
|
||||
484
lib/Screens/DashBoard/dashboard.dart
Normal file
484
lib/Screens/DashBoard/dashboard.dart
Normal file
@@ -0,0 +1,484 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/DashBoard/global_container.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../widgets/build_date_selector/build_date_selector.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import 'numeric_axis.dart';
|
||||
|
||||
class DashboardScreen extends ConsumerStatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final Map<String, String> dateOptions = {
|
||||
'today': lang.S.current.today,
|
||||
'yesterday': lang.S.current.yesterday,
|
||||
'last_seven_days': lang.S.current.last7Days,
|
||||
'last_thirty_days': lang.S.current.last30Days,
|
||||
'current_month': lang.S.current.currentMonth,
|
||||
'last_month': lang.S.current.lastMonth,
|
||||
'current_year': lang.S.current.currentYear,
|
||||
'custom_date': lang.S.current.customDate,
|
||||
};
|
||||
String selectedTime = 'today';
|
||||
bool _isRefreshing = false; // Prevents multiple refresh calls
|
||||
|
||||
Future<void> refreshData(WidgetRef ref) async {
|
||||
if (_isRefreshing) return; // Prevent duplicate refresh calls
|
||||
_isRefreshing = true;
|
||||
|
||||
ref.refresh(dashboardInfoProvider(selectedTime.toLowerCase()));
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1)); // Optional delay
|
||||
_isRefreshing = false;
|
||||
}
|
||||
|
||||
bool _showCustomDatePickers = false; // Track if custom date pickers should be shown
|
||||
|
||||
DateTime? fromDate;
|
||||
DateTime? toDate;
|
||||
|
||||
String _getDateRangeString() {
|
||||
if (selectedTime != 'custom_date') {
|
||||
return selectedTime.toLowerCase();
|
||||
} else if (fromDate != null && toDate != null) {
|
||||
final formattedFrom = DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!);
|
||||
final formattedTo = DateFormat('yyyy-MM-dd', 'en_US').format(toDate!);
|
||||
return 'custom_date&from_date=$formattedFrom&to_date=$formattedTo';
|
||||
} else {
|
||||
return 'custom_date'; // fallback
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectedFormDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2021),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null && picked != fromDate) {
|
||||
setState(() {
|
||||
fromDate = picked;
|
||||
});
|
||||
if (toDate != null) refreshData(ref);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectToDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: fromDate ?? DateTime(2021),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null && picked != toDate) {
|
||||
setState(() {
|
||||
toDate = picked;
|
||||
});
|
||||
if (fromDate != null) refreshData(ref);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Consumer(builder: (_, ref, watch) {
|
||||
final dateRangeString = _getDateRangeString();
|
||||
final dashboardInfo = ref.watch(dashboardInfoProvider(dateRangeString));
|
||||
final permissionService = PermissionService(ref);
|
||||
return dashboardInfo.when(data: (dashboard) {
|
||||
final totalSales = dashboard.data!.sales!.fold<double>(
|
||||
0,
|
||||
(sum, item) => sum + (item.amount ?? 0),
|
||||
);
|
||||
|
||||
final totalPurchase = dashboard.data!.purchases!.fold<double>(
|
||||
0,
|
||||
(sum, items) => sum + (items.amount ?? 0),
|
||||
);
|
||||
return Scaffold(
|
||||
backgroundColor: kBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
title: Text(lang.S.of(context).dashboard),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 32,
|
||||
child: DropdownButtonFormField2<String>(
|
||||
isExpanded: true,
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.keyboard_arrow_down, color: kPeraColor, size: 20),
|
||||
),
|
||||
value: selectedTime,
|
||||
items: dateOptions.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
entry.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: kPeraColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedTime = value!;
|
||||
_showCustomDatePickers = selectedTime == 'custom_date';
|
||||
|
||||
if (_showCustomDatePickers) {
|
||||
fromDate = DateTime.now().subtract(const Duration(days: 7));
|
||||
toDate = DateTime.now();
|
||||
}
|
||||
|
||||
if (selectedTime != 'custom_date') {
|
||||
refreshData(ref);
|
||||
}
|
||||
});
|
||||
},
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
maxHeight: 500,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
radius: const Radius.circular(40),
|
||||
thickness: WidgetStateProperty.all<double>(6),
|
||||
thumbVisibility: WidgetStateProperty.all<bool>(true),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(padding: EdgeInsets.symmetric(horizontal: 6)),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
bottom: _showCustomDatePickers
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(50),
|
||||
child: Column(
|
||||
children: [
|
||||
Divider(thickness: 1, color: kBottomBorder, height: 1),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _selectedFormDate(context),
|
||||
child: buildDateSelector(
|
||||
prefix: 'From',
|
||||
date:
|
||||
fromDate != null ? DateFormat('dd MMMM yyyy').format(fromDate!) : 'Select Date',
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: 22,
|
||||
color: kSubPeraColor,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
GestureDetector(
|
||||
onTap: () => _selectToDate(context),
|
||||
child: buildDateSelector(
|
||||
prefix: 'To',
|
||||
date: toDate != null ? DateFormat('dd MMMM yyyy').format(toDate!) : 'Select Date',
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => refreshData(ref),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (permissionService.hasPermission(Permit.dashboardRead.value)) ...{
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).quickOver,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: kWhite,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
minVerticalPadding: 0,
|
||||
minTileHeight: 0,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
// isShadow: true,
|
||||
textColor: true,
|
||||
title: lang.S.of(context).sales,
|
||||
subtitle: '$currency${formatAmount(totalSales.toString())}',
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
alainRight: true,
|
||||
minVerticalPadding: 0,
|
||||
minTileHeight: 0,
|
||||
// isShadow: true,
|
||||
textColor: true,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: lang.S.of(context).purchased,
|
||||
subtitle: '$currency${formatAmount(totalPurchase.toString())}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
minVerticalPadding: 0,
|
||||
textColor: true,
|
||||
minTileHeight: 0,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: lang.S.of(context).income,
|
||||
subtitle: '$currency${formatAmount(dashboard.data?.totalIncome.toString() ?? '0')}',
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
alainRight: true,
|
||||
minVerticalPadding: 0,
|
||||
minTileHeight: 0,
|
||||
textColor: true,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: lang.S.of(context).expense,
|
||||
subtitle:
|
||||
'$currency${formatAmount(dashboard.data?.totalExpense.toString() ?? '0')}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
|
||||
///---------------chart----------------------
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: kWhite),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).tranSacOver,
|
||||
//'Sales & Purchase Overview',
|
||||
style: theme.textTheme.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold, fontSize: 18, color: kTitleColor),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.circle,
|
||||
color: Colors.green,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: '${lang.S.of(context).sales}: ',
|
||||
//'Sales',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$currency${formatAmount(totalSales.toString())}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600, color: kTitleColor)),
|
||||
])),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const Icon(
|
||||
Icons.circle,
|
||||
color: kMainColor,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: '${lang.S.of(context).purchase}: ',
|
||||
//'Purchase',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$currency${formatAmount(totalPurchase.toString())}',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600, color: kTitleColor)),
|
||||
])),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
height: 250,
|
||||
width: double.infinity,
|
||||
child: DashboardChart(
|
||||
model: dashboard,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GlobalContainer(
|
||||
title: lang.S.of(context).totalDue,
|
||||
image: 'assets/duelist.svg',
|
||||
subtitle: '$currency ${formatAmount(dashboard.data!.totalDue.toString())}')),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Expanded(
|
||||
child: GlobalContainer(
|
||||
title: lang.S.of(context).stockValue,
|
||||
image: 'assets/h_stock.svg',
|
||||
subtitle: "$currency${formatAmount(dashboard.data!.stockValue.toString())}"))
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 19),
|
||||
|
||||
///_________Items_Category________________________
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GlobalContainer(
|
||||
title: lang.S.of(context).item,
|
||||
image: 'assets/totalItem.svg',
|
||||
subtitle: formatAmount('${dashboard.data?.totalItems!.round().toString()}'))),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Expanded(
|
||||
child: GlobalContainer(
|
||||
title: lang.S.of(context).categories,
|
||||
image: 'assets/purchaseLisst.svg',
|
||||
subtitle: formatAmount('${dashboard.data?.totalCategories?.round().toString()}')))
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 21),
|
||||
Text(
|
||||
lang.S.of(context).profitLoss,
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
|
||||
///__________Total_Lass_and_Total_profit_____________________________________
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GlobalContainer(
|
||||
title: lang.S.of(context).profit,
|
||||
image: 'assets/h_lossProfit.svg',
|
||||
subtitle: '$currency ${formatAmount(dashboard.data!.totalProfit.toString())}')),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GlobalContainer(
|
||||
title: lang.S.of(context).loss,
|
||||
image: 'assets/expense.svg',
|
||||
subtitle: '$currency ${formatAmount(dashboard.data!.totalLoss!.abs().toString())}'))
|
||||
],
|
||||
),
|
||||
} else
|
||||
Center(child: PermitDenyWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
print('--------------print-------${e.toString()}-----------------');
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
//'{No data found} $e',
|
||||
'${lang.S.of(context).noDataFound} $e',
|
||||
style: const TextStyle(color: kGreyTextColor, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}, loading: () {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
58
lib/Screens/DashBoard/global_container.dart
Normal file
58
lib/Screens/DashBoard/global_container.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../../constant.dart';
|
||||
|
||||
class GlobalContainer extends StatelessWidget {
|
||||
final String title;
|
||||
final String? image;
|
||||
final String subtitle;
|
||||
final double? minVerticalPadding;
|
||||
final double? minTileHeight;
|
||||
final EdgeInsets? titlePadding;
|
||||
final bool? textColor;
|
||||
final bool? alainRight;
|
||||
const GlobalContainer({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.image,
|
||||
required this.subtitle,
|
||||
this.minVerticalPadding,
|
||||
this.minTileHeight,
|
||||
this.titlePadding,
|
||||
this.textColor,
|
||||
this.alainRight,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: textColor == true ? Colors.transparent : Colors.white),
|
||||
child: ListTile(
|
||||
minVerticalPadding: minVerticalPadding ?? 4,
|
||||
minTileHeight: minTileHeight ?? 0,
|
||||
contentPadding: titlePadding ?? EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
||||
visualDensity: const VisualDensity(vertical: -4, horizontal: -4),
|
||||
leading: image != null
|
||||
? SvgPicture.asset(
|
||||
image!,
|
||||
height: 40,
|
||||
width: 40,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
title,
|
||||
textAlign: (alainRight ?? false) ? TextAlign.end : null,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: textColor == true ? Colors.white : Colors.black),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
textAlign: (alainRight ?? false) ? TextAlign.end : null,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: textColor == true ? Colors.white : Colors.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
268
lib/Screens/DashBoard/numeric_axis.dart
Normal file
268
lib/Screens/DashBoard/numeric_axis.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/model/dashboard_overview_model.dart';
|
||||
|
||||
import 'chart_data.dart';
|
||||
|
||||
class DashboardChart extends StatefulWidget {
|
||||
const DashboardChart({Key? key, required this.model}) : super(key: key);
|
||||
|
||||
final DashboardOverviewModel model;
|
||||
|
||||
@override
|
||||
State<DashboardChart> createState() => _DashboardChartState();
|
||||
}
|
||||
|
||||
class _DashboardChartState extends State<DashboardChart> {
|
||||
List<ChartData> chartData = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getData(widget.model);
|
||||
}
|
||||
|
||||
void getData(DashboardOverviewModel model) {
|
||||
chartData = [];
|
||||
for (int i = 0; i < model.data!.sales!.length; i++) {
|
||||
chartData.add(ChartData(
|
||||
model.data!.sales![i].date!,
|
||||
model.data!.sales![i].amount!.toDouble(),
|
||||
model.data!.purchases![i].amount!.toDouble(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: chartData.length * 50.0, // Adjust width based on the number of data points
|
||||
child: Stack(
|
||||
alignment: Alignment.topRight,
|
||||
children: [
|
||||
BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: _getMaxY(),
|
||||
barTouchData: BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: _getBottomTitles(value, meta),
|
||||
);
|
||||
},
|
||||
reservedSize: 42,
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: false,
|
||||
),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false, reservedSize: 20),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: _getLeftTitles,
|
||||
reservedSize: _getLeftTitleReservedSize(),
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
drawHorizontalLine: true,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return const FlLine(
|
||||
color: Color(0xffD1D5DB),
|
||||
dashArray: [4, 4],
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
barGroups: _buildBarGroups(),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 42),
|
||||
child: CustomPaint(
|
||||
size: Size(
|
||||
chartData.length * 50.0 - _getLeftTitleReservedSize(), // Adjust to match the width of the BarChart exactly
|
||||
0.1),
|
||||
painter: DashedBarPainter(
|
||||
barHeight: 1,
|
||||
barColor: const Color(0xffD1D5DB),
|
||||
dashWidth: 4,
|
||||
dashSpace: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _getMaxY() {
|
||||
double maxY = 0;
|
||||
for (var data in chartData) {
|
||||
maxY = maxY > data.y ? maxY : data.y;
|
||||
maxY = maxY > data.y1 ? maxY : data.y1;
|
||||
}
|
||||
return maxY + 10;
|
||||
}
|
||||
|
||||
double _getLeftTitleReservedSize() {
|
||||
double maxY = _getMaxY();
|
||||
if (maxY < 999) {
|
||||
return 32;
|
||||
} else if (maxY < 1000) {
|
||||
return 35;
|
||||
} else if (maxY < 10000) {
|
||||
return 54;
|
||||
} else {
|
||||
return 50; // Add more cases if needed
|
||||
}
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups() {
|
||||
return chartData.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
ChartData data = entry.value;
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: data.y,
|
||||
color: Colors.green,
|
||||
width: 6,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: data.y1,
|
||||
color: kMainColor,
|
||||
width: 6,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
],
|
||||
barsSpace: 8,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _getBottomTitles(double value, TitleMeta meta) {
|
||||
const style = TextStyle(
|
||||
color: Color(0xff4D4D4D),
|
||||
fontSize: 12,
|
||||
);
|
||||
|
||||
String text = chartData[value.toInt()].x;
|
||||
|
||||
return SideTitleWidget(
|
||||
space: 8,
|
||||
meta: TitleMeta(
|
||||
min: meta.min,
|
||||
max: meta.max,
|
||||
parentAxisSize: meta.parentAxisSize,
|
||||
axisPosition: meta.axisPosition,
|
||||
appliedInterval: meta.appliedInterval,
|
||||
sideTitles: meta.sideTitles,
|
||||
formattedValue: meta.formattedValue,
|
||||
axisSide: meta.axisSide,
|
||||
rotationQuarterTurns: meta.rotationQuarterTurns,
|
||||
),
|
||||
child: Text(text, style: style),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getLeftTitles(double value, TitleMeta meta) {
|
||||
// Skip the highest value (already handled in your code)
|
||||
double maxY = _getMaxY();
|
||||
if (value == maxY) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Format the number
|
||||
String formattedValue;
|
||||
if (value >= 1e9) {
|
||||
formattedValue = '${(value / 1e9).toStringAsFixed(1)}B';
|
||||
} else if (value >= 1e6) {
|
||||
formattedValue = '${(value / 1e6).toStringAsFixed(1)}M';
|
||||
} else if (value >= 1e3) {
|
||||
formattedValue = '${(value / 1e3).toStringAsFixed(1)}K';
|
||||
} else {
|
||||
formattedValue = value.toInt().toString();
|
||||
}
|
||||
|
||||
return SideTitleWidget(
|
||||
meta: meta,
|
||||
child: Text(
|
||||
formattedValue,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///---------------------------------dash line-------------------------------
|
||||
|
||||
class DashedBarPainter extends CustomPainter {
|
||||
final double barHeight;
|
||||
final Color barColor;
|
||||
final double dashWidth;
|
||||
final double dashSpace;
|
||||
|
||||
DashedBarPainter({
|
||||
required this.barHeight,
|
||||
required this.barColor,
|
||||
this.dashWidth = 4.0,
|
||||
this.dashSpace = 2.0,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = barColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = barHeight;
|
||||
|
||||
final dashPath = Path();
|
||||
for (double i = 0; i < size.width; i += dashWidth + dashSpace) {
|
||||
dashPath.addRect(Rect.fromLTWH(i, 0, dashWidth, size.height));
|
||||
}
|
||||
canvas.drawPath(dashPath, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
154
lib/Screens/DashBoard/test_numeric.dart
Normal file
154
lib/Screens/DashBoard/test_numeric.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TestNumericAxisChart extends StatefulWidget {
|
||||
const TestNumericAxisChart({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TestNumericAxisChart> createState() => _TestNumericAxisChartState();
|
||||
}
|
||||
|
||||
class _TestNumericAxisChartState extends State<TestNumericAxisChart> {
|
||||
final List<ChartData> chartData = [
|
||||
ChartData('Sat', 20000, 15000),
|
||||
ChartData('Sun', 10000, 25000),
|
||||
ChartData('Mon', 5000, 5000),
|
||||
ChartData('Tues', 45000, 35000),
|
||||
ChartData('Wed', 25000, 30000),
|
||||
ChartData('Thurs', 20000, 10000),
|
||||
ChartData('Fri', 25000, 20000),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: 50000,
|
||||
barTouchData: BarTouchData(enabled: false),
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: _getBottomTitles,
|
||||
reservedSize: 42,
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: _getLeftTitles,
|
||||
reservedSize: 42,
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) {
|
||||
return const FlLine(
|
||||
color: Color(0xffD1D5DB),
|
||||
dashArray: [5, 5],
|
||||
strokeWidth: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
barGroups: _buildBarGroups(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups() {
|
||||
return chartData.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
ChartData data = entry.value;
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: data.y,
|
||||
color: Colors.green,
|
||||
width: 10,
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: data.y1,
|
||||
color: Colors.red,
|
||||
width: 10,
|
||||
borderRadius: BorderRadius.circular(0),
|
||||
),
|
||||
],
|
||||
barsSpace: 10,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _getBottomTitles(double value, TitleMeta meta) {
|
||||
final style = const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 12,
|
||||
);
|
||||
|
||||
String text = chartData[value.toInt()].x;
|
||||
|
||||
return SideTitleWidget(
|
||||
meta: TitleMeta(
|
||||
min: meta.min,
|
||||
max: meta.max,
|
||||
parentAxisSize: meta.parentAxisSize,
|
||||
axisPosition: meta.axisPosition,
|
||||
appliedInterval: meta.appliedInterval,
|
||||
sideTitles: meta.sideTitles,
|
||||
formattedValue: meta.formattedValue,
|
||||
axisSide: meta.axisSide,
|
||||
rotationQuarterTurns: meta.rotationQuarterTurns,
|
||||
),
|
||||
space: 8,
|
||||
child: Text(text, style: style),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getLeftTitles(double value, TitleMeta meta) {
|
||||
return SideTitleWidget(
|
||||
meta: TitleMeta(
|
||||
min: meta.min,
|
||||
max: meta.max,
|
||||
parentAxisSize: meta.parentAxisSize,
|
||||
axisPosition: meta.axisPosition,
|
||||
appliedInterval: meta.appliedInterval,
|
||||
sideTitles: meta.sideTitles,
|
||||
formattedValue: meta.formattedValue,
|
||||
axisSide: meta.axisSide,
|
||||
rotationQuarterTurns: meta.rotationQuarterTurns,
|
||||
),
|
||||
child: Text(
|
||||
value.toInt().toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChartData {
|
||||
ChartData(this.x, this.y, this.y1);
|
||||
|
||||
final String x;
|
||||
final double y;
|
||||
final double y1;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
class DueCollectionInvoice {
|
||||
DueCollectionInvoice({
|
||||
this.id,
|
||||
this.due,
|
||||
this.name,
|
||||
this.type,
|
||||
this.salesDues,
|
||||
});
|
||||
|
||||
DueCollectionInvoice.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
due = json['due'];
|
||||
name = json['name'];
|
||||
type = json['type'];
|
||||
if (json[json['type'] == 'Supplier' ? 'purchases_dues' : 'sales_dues'] != null) {
|
||||
salesDues = [];
|
||||
json[json['type'] == 'Supplier' ? 'purchases_dues' : 'sales_dues'].forEach((v) {
|
||||
salesDues?.add(SalesDuesInvoice.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
num? id;
|
||||
num? due;
|
||||
String? name;
|
||||
String? type;
|
||||
List<SalesDuesInvoice>? salesDues;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['due'] = due;
|
||||
map['name'] = name;
|
||||
map['type'] = type;
|
||||
if (salesDues != null) {
|
||||
map['sales_dues'] = salesDues?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class SalesDuesInvoice {
|
||||
SalesDuesInvoice({
|
||||
this.id,
|
||||
this.partyId,
|
||||
this.dueAmount,
|
||||
this.paidAmount,
|
||||
this.totalAmount,
|
||||
this.invoiceNumber,
|
||||
});
|
||||
|
||||
SalesDuesInvoice.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
partyId = json['party_id'];
|
||||
dueAmount = json['dueAmount'];
|
||||
paidAmount = json['paidAmount'];
|
||||
totalAmount = json['totalAmount'];
|
||||
invoiceNumber = json['invoiceNumber'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
num? partyId;
|
||||
num? dueAmount;
|
||||
num? paidAmount;
|
||||
num? totalAmount;
|
||||
String? invoiceNumber;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['party_id'] = partyId;
|
||||
map['dueAmount'] = dueAmount;
|
||||
map['paidAmount'] = paidAmount;
|
||||
map['totalAmount'] = totalAmount;
|
||||
map['invoiceNumber'] = invoiceNumber;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
144
lib/Screens/Due Calculation/Model/due_collection_model.dart
Normal file
144
lib/Screens/Due Calculation/Model/due_collection_model.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import '../../../model/sale_transaction_model.dart';
|
||||
import '../../../widgets/multipal payment mathods/model/payment_transaction_model.dart';
|
||||
import '../../Customers/Model/parties_model.dart';
|
||||
|
||||
class DueCollection {
|
||||
DueCollection(
|
||||
{this.id,
|
||||
this.businessId,
|
||||
this.partyId,
|
||||
this.userId,
|
||||
this.saleId,
|
||||
this.purchaseId,
|
||||
this.totalDue,
|
||||
this.dueAmountAfterPay,
|
||||
this.payDueAmount,
|
||||
this.paymentTypeId,
|
||||
this.paymentType,
|
||||
this.paymentDate,
|
||||
this.invoiceNumber,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.user,
|
||||
this.party,
|
||||
this.transactions,
|
||||
this.branch});
|
||||
|
||||
DueCollection.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
businessId = json['business_id'];
|
||||
partyId = json['party_id'];
|
||||
userId = json['user_id'];
|
||||
saleId = json['sale_id'];
|
||||
purchaseId = json['purchase_id'];
|
||||
totalDue = json['totalDue'];
|
||||
dueAmountAfterPay = json['dueAmountAfterPay'];
|
||||
payDueAmount = json['payDueAmount'];
|
||||
paymentTypeId = int.tryParse(json["payment_type_id"].toString());
|
||||
// paymentType = json['paymentType'];
|
||||
paymentDate = json['paymentDate'];
|
||||
invoiceNumber = json['invoiceNumber'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
user = json['user'] != null ? User.fromJson(json['user']) : null;
|
||||
party = json['party'] != null ? Party.fromJson(json['party']) : null;
|
||||
paymentType = json['payment_type'] != null ? PaymentType.fromJson(json['payment_type']) : null;
|
||||
branch = json['branch'] != null ? Branch.fromJson(json['branch']) : null;
|
||||
// NEW: Parsing the transactions list
|
||||
if (json['transactions'] != null) {
|
||||
transactions = [];
|
||||
json['transactions'].forEach((v) {
|
||||
transactions?.add(PaymentsTransaction.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
num? id;
|
||||
num? businessId;
|
||||
num? partyId;
|
||||
num? userId;
|
||||
num? saleId;
|
||||
num? purchaseId;
|
||||
num? totalDue;
|
||||
num? dueAmountAfterPay;
|
||||
num? payDueAmount;
|
||||
int? paymentTypeId;
|
||||
PaymentType? paymentType;
|
||||
String? invoiceNumber;
|
||||
String? paymentDate;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
User? user;
|
||||
Party? party;
|
||||
Branch? branch;
|
||||
List<PaymentsTransaction>? transactions; // NEW Variable
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['business_id'] = businessId;
|
||||
map['party_id'] = partyId;
|
||||
map['user_id'] = userId;
|
||||
map['sale_id'] = saleId;
|
||||
map['purchase_id'] = purchaseId;
|
||||
map['totalDue'] = totalDue;
|
||||
map['dueAmountAfterPay'] = dueAmountAfterPay;
|
||||
map['payDueAmount'] = payDueAmount;
|
||||
map['paymentType'] = paymentType;
|
||||
map['paymentDate'] = paymentDate;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
if (user != null) {
|
||||
map['user'] = user?.toJson();
|
||||
}
|
||||
if (party != null) {
|
||||
map['party'] = party?.toJson();
|
||||
}
|
||||
map['branch'] = branch;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentType {
|
||||
int? id;
|
||||
String? name;
|
||||
|
||||
PaymentType({required this.id, required this.name});
|
||||
|
||||
// Factory constructor to create an instance from a Map
|
||||
factory PaymentType.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentType(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
// Method to convert an instance to a Map
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Branch {
|
||||
Branch({
|
||||
this.id,
|
||||
this.name,
|
||||
this.phone,
|
||||
this.address,
|
||||
});
|
||||
|
||||
Branch.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
phone = json['phone'];
|
||||
address = json['address'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? name;
|
||||
String? phone;
|
||||
String? address;
|
||||
}
|
||||
29
lib/Screens/Due Calculation/Providers/due_provider.dart
Normal file
29
lib/Screens/Due Calculation/Providers/due_provider.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Due%20Calculation/Model/due_collection_model.dart';
|
||||
|
||||
import '../../../Provider/transactions_provider.dart';
|
||||
import '../Model/due_collection_invoice_model.dart';
|
||||
import '../Repo/due_repo.dart';
|
||||
|
||||
//------------dues-------------------------------------
|
||||
final dueRepo = Provider<DueRepo>((ref) => DueRepo());
|
||||
|
||||
final dueCollectionListProvider = FutureProvider.autoDispose<List<DueCollection>>((ref) {
|
||||
final repo = ref.read(dueRepo);
|
||||
return repo.fetchDueCollectionList();
|
||||
});
|
||||
|
||||
final filteredDueProvider = FutureProvider.family.autoDispose<List<DueCollection>, FilterModel>(
|
||||
(ref, filter) {
|
||||
final repo = ref.read(dueRepo);
|
||||
return repo.fetchDueCollectionList(
|
||||
type: filter.duration,
|
||||
fromDate: filter.fromDate,
|
||||
toDate: filter.toDate,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
DueRepo repo = DueRepo();
|
||||
final dueInvoiceListProvider =
|
||||
FutureProvider.autoDispose.family<DueCollectionInvoice, int>((ref, id) => repo.fetchDueInvoiceList(id: id));
|
||||
132
lib/Screens/Due Calculation/Repo/due_repo.dart
Normal file
132
lib/Screens/Due Calculation/Repo/due_repo.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
// ignore_for_file: unused_local_variable
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Provider/profile_provider.dart';
|
||||
import '../../../Provider/transactions_provider.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../Customers/Provider/customer_provider.dart';
|
||||
import '../Model/due_collection_invoice_model.dart';
|
||||
import '../Model/due_collection_model.dart';
|
||||
import '../Providers/due_provider.dart';
|
||||
|
||||
class DueRepo {
|
||||
Future<List<DueCollection>> fetchDueCollectionList({
|
||||
String? type,
|
||||
String? fromDate,
|
||||
String? toDate,
|
||||
}) async {
|
||||
final client = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
// Manually build query string to preserve order
|
||||
final List<String> queryList = [];
|
||||
|
||||
if (type != null && type.isNotEmpty) {
|
||||
queryList.add('duration=$type');
|
||||
}
|
||||
|
||||
if (type == 'custom_date' && fromDate != null && toDate != null && fromDate.isNotEmpty && toDate.isNotEmpty) {
|
||||
queryList.add('from_date=$fromDate');
|
||||
queryList.add('to_date=$toDate');
|
||||
}
|
||||
|
||||
final String queryString = queryList.join('&');
|
||||
final Uri uri = Uri.parse('${APIConfig.url}/dues${queryString.isNotEmpty ? '?$queryString' : ''}');
|
||||
|
||||
print(uri);
|
||||
|
||||
final response = await client.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final list = parsed['data'] as List<dynamic>;
|
||||
return list.map((json) => DueCollection.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch Due List. Status code: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<DueCollectionInvoice> fetchDueInvoiceList({required int id}) async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/invoices?party_id=$id');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return DueCollectionInvoice.fromJson(parsedData['data']);
|
||||
} else {
|
||||
throw Exception('Failed to fetch Sales List');
|
||||
}
|
||||
}
|
||||
|
||||
Future<DueCollection?> dueCollect({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num partyId,
|
||||
required String? invoiceNumber,
|
||||
required String paymentDate,
|
||||
required List<Map<String, dynamic>> payments,
|
||||
required num payDueAmount,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/dues');
|
||||
final requestBody = jsonEncode({
|
||||
'party_id': partyId,
|
||||
'invoiceNumber': invoiceNumber,
|
||||
'paymentDate': paymentDate,
|
||||
'payments': payments,
|
||||
'payDueAmount': payDueAmount,
|
||||
});
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
headers: {
|
||||
"Accept": 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: requestBody);
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
print("Print Due data: $parsedData");
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
EasyLoading.showSuccess('Collected successful!');
|
||||
|
||||
ref.refresh(partiesProvider);
|
||||
|
||||
ref.refresh(purchaseTransactionProvider);
|
||||
ref.refresh(salesTransactionProvider);
|
||||
ref.refresh(businessInfoProvider);
|
||||
ref.refresh(getExpireDateProvider(ref));
|
||||
|
||||
// ref.refresh(dueInvoiceListProvider(partyId.round()));
|
||||
ref.refresh(dueCollectionListProvider);
|
||||
ref.refresh(summaryInfoProvider);
|
||||
|
||||
return DueCollection.fromJson(parsedData['data']);
|
||||
// Navigator.pop(context);
|
||||
// return PurchaseTransaction.fromJson(parsedData);
|
||||
} else {
|
||||
EasyLoading.dismiss().then(
|
||||
(value) => ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Due creation failed: ${parsedData['message']}'))),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss().then(
|
||||
(value) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error'))),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
475
lib/Screens/Due Calculation/due_collection_screen.dart
Normal file
475
lib/Screens/Due Calculation/due_collection_screen.dart
Normal file
@@ -0,0 +1,475 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Due%20Calculation/Model/due_collection_model.dart';
|
||||
import 'package:mobile_pos/Screens/Due%20Calculation/Repo/due_repo.dart';
|
||||
import 'package:mobile_pos/Screens/invoice_details/due_invoice_details.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
|
||||
import '../Customers/Model/parties_model.dart';
|
||||
import 'Model/due_collection_invoice_model.dart';
|
||||
import 'Providers/due_provider.dart';
|
||||
|
||||
class DueCollectionScreen extends StatefulWidget {
|
||||
const DueCollectionScreen({super.key, required this.customerModel});
|
||||
|
||||
@override
|
||||
State<DueCollectionScreen> createState() => _DueCollectionScreenState();
|
||||
final Party customerModel;
|
||||
}
|
||||
|
||||
class _DueCollectionScreenState extends State<DueCollectionScreen> {
|
||||
// Key for MultiPaymentWidget
|
||||
final GlobalKey<MultiPaymentWidgetState> paymentWidgetKey = GlobalKey();
|
||||
|
||||
num paidAmount = 0;
|
||||
num remainDueAmount = 0;
|
||||
num dueAmount = 0;
|
||||
|
||||
num calculateDueAmount({required num total}) {
|
||||
if (total < 0) {
|
||||
remainDueAmount = 0;
|
||||
} else {
|
||||
remainDueAmount = dueAmount - total;
|
||||
}
|
||||
return dueAmount - total;
|
||||
}
|
||||
|
||||
TextEditingController paidText = TextEditingController();
|
||||
TextEditingController dateController = TextEditingController(text: DateTime.now().toString().substring(0, 10));
|
||||
DateTime selectedDate = DateTime.now();
|
||||
|
||||
SalesDuesInvoice? selectedInvoice;
|
||||
// int? paymentType; // Removed old single payment type
|
||||
|
||||
// List of items in our dropdown menu
|
||||
int count = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Listener to update state when paidText changes (either manually or via MultiPaymentWidget)
|
||||
paidText.addListener(() {
|
||||
if (paidText.text.isEmpty) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
paidAmount = 0;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
final val = double.tryParse(paidText.text) ?? 0;
|
||||
// Validation: Cannot pay more than due
|
||||
if (val <= dueAmount) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
paidAmount = val;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If widget pushes value > due, or user types > due
|
||||
// You might want to handle this gracefully.
|
||||
// For now, keeping your old logic:
|
||||
paidText.clear();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
paidAmount = 0;
|
||||
});
|
||||
}
|
||||
EasyLoading.showError(lang.S.of(context).youCanNotPayMoreThenDue);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
count++;
|
||||
return Consumer(builder: (context, consumerRef, __) {
|
||||
final personalData = consumerRef.watch(businessInfoProvider);
|
||||
final dueInvoiceData = consumerRef.watch(dueInvoiceListProvider(widget.customerModel.id?.round() ?? 0));
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return personalData.when(data: (data) {
|
||||
List<SalesDuesInvoice> items = [];
|
||||
num openingDueAmount = 0;
|
||||
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).collectDue,
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
dueInvoiceData.when(data: (data) {
|
||||
num totalDueInInvoice = 0;
|
||||
if (data.salesDues?.isNotEmpty ?? false) {
|
||||
for (var element in data.salesDues!) {
|
||||
totalDueInInvoice += element.dueAmount ?? 0;
|
||||
items.add(element);
|
||||
}
|
||||
}
|
||||
openingDueAmount = (data.due ?? 0) - totalDueInInvoice;
|
||||
if (selectedInvoice == null) {
|
||||
dueAmount = openingDueAmount;
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: DropdownButtonFormField<SalesDuesInvoice>(
|
||||
isExpanded: true,
|
||||
value: selectedInvoice,
|
||||
hint: Text(
|
||||
lang.S.of(context).selectAInvoice,
|
||||
),
|
||||
icon: selectedInvoice != null
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedInvoice = null;
|
||||
// Reset payment widget when invoice is cleared
|
||||
// paymentWidgetKey.currentState?.clear();
|
||||
});
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.keyboard_arrow_down, color: kGreyTextColor),
|
||||
items: items.map((SalesDuesInvoice invoice) {
|
||||
return DropdownMenuItem(
|
||||
value: invoice,
|
||||
child: Text(
|
||||
invoice.invoiceNumber.toString(),
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
dueAmount = newValue?.dueAmount ?? 0;
|
||||
paidAmount = 0;
|
||||
paidText.clear();
|
||||
selectedInvoice = newValue;
|
||||
// Reset payment widget when invoice changes
|
||||
// paymentWidgetKey.currentState?.clear();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.name,
|
||||
readOnly: true,
|
||||
controller: dateController,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).date,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
selectedDate = selectedDate.copyWith(
|
||||
year: picked.year,
|
||||
month: picked.month,
|
||||
day: picked.day,
|
||||
);
|
||||
dateController.text = picked.toString().substring(0, 10);
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: const Icon(FeatherIcons.calendar),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: "${lang.S.of(context).totalDueAmount}: ",
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
color: DAppColors.kSecondary,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: widget.customerModel.due == null
|
||||
? '$currency${0}'
|
||||
: '$currency${widget.customerModel.due!}',
|
||||
style: const TextStyle(color: Color(0xFFFF8C34)),
|
||||
),
|
||||
]),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.name,
|
||||
readOnly: true,
|
||||
initialValue: widget.customerModel.name,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).customerName,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
///_____Total______________________________
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(5),
|
||||
),
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xff000000).withValues(alpha: 0.08),
|
||||
spreadRadius: 0,
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffFEF0F1),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(5),
|
||||
topLeft: Radius.circular(5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).totalAmount,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
dueAmount.toStringAsFixed(2),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).paidAmount,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
SizedBox(
|
||||
width: context.width() / 4,
|
||||
height: 30,
|
||||
child: TextFormField(
|
||||
controller: paidText,
|
||||
// Make ReadOnly if multiple payments are selected to avoid conflict
|
||||
readOnly: (paymentWidgetKey.currentState?.getPaymentEntries().length ?? 1) > 1,
|
||||
textAlign: TextAlign.right,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '0',
|
||||
hintStyle: TextStyle(color: kNeutralColor),
|
||||
border: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
|
||||
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: kBorder)),
|
||||
focusedBorder: UnderlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).dueAmount,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
calculateDueAmount(total: paidAmount).toStringAsFixed(2),
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
///__________Payment_Type_Widget_______________________________________
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(height: 20),
|
||||
MultiPaymentWidget(
|
||||
key: paymentWidgetKey,
|
||||
showWalletOption: true, // Configure as needed
|
||||
showChequeOption: (widget.customerModel.type != 'Supplier'), // Configure as needed
|
||||
totalAmountController: paidText,
|
||||
onPaymentListChanged: () {},
|
||||
),
|
||||
const Divider(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
maximumSize: const Size(double.infinity, 48),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).cancel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: _theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
maximumSize: const Size(double.infinity, 48),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
disabledBackgroundColor: _theme.colorScheme.primary.withValues(alpha: 0.15),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (paidAmount > 0 && dueAmount > 0) {
|
||||
// Get payments from widget
|
||||
List<PaymentEntry> payments = paymentWidgetKey.currentState?.getPaymentEntries() ?? [];
|
||||
|
||||
if (payments.isEmpty) {
|
||||
EasyLoading.showError(lang.S.of(context).noDueSelected); // Or "Please select payment"
|
||||
} else {
|
||||
EasyLoading.show();
|
||||
|
||||
// Serialize Payment List
|
||||
List<Map<String, dynamic>> paymentData = payments.map((e) => e.toJson()).toList();
|
||||
|
||||
DueRepo repo = DueRepo();
|
||||
DueCollection? dueData;
|
||||
dueData = await repo.dueCollect(
|
||||
ref: consumerRef,
|
||||
context: context,
|
||||
partyId: widget.customerModel.id ?? 0,
|
||||
invoiceNumber: selectedInvoice?.invoiceNumber,
|
||||
paymentDate: selectedDate.toIso8601String(),
|
||||
payments: paymentData,
|
||||
payDueAmount: paidAmount,
|
||||
);
|
||||
|
||||
if (dueData != null) {
|
||||
DueInvoiceDetails(
|
||||
dueCollection: dueData,
|
||||
personalInformationModel: data,
|
||||
isFromDue: true,
|
||||
).launch(context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EasyLoading.showError(
|
||||
lang.S.of(context).noDueSelected,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).save,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Center(
|
||||
child: Text(e.toString()),
|
||||
);
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
200
lib/Screens/Due Calculation/due_list_screen.dart
Normal file
200
lib/Screens/Due Calculation/due_list_screen.dart
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Customers/Model/parties_model.dart';
|
||||
import 'package:mobile_pos/Screens/Due%20Calculation/due_collection_screen.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../Const/api_config.dart';
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart' as DAppColors;
|
||||
import '../../constant.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../Customers/Provider/customer_provider.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
|
||||
class DueCalculationContactScreen extends StatefulWidget {
|
||||
const DueCalculationContactScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DueCalculationContactScreen> createState() => _DueCalculationContactScreenState();
|
||||
}
|
||||
|
||||
class _DueCalculationContactScreenState extends State<DueCalculationContactScreen> {
|
||||
late Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).dueList,
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Consumer(builder: (context, ref, __) {
|
||||
final providerData = ref.watch(partiesProvider);
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
return providerData.when(data: (parties) {
|
||||
List<Party> dueCustomerList = [];
|
||||
|
||||
for (var party in parties) {
|
||||
if ((party.due ?? 0) > 0) {
|
||||
dueCustomerList.add(party);
|
||||
}
|
||||
}
|
||||
return dueCustomerList.isNotEmpty
|
||||
? businessInfo.when(data: (details) {
|
||||
if (!permissionService.hasPermission(Permit.duesRead.value)) {
|
||||
return Center(child: PermitDenyWidget());
|
||||
}
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: dueCustomerList.length,
|
||||
itemBuilder: (_, index) {
|
||||
dueCustomerList[index].type == 'Retailer' ? color = const Color(0xFF56da87) : Colors.white;
|
||||
dueCustomerList[index].type == 'Wholesaler'
|
||||
? color = const Color(0xFF25a9e0)
|
||||
: Colors.white;
|
||||
dueCustomerList[index].type == 'Dealer' ? color = const Color(0xFFff5f00) : Colors.white;
|
||||
dueCustomerList[index].type == 'Supplier' ? color = const Color(0xFFA569BD) : Colors.white;
|
||||
|
||||
final item = dueCustomerList[index];
|
||||
final normalizedType = (item.type ?? '').toLowerCase();
|
||||
|
||||
String effectiveDisplayType;
|
||||
|
||||
if (normalizedType == 'retailer') {
|
||||
effectiveDisplayType = lang.S.of(context).customer;
|
||||
} else if (normalizedType == 'wholesaler') {
|
||||
effectiveDisplayType = lang.S.of(context).wholesaler;
|
||||
} else if (normalizedType == 'dealer') {
|
||||
effectiveDisplayType = lang.S.of(context).dealer;
|
||||
} else if (normalizedType == 'supplier') {
|
||||
effectiveDisplayType = lang.S.of(context).supplier;
|
||||
} else {
|
||||
effectiveDisplayType = item.type ?? '';
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -2),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onTap: () async {
|
||||
DueCollectionScreen(customerModel: dueCustomerList[index]).launch(context);
|
||||
},
|
||||
leading: dueCustomerList[index].image != null
|
||||
? Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: DAppColors.kBorder, width: 0.3),
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(
|
||||
'${APIConfig.domain}${dueCustomerList[index].image ?? ''}',
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CircleAvatarWidget(name: dueCustomerList[index].name ?? 'n/a'),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
dueCustomerList[index].name ?? '',
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.black,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$currency ${dueCustomerList[index].due}',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
// dueCustomerList[index].type ?? '',
|
||||
effectiveDisplayType,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: color,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
dueCustomerList[index].due != null && dueCustomerList[index].due != 0
|
||||
? lang.S.of(context).due
|
||||
: 'No Due',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: dueCustomerList[index].due != null && dueCustomerList[index].due != 0
|
||||
? const Color(0xFFff5f00)
|
||||
: const Color(0xff808191),
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: const Icon(
|
||||
IconlyLight.arrow_right_2,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
});
|
||||
}, error: (e, stack) {
|
||||
return const CircularProgressIndicator();
|
||||
}, loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
})
|
||||
: Center(
|
||||
child: Text(
|
||||
lang.S.of(context).noDataAvailabe,
|
||||
maxLines: 2,
|
||||
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 20.0),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/Screens/Expense/Model/expanse_category.dart
Normal file
41
lib/Screens/Expense/Model/expanse_category.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
class ExpenseCategory {
|
||||
ExpenseCategory({
|
||||
this.id,
|
||||
this.categoryName,
|
||||
this.businessId,
|
||||
this.categoryDescription,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
ExpenseCategory.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
categoryName = json['categoryName'];
|
||||
businessId = json['business_id'];
|
||||
categoryDescription = json['categoryDescription'];
|
||||
status = json['status'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? categoryName;
|
||||
num? businessId;
|
||||
String? categoryDescription;
|
||||
bool? status;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['categoryName'] = categoryName;
|
||||
map['business_id'] = businessId;
|
||||
map['categoryDescription'] = categoryDescription;
|
||||
map['status'] = status;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
96
lib/Screens/Expense/Model/expense_modle.dart
Normal file
96
lib/Screens/Expense/Model/expense_modle.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
class Expense {
|
||||
Expense({
|
||||
this.id,
|
||||
this.account,
|
||||
this.amount,
|
||||
this.expenseCategoryId,
|
||||
this.userId,
|
||||
this.businessId,
|
||||
this.expanseFor,
|
||||
this.paymentType,
|
||||
this.paymentTypeId,
|
||||
this.referenceNo,
|
||||
this.note,
|
||||
this.expenseDate,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.category,
|
||||
});
|
||||
|
||||
Expense.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
account = json['account'];
|
||||
amount = json['amount'];
|
||||
expenseCategoryId = json['expense_category_id'];
|
||||
userId = json['user_id'];
|
||||
businessId = json['business_id'];
|
||||
expanseFor = json['expanseFor'];
|
||||
paymentTypeId = json["payment_type_id"];
|
||||
paymentType = json['paymentType'];
|
||||
referenceNo = json['referenceNo'];
|
||||
note = json['note'];
|
||||
expenseDate = json['expenseDate'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
category = json['category'] != null ? Category.fromJson(json['category']) : null;
|
||||
}
|
||||
|
||||
num? id;
|
||||
dynamic account;
|
||||
num? amount;
|
||||
num? expenseCategoryId;
|
||||
num? userId;
|
||||
num? businessId;
|
||||
String? expanseFor;
|
||||
int? paymentTypeId;
|
||||
String? paymentType;
|
||||
String? referenceNo;
|
||||
String? note;
|
||||
String? expenseDate;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
Category? category;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['account'] = account;
|
||||
map['amount'] = amount;
|
||||
map['expense_category_id'] = expenseCategoryId;
|
||||
map['user_id'] = userId;
|
||||
map['business_id'] = businessId;
|
||||
map['expanseFor'] = expanseFor;
|
||||
map['paymentType'] = paymentType;
|
||||
map['referenceNo'] = referenceNo;
|
||||
map['note'] = note;
|
||||
map['expenseDate'] = expenseDate;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
if (category != null) {
|
||||
map['category'] = category?.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Category {
|
||||
Category({
|
||||
this.id,
|
||||
this.categoryName,
|
||||
});
|
||||
|
||||
Category.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
categoryName = json['categoryName'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? categoryName;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['categoryName'] = categoryName;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
23
lib/Screens/Expense/Providers/all_expanse_provider.dart
Normal file
23
lib/Screens/Expense/Providers/all_expanse_provider.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/Model/expense_modle.dart';
|
||||
|
||||
import '../../../Provider/transactions_provider.dart';
|
||||
import '../Repo/expanse_repo.dart';
|
||||
|
||||
//---------income for duration--------------------------------
|
||||
|
||||
final expenseRepoProvider = Provider<ExpenseRepo>(
|
||||
(ref) => ExpenseRepo(),
|
||||
);
|
||||
|
||||
final filteredExpenseProvider = FutureProvider.family.autoDispose<List<Expense>, FilterModel>(
|
||||
(ref, filter) {
|
||||
final repo = ref.read(expenseRepoProvider);
|
||||
|
||||
return repo.fetchAllIExpense(
|
||||
type: filter.duration,
|
||||
fromDate: filter.fromDate,
|
||||
toDate: filter.toDate,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/Model/expanse_category.dart';
|
||||
|
||||
import '../Repo/expanse_category_repo.dart';
|
||||
|
||||
ExpanseCategoryRepo expenseCategoryRepo = ExpanseCategoryRepo();
|
||||
final expanseCategoryProvider = FutureProvider.autoDispose<List<ExpenseCategory>>((ref) => expenseCategoryRepo.fetchAllExpanseCategory());
|
||||
68
lib/Screens/Expense/Repo/expanse_category_repo.dart
Normal file
68
lib/Screens/Expense/Repo/expanse_category_repo.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
//ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Screens/Expense/Model/expanse_category.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/Providers/expense_category_proivder.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
|
||||
class ExpanseCategoryRepo {
|
||||
Future<List<ExpenseCategory>> fetchAllExpanseCategory() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/expense-categories');
|
||||
|
||||
try {
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final categoryList = parsedData['data'] as List<dynamic>;
|
||||
return categoryList.map((category) => ExpenseCategory.fromJson(category)).toList();
|
||||
} else {
|
||||
// Handle specific error cases based on response codes
|
||||
throw Exception('Failed to fetch categories: ${response.statusCode}');
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
rethrow; // Re-throw to allow further handling upstream
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addExpanseCategory({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String categoryName,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/expense-categories');
|
||||
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
var responseData = await customHttpClient.post(url: uri, body: {
|
||||
'categoryName': categoryName,
|
||||
});
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
try {
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
|
||||
var data1 = ref.refresh(expanseCategoryProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Category creation failed: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
}
|
||||
147
lib/Screens/Expense/Repo/expanse_repo.dart
Normal file
147
lib/Screens/Expense/Repo/expanse_repo.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
//ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/Providers/all_expanse_provider.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../../widgets/multipal payment mathods/multi_payment_widget.dart';
|
||||
import '../Model/expense_modle.dart';
|
||||
import '../add_erxpense.dart';
|
||||
|
||||
class ExpenseRepo {
|
||||
Future<List<Expense>> fetchAllIExpense({
|
||||
String? type,
|
||||
String? fromDate,
|
||||
String? toDate,
|
||||
}) async {
|
||||
final client = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
final Map<String, String> queryParams = {};
|
||||
|
||||
if (type != null && type.isNotEmpty) {
|
||||
queryParams['duration'] = type;
|
||||
}
|
||||
|
||||
if (type == 'custom_date') {
|
||||
if (fromDate != null && fromDate.isNotEmpty) {
|
||||
queryParams['from_date'] = fromDate;
|
||||
}
|
||||
if (toDate != null && toDate.isNotEmpty) {
|
||||
queryParams['to_date'] = toDate;
|
||||
}
|
||||
}
|
||||
|
||||
final Uri uri = Uri.parse('${APIConfig.url}/expenses').replace(
|
||||
queryParameters: queryParams.isNotEmpty ? queryParams : null,
|
||||
);
|
||||
|
||||
print('Request URI: $uri');
|
||||
|
||||
final response = await client.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final list = parsed['data'] as List<dynamic>;
|
||||
return list.map((json) => Expense.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch Due List. Status code: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
// Future<List<Expense>> fetchExpense() async {
|
||||
// CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
// final uri = Uri.parse('${APIConfig.url}/expenses');
|
||||
//
|
||||
// final response = await clientGet.get(url: uri);
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
//
|
||||
// final partyList = parsedData['data'] as List<dynamic>;
|
||||
// return partyList.map((category) => Expense.fromJson(category)).toList();
|
||||
// // Parse into Party objects
|
||||
// } else {
|
||||
// throw Exception('Failed to fetch expense list');
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> createExpense({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num amount,
|
||||
required num expenseCategoryId,
|
||||
required String expanseFor,
|
||||
required String referenceNo,
|
||||
required String expenseDate,
|
||||
required String note,
|
||||
required List<PaymentEntry> payments, // <<< Updated parameter
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/expenses');
|
||||
|
||||
// Build the request body as a Map<String, String> for form-data
|
||||
// This will be sent as 'application/x-www-form-urlencoded'
|
||||
Map<String, String> requestBody = {
|
||||
'amount': amount.toString(),
|
||||
'expense_category_id': expenseCategoryId.toString(),
|
||||
'expanseFor': expanseFor,
|
||||
'referenceNo': referenceNo,
|
||||
'expenseDate': expenseDate,
|
||||
'note': note,
|
||||
};
|
||||
|
||||
// Add payments in the format: payments[index][key]
|
||||
for (int i = 0; i < payments.length; i++) {
|
||||
final payment = payments[i];
|
||||
final paymentAmount = num.tryParse(payment.amountController.text) ?? 0;
|
||||
|
||||
// Only add valid payments
|
||||
if (payment.type != null && paymentAmount > 0) {
|
||||
requestBody['payments[$i][type]'] = payment.type!;
|
||||
requestBody['payments[$i][amount]'] = paymentAmount.toString();
|
||||
|
||||
if (payment.type == 'cheque' && payment.chequeNumberController.text.isNotEmpty) {
|
||||
requestBody['payments[$i][cheque_number]'] = payment.chequeNumberController.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
print('POST DATA OF EXPENSE: $requestBody');
|
||||
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: requestBody,
|
||||
addContentTypeInHeader: false,
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200 || responseData.statusCode == 201) {
|
||||
Navigator.pop(context, true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(parsedData['message'] ?? 'Expense created successfully'),
|
||||
));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Expense creation failed: ${parsedData['message']}')));
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
// return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
332
lib/Screens/Expense/add_erxpense.dart
Normal file
332
lib/Screens/Expense/add_erxpense.dart
Normal file
@@ -0,0 +1,332 @@
|
||||
// ignore_for_file: unused_result
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/Model/expanse_category.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/expense_category_list.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../generated/l10n.dart' as lang;
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
|
||||
import 'Repo/expanse_repo.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class AddExpense extends ConsumerStatefulWidget {
|
||||
const AddExpense({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_AddExpenseState createState() => _AddExpenseState();
|
||||
}
|
||||
|
||||
class _AddExpenseState extends ConsumerState<AddExpense> {
|
||||
ExpenseCategory? selectedCategory;
|
||||
final dateController = TextEditingController();
|
||||
TextEditingController expanseForNameController = TextEditingController();
|
||||
TextEditingController expanseAmountController = TextEditingController();
|
||||
TextEditingController expanseNoteController = TextEditingController();
|
||||
TextEditingController expanseRefController = TextEditingController();
|
||||
|
||||
// (CHANGE 1) GlobalKey-ke public state class (`MultiPaymentWidgetState`) diye update kora holo
|
||||
final GlobalKey<MultiPaymentWidgetState> _paymentKey = GlobalKey<MultiPaymentWidgetState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// All payment listeners are now in MultiPaymentWidget
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose all parent controllers
|
||||
dateController.dispose();
|
||||
expanseForNameController.dispose();
|
||||
expanseAmountController.dispose();
|
||||
expanseNoteController.dispose();
|
||||
expanseRefController.dispose();
|
||||
// All payment controllers are disposed by MultiPaymentWidget
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
DateTime selectedDate = DateTime.now();
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2021)); // Error fixed: 20121 -> 2021
|
||||
if (picked != null && picked != selectedDate) {
|
||||
setState(() {
|
||||
selectedDate = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
bool validateAndSave() {
|
||||
final form = formKey.currentState;
|
||||
if (form!.validate()) {
|
||||
form.save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissionService = PermissionService(ref);
|
||||
// bankListAsync is no longer needed here, MultiPaymentWidget will handle it
|
||||
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).addExpense,
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: context.width(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
///_______date________________________________
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: FormField(
|
||||
builder: (FormFieldState<dynamic> field) {
|
||||
return InputDecorator(
|
||||
decoration: kInputDecoration.copyWith(
|
||||
suffixIcon: const Icon(IconlyLight.calendar, color: kGreyTextColor),
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
labelText: lang.S.of(context).expenseDate,
|
||||
hintText: lang.S.of(context).enterExpenseDate,
|
||||
),
|
||||
child: Text(
|
||||
'${DateFormat.d().format(selectedDate)} ${DateFormat.MMM().format(selectedDate)} ${DateFormat.y().format(selectedDate)}',
|
||||
),
|
||||
);
|
||||
},
|
||||
).onTap(() => _selectDate(context)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_________category_______________________________________________
|
||||
Container(
|
||||
height: 48.0,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(color: kBorderColor),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
selectedCategory = await const ExpenseCategoryList().launch(context);
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 10.0),
|
||||
Text(selectedCategory?.categoryName ?? lang.S.of(context).selectCategory),
|
||||
const Spacer(),
|
||||
const Icon(Icons.keyboard_arrow_down),
|
||||
const SizedBox(
|
||||
width: 10.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///________Expense_for_______________________________________________
|
||||
TextFormField(
|
||||
showCursor: true,
|
||||
controller: expanseForNameController,
|
||||
validator: (value) {
|
||||
if (value.isEmptyOrNull) {
|
||||
return lang.S.of(context).pleaseEnterName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
expanseForNameController.text = value!;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).expenseFor,
|
||||
hintText: lang.S.of(context).enterName,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_________________Total Amount_____________________________
|
||||
TextFormField(
|
||||
controller: expanseAmountController,
|
||||
// (CHANGE 2) readOnly logic-ti notun key diye update kora holo
|
||||
readOnly: (_paymentKey.currentState?.getPaymentEntries().length ?? 1) > 1,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
validator: (value) {
|
||||
if (value.isEmptyOrNull) {
|
||||
return lang.S.of(context).pleaseEnterAmount;
|
||||
}
|
||||
// Get total from the controller itself
|
||||
final total = double.tryParse(value ?? '') ?? 0.0;
|
||||
if (total <= 0) {
|
||||
return lang.S.of(context).amountMustBeGreaterThanZero;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
// (CHANGE 3) fillColor logic-ti notun key diye update kora holo
|
||||
fillColor: (_paymentKey.currentState?.getPaymentEntries().length ?? 1) > 1
|
||||
? Colors.grey.shade100
|
||||
: Colors.white,
|
||||
filled: true,
|
||||
border: const OutlineInputBorder(),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.red),
|
||||
),
|
||||
labelText: lang.S.of(context).amount,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
hintText: '0.00',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_______reference_________________________________
|
||||
TextFormField(
|
||||
showCursor: true,
|
||||
controller: expanseRefController,
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
expanseRefController.text = value!;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).referenceNo,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
hintText: lang.S.of(context).enterRefNumber,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_________note____________________________________________________
|
||||
TextFormField(
|
||||
showCursor: true,
|
||||
controller: expanseNoteController,
|
||||
validator: (value) {
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
expanseNoteController.text = value!;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).note,
|
||||
hintText: lang.S.of(context).enterNote,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
MultiPaymentWidget(
|
||||
key: _paymentKey,
|
||||
totalAmountController: expanseAmountController,
|
||||
showChequeOption: false,
|
||||
onPaymentListChanged: () {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_______button_________________________________
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 45,
|
||||
child: ElevatedButton.icon(
|
||||
iconAlignment: IconAlignment.end,
|
||||
label: Text(lang.S.of(context).continueButton),
|
||||
onPressed: () async {
|
||||
if (!permissionService.hasPermission(Permit.expensesCreate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDonNotHavePermissionToCreateExpense),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (validateAndSave()) {
|
||||
// (CHANGE 5) Notun key diye data neya hocche
|
||||
final totalExpense = double.tryParse(expanseAmountController.text) ?? 0.0;
|
||||
final payments = _paymentKey.currentState?.getPaymentEntries();
|
||||
|
||||
if (selectedCategory == null) {
|
||||
EasyLoading.showError(lang.S.of(context).pleaseSelectAExpenseCategory);
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalExpense <= 0) {
|
||||
EasyLoading.showError(lang.S.of(context).amountMustBeGreaterThanZero);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payments == null || payments.isEmpty) {
|
||||
EasyLoading.showError(lang.S.of(context).canNotRetrievePaymentDetails);
|
||||
return;
|
||||
}
|
||||
|
||||
EasyLoading.show();
|
||||
ExpenseRepo repo = ExpenseRepo();
|
||||
|
||||
await repo.createExpense(
|
||||
ref: ref,
|
||||
context: context,
|
||||
amount: totalExpense, // Use state variable
|
||||
expenseCategoryId: selectedCategory?.id ?? 0,
|
||||
expanseFor: expanseForNameController.text,
|
||||
referenceNo: expanseRefController.text,
|
||||
expenseDate: selectedDate.toString(),
|
||||
note: expanseNoteController.text,
|
||||
payments: payments, // Pass the payment list
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
110
lib/Screens/Expense/add_expense_category.dart
Normal file
110
lib/Screens/Expense/add_expense_category.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
// ignore_for_file: unused_result
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/Repo/expanse_category_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
|
||||
class AddExpenseCategory extends StatefulWidget {
|
||||
const AddExpenseCategory({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_AddExpenseCategoryState createState() => _AddExpenseCategoryState();
|
||||
}
|
||||
|
||||
class _AddExpenseCategoryState extends State<AddExpenseCategory> {
|
||||
bool showProgress = false;
|
||||
|
||||
TextEditingController nameController = TextEditingController();
|
||||
GlobalKey<FormState> key = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(builder: (context, ref, __) {
|
||||
//final allCategory = ref.watch(expanseCategoryProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).addExpenseCat,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: showProgress,
|
||||
child: const CircularProgressIndicator(
|
||||
color: kMainColor,
|
||||
strokeWidth: 5.0,
|
||||
),
|
||||
),
|
||||
Form(
|
||||
key: key,
|
||||
child: TextFormField(
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmptyOrNull ?? true) {
|
||||
//return 'Enter expanse category name';
|
||||
return lang.S.of(context).enterExpanseCategoryName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: lang.S.of(context).fashions,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).categoryName,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (!permissionService.hasPermission(Permit.expenseCategoriesCreate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotHavePermissionToCreateExpenseCategory),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (key.currentState?.validate() ?? false) {
|
||||
EasyLoading.show();
|
||||
final categoryRepo = ExpanseCategoryRepo();
|
||||
await categoryRepo.addExpanseCategory(
|
||||
ref: ref,
|
||||
context: context,
|
||||
categoryName: nameController.text.trim(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
153
lib/Screens/Expense/expense_category_list.dart
Normal file
153
lib/Screens/Expense/expense_category_list.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Expense/add_expense_category.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import 'Providers/expense_category_proivder.dart';
|
||||
|
||||
class ExpenseCategoryList extends StatefulWidget {
|
||||
const ExpenseCategoryList({Key? key, this.mainContext}) : super(key: key);
|
||||
|
||||
final BuildContext? mainContext;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_ExpenseCategoryListState createState() => _ExpenseCategoryListState();
|
||||
}
|
||||
|
||||
class _ExpenseCategoryListState extends State<ExpenseCategoryList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Consumer(builder: (context, ref, _) {
|
||||
final data = ref.watch(expanseCategoryProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).expenseCat,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: AppTextField(
|
||||
textFieldType: TextFieldType.NAME,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: lang.S.of(context).search,
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: kGreyTextColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10.0,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
const AddExpenseCategory().launch(context);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 20.0, right: 20.0),
|
||||
height: 48.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(color: kBorderColor),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
data.when(data: (data) {
|
||||
if (!permissionService.hasPermission(Permit.incomeCategoriesRead.value)) {
|
||||
return Center(child: PermitDenyWidget());
|
||||
}
|
||||
return ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
data[index].categoryName ?? '',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kBackgroundColor,
|
||||
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 12),
|
||||
minimumSize: Size(
|
||||
50,
|
||||
25,
|
||||
),
|
||||
),
|
||||
// buttonDecoration: kButtonDecoration.copyWith(color: kDarkWhite),
|
||||
onPressed: () {
|
||||
// const AddExpense().launch(context);
|
||||
Navigator.pop(
|
||||
context,
|
||||
data[index],
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
lang.S.of(context).select,
|
||||
style: theme.textTheme.titleSmall?.copyWith(color: Colors.black, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}, error: (error, stackTrace) {
|
||||
return Text(error.toString());
|
||||
}, loading: () {
|
||||
return const CircularProgressIndicator();
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
332
lib/Screens/Expense/expense_list.dart
Normal file
332
lib/Screens/Expense/expense_list.dart
Normal file
@@ -0,0 +1,332 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
// import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
// import 'package:intl/intl.dart';
|
||||
// import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
// import 'package:mobile_pos/Screens/Expense/Providers/all_expanse_provider.dart';
|
||||
// import 'package:mobile_pos/Screens/Expense/add_erxpense.dart';
|
||||
// import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// import 'package:nb_utils/nb_utils.dart';
|
||||
//
|
||||
// import '../../GlobalComponents/glonal_popup.dart';
|
||||
// import '../../constant.dart';
|
||||
// import '../../currency.dart';
|
||||
// import '../../http_client/custome_http_client.dart';
|
||||
// import '../../service/check_actions_when_no_branch.dart';
|
||||
// import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
// import '../../service/check_user_role_permission_provider.dart';
|
||||
// import 'Providers/expense_category_proivder.dart';
|
||||
//
|
||||
// class ExpenseList extends StatefulWidget {
|
||||
// const ExpenseList({super.key});
|
||||
//
|
||||
// @override
|
||||
// // ignore: library_private_types_in_public_api
|
||||
// _ExpenseListState createState() => _ExpenseListState();
|
||||
// }
|
||||
//
|
||||
// class _ExpenseListState extends State<ExpenseList> {
|
||||
// final dateController = TextEditingController();
|
||||
// TextEditingController fromDateTextEditingController = TextEditingController(text: DateFormat.yMMMd().format(DateTime(2021)));
|
||||
// TextEditingController toDateTextEditingController = TextEditingController(text: DateFormat.yMMMd().format(DateTime.now()));
|
||||
// DateTime fromDate = DateTime(2021);
|
||||
// DateTime toDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
|
||||
// num totalExpense = 0;
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// dateController.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
//
|
||||
// bool _isRefreshing = false; // Prevents multiple refresh calls
|
||||
//
|
||||
// Future<void> refreshData(WidgetRef ref) async {
|
||||
// if (_isRefreshing) return; // Prevent duplicate refresh calls
|
||||
// _isRefreshing = true;
|
||||
//
|
||||
// ref.refresh(expenseProvider);
|
||||
// ref.refresh(expanseCategoryProvider);
|
||||
//
|
||||
// await Future.delayed(const Duration(seconds: 1)); // Optional delay
|
||||
// _isRefreshing = false;
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// totalExpense = 0;
|
||||
// return Consumer(builder: (context, ref, __) {
|
||||
// final expenseData = ref.watch(expenseProvider);
|
||||
// final businessInfoData = ref.watch(businessInfoProvider);
|
||||
// final permissionService = PermissionService(ref);
|
||||
// return GlobalPopup(
|
||||
// child: Scaffold(
|
||||
// backgroundColor: kWhite,
|
||||
// appBar: AppBar(
|
||||
// title: Text(
|
||||
// lang.S.of(context).expense,
|
||||
// ),
|
||||
// iconTheme: const IconThemeData(color: Colors.black),
|
||||
// centerTitle: true,
|
||||
// backgroundColor: Colors.white,
|
||||
// elevation: 0.0,
|
||||
// ),
|
||||
// body: RefreshIndicator(
|
||||
// onRefresh: () => refreshData(ref),
|
||||
// child: SingleChildScrollView(
|
||||
// physics: const AlwaysScrollableScrollPhysics(),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// if (permissionService.hasPermission(Permit.expensesRead.value)) ...{
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(right: 10.0, left: 10.0, top: 10, bottom: 10),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: AppTextField(
|
||||
// textFieldType: TextFieldType.NAME,
|
||||
// readOnly: true,
|
||||
// controller: fromDateTextEditingController,
|
||||
// decoration: InputDecoration(
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).fromDate,
|
||||
// border: const OutlineInputBorder(),
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () async {
|
||||
// final DateTime? picked = await showDatePicker(
|
||||
// initialDate: DateTime.now(),
|
||||
// firstDate: DateTime(2015, 8),
|
||||
// lastDate: DateTime(2101),
|
||||
// context: context,
|
||||
// );
|
||||
// setState(() {
|
||||
// fromDateTextEditingController.text = DateFormat.yMMMd().format(picked ?? DateTime.now());
|
||||
// fromDate = picked!;
|
||||
// totalExpense = 0;
|
||||
// });
|
||||
// },
|
||||
// icon: const Icon(FeatherIcons.calendar),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
// Expanded(
|
||||
// child: AppTextField(
|
||||
// textFieldType: TextFieldType.NAME,
|
||||
// readOnly: true,
|
||||
// controller: toDateTextEditingController,
|
||||
// decoration: InputDecoration(
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).toDate,
|
||||
// border: const OutlineInputBorder(),
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () async {
|
||||
// final DateTime? picked = await showDatePicker(
|
||||
// initialDate: toDate,
|
||||
// firstDate: DateTime(2015, 8),
|
||||
// lastDate: DateTime(2101),
|
||||
// context: context,
|
||||
// );
|
||||
//
|
||||
// setState(() {
|
||||
// toDateTextEditingController.text = DateFormat.yMMMd().format(picked ?? DateTime.now());
|
||||
// picked!.isToday ? toDate = DateTime.now() : toDate = picked;
|
||||
// totalExpense = 0;
|
||||
// });
|
||||
// },
|
||||
// icon: const Icon(FeatherIcons.calendar),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// ///__________expense_data_table____________________________________________
|
||||
// Container(
|
||||
// width: context.width(),
|
||||
// height: 50,
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// decoration: const BoxDecoration(color: kDarkWhite),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: 130,
|
||||
// child: Text(
|
||||
// lang.S.of(context).expenseFor,
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 100,
|
||||
// child: Text(lang.S.of(context).date),
|
||||
// ),
|
||||
// Container(
|
||||
// alignment: Alignment.centerRight,
|
||||
// width: 70,
|
||||
// child: Text(lang.S.of(context).amount),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// expenseData.when(data: (mainData) {
|
||||
// if (mainData.isNotEmpty) {
|
||||
// totalExpense = 0;
|
||||
// for (var element in mainData) {
|
||||
// final dateStr = element.expenseDate;
|
||||
// if (dateStr != null && dateStr.isNotEmpty) {
|
||||
// final parsedDate = DateTime.tryParse(dateStr.substring(0, 10));
|
||||
// if (parsedDate != null &&
|
||||
// (fromDate.isBefore(parsedDate) || fromDate.isAtSameMomentAs(parsedDate)) &&
|
||||
// (toDate.isAfter(parsedDate) || toDate.isAtSameMomentAs(parsedDate))) {
|
||||
// totalExpense += element.amount ?? 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return SizedBox(
|
||||
// width: context.width(),
|
||||
// child: ListView.builder(
|
||||
// shrinkWrap: true,
|
||||
// itemCount: mainData.length,
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// itemBuilder: (BuildContext context, int index) {
|
||||
// return Visibility(
|
||||
// visible: mainData[index].expenseDate != null &&
|
||||
// mainData[index].expenseDate!.isNotEmpty &&
|
||||
// DateTime.tryParse(mainData[index].expenseDate!.substring(0, 10)) != null &&
|
||||
// (fromDate.isBefore(DateTime.parse(mainData[index].expenseDate!.substring(0, 10))) ||
|
||||
// fromDate.isAtSameMomentAs(DateTime.parse(mainData[index].expenseDate!.substring(0, 10)))) &&
|
||||
// (toDate.isAfter(DateTime.parse(mainData[index].expenseDate!.substring(0, 10))) ||
|
||||
// toDate.isAtSameMomentAs(DateTime.parse(mainData[index].expenseDate!.substring(0, 10)))),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: 130,
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Text(
|
||||
// mainData[index].expanseFor ?? '',
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// const SizedBox(height: 5),
|
||||
// Text(
|
||||
// mainData[index].category?.categoryName ?? '',
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(color: Colors.grey, fontSize: 11),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 100,
|
||||
// child: Text(
|
||||
// mainData[index].expenseDate != null && mainData[index].expenseDate!.isNotEmpty
|
||||
// ? DateFormat.yMMMd().format(DateTime.parse(mainData[index].expenseDate!))
|
||||
// : 'N/A',
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// alignment: Alignment.centerRight,
|
||||
// width: 70,
|
||||
// child: Text('$currency${mainData[index].amount.toString()}'),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// height: 1,
|
||||
// color: Colors.black12,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(20),
|
||||
// child: Center(
|
||||
// child: Text(lang.S.of(context).noData),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }, error: (Object error, StackTrace? stackTrace) {
|
||||
// return Text(error.toString());
|
||||
// }, loading: () {
|
||||
// return const Center(child: CircularProgressIndicator());
|
||||
// }),
|
||||
// } else
|
||||
// Center(child: PermitDenyWidget()),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// bottomNavigationBar: Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// ///_________total______________________________________________
|
||||
// if (permissionService.hasPermission(Permit.expensesRead.value))
|
||||
// Container(
|
||||
// height: 50,
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// decoration: const BoxDecoration(color: kDarkWhite),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text(
|
||||
// lang.S.of(context).totalExpense,
|
||||
// ),
|
||||
// Text('$currency$totalExpense')
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 10),
|
||||
//
|
||||
// ///________button________________________________________________
|
||||
// businessInfoData.when(data: (details) {
|
||||
// return ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
// if (!result) {
|
||||
// return;
|
||||
// }
|
||||
// const AddExpense().launch(context);
|
||||
// },
|
||||
// child: Text(lang.S.of(context).addExpense),
|
||||
// );
|
||||
// }, error: (e, stack) {
|
||||
// return Text(e.toString());
|
||||
// }, loading: () {
|
||||
// return const Center(
|
||||
// child: CircularProgressIndicator(),
|
||||
// );
|
||||
// })
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
33
lib/Screens/Home/Model/banner_model.dart
Normal file
33
lib/Screens/Home/Model/banner_model.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
class Banner {
|
||||
Banner({
|
||||
this.id,
|
||||
this.imageUrl,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
Banner.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
imageUrl = json['imageUrl'];
|
||||
status = json['status'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? imageUrl;
|
||||
num? status;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['imageUrl'] = imageUrl;
|
||||
map['status'] = status;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
7
lib/Screens/Home/Provider/banner_provider.dart
Normal file
7
lib/Screens/Home/Provider/banner_provider.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../Model/banner_model.dart';
|
||||
import '../Repo/banner_repo.dart';
|
||||
|
||||
BannerRepo imageRepo = BannerRepo();
|
||||
final bannerProvider = FutureProvider<List<Banner>>((ref) => imageRepo.fetchAllIBanners());
|
||||
26
lib/Screens/Home/Repo/banner_repo.dart
Normal file
26
lib/Screens/Home/Repo/banner_repo.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../Model/banner_model.dart';
|
||||
|
||||
class BannerRepo {
|
||||
Future<List<Banner>> fetchAllIBanners() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/banners');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final partyList = parsedData['data'] as List<dynamic>;
|
||||
return partyList.map((user) => Banner.fromJson(user)).toList();
|
||||
// Parse into Party objects
|
||||
} else {
|
||||
throw Exception('Failed to fetch Users');
|
||||
}
|
||||
}
|
||||
}
|
||||
64
lib/Screens/Home/components/bottom_nav.dart
Normal file
64
lib/Screens/Home/components/bottom_nav.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/Screens/Settings/settings_screen.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
class BottomNav extends StatefulWidget {
|
||||
const BottomNav({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BottomNav> createState() => _BottomNavState();
|
||||
}
|
||||
|
||||
class _BottomNavState extends State<BottomNav> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
switch (_selectedIndex) {
|
||||
case 0:
|
||||
Navigator.pushNamed(context, '/home');
|
||||
break;
|
||||
case 1:
|
||||
Navigator.pushNamed(context, '/order');
|
||||
break;
|
||||
case 2:
|
||||
Navigator.pushNamed(context, '/featuredProduct');
|
||||
break;
|
||||
case 3:
|
||||
const SettingScreen().launch(context);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomNavigationBar(
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 6.0,
|
||||
selectedItemColor: kMainColor,
|
||||
// ignore: prefer_const_literals_to_create_immutables
|
||||
items: [
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.flare_sharp),
|
||||
label: 'Maan',
|
||||
),
|
||||
const BottomNavigationBarItem(
|
||||
icon: Icon(Icons.backpack),
|
||||
label: 'Package',
|
||||
),
|
||||
const BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
|
||||
],
|
||||
currentIndex: _selectedIndex,
|
||||
onTap: _onItemTapped,
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/Screens/Home/components/grid_items.dart
Normal file
113
lib/Screens/Home/components/grid_items.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class GridItems {
|
||||
final String title, icon, route;
|
||||
|
||||
GridItems({required this.title, required this.icon, required this.route});
|
||||
}
|
||||
|
||||
List<GridItems> getFreeIcons({required BuildContext context, bool? brunchPermission, bool? hrmPermission}) {
|
||||
List<GridItems> freeIcons = [
|
||||
GridItems(
|
||||
title: lang.S.of(context).sale,
|
||||
icon: 'assets/sales.svg',
|
||||
route: 'Sales',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).posSale,
|
||||
icon: 'images/dash_pos.svg',
|
||||
route: 'Pos Sale',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).parties,
|
||||
icon: 'assets/parties.svg',
|
||||
route: 'Parties',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).purchase,
|
||||
icon: 'assets/purchase.svg',
|
||||
route: 'Purchase',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).product,
|
||||
icon: 'assets/products.svg',
|
||||
route: 'Products',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).dueList,
|
||||
icon: 'assets/duelist.svg',
|
||||
route: 'Due List',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).stockList,
|
||||
icon: 'assets/h_stock.svg',
|
||||
route: 'Stock',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).reports,
|
||||
icon: 'assets/reports.svg',
|
||||
route: 'Reports',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).saleList,
|
||||
icon: 'assets/salelist.svg',
|
||||
route: 'Sales List',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).purchaseList,
|
||||
icon: 'assets/purchaseLisst.svg',
|
||||
route: 'Purchase List',
|
||||
),
|
||||
GridItems(
|
||||
// TODO: Shakil change this to `Profit & Loss`
|
||||
title: lang.S.of(context).profitAndLoss,
|
||||
icon: 'assets/h_lossProfit.svg',
|
||||
route: 'Loss/Profit',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).ledger,
|
||||
icon: 'assets/ledger.svg',
|
||||
route: 'ledger',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).income,
|
||||
icon: 'assets/h_income.svg',
|
||||
route: 'Income',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).expense,
|
||||
icon: 'assets/expense.svg',
|
||||
route: 'Expense',
|
||||
),
|
||||
GridItems(
|
||||
title: lang.S.of(context).vatAndTax,
|
||||
icon: 'assets/tax.svg',
|
||||
route: 'tax',
|
||||
),
|
||||
// GridItems(
|
||||
// title: 'Warehouse',
|
||||
// icon: 'assets/tax.svg',
|
||||
// route: 'warehouse',
|
||||
// ),
|
||||
GridItems(
|
||||
title: lang.S.of(context).customPrint,
|
||||
icon: 'assets/printer.svg',
|
||||
route: 'customPrint',
|
||||
),
|
||||
if (brunchPermission == true)
|
||||
GridItems(
|
||||
title: lang.S.of(context).branch,
|
||||
icon: 'assets/branch.svg',
|
||||
route: 'branch',
|
||||
),
|
||||
if (hrmPermission ?? false)
|
||||
GridItems(
|
||||
title: lang.S.of(context).hrm,
|
||||
icon: 'assets/hrm/hrm.svg',
|
||||
route: 'hrm',
|
||||
),
|
||||
];
|
||||
|
||||
return freeIcons;
|
||||
}
|
||||
132
lib/Screens/Home/home.dart
Normal file
132
lib/Screens/Home/home.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:mobile_pos/Screens/DashBoard/dashboard.dart';
|
||||
import 'package:mobile_pos/Screens/Home/home_screen.dart';
|
||||
import 'package:mobile_pos/Screens/Report/reports.dart';
|
||||
import 'package:mobile_pos/Screens/Settings/settings_screen.dart';
|
||||
import 'package:mobile_pos/Screens/pos_sale/pos_sale.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/model/business_info_model.dart' as visible;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
const Home({super.key});
|
||||
|
||||
@override
|
||||
_HomeState createState() => _HomeState();
|
||||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
int _tabIndex = 0;
|
||||
late final PageController pageController = PageController(initialPage: _tabIndex);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleNavigation(
|
||||
int index,
|
||||
BuildContext context,
|
||||
) {
|
||||
setState(() => _tabIndex = index);
|
||||
pageController.jumpToPage(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async =>
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(lang.S.of(context).areYouSure),
|
||||
content: Text(lang.S.of(context).doYouWantToExitTheApp),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: Text(lang.S.of(context).no)),
|
||||
TextButton(onPressed: () => Navigator.pop(context, true), child: Text(lang.S.of(context).yes)),
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false,
|
||||
child: Consumer(builder: (context, ref, __) {
|
||||
ref.watch(getExpireDateProvider(ref));
|
||||
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
body: PageView(
|
||||
controller: pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
onPageChanged: (v) => setState(() => _tabIndex = v),
|
||||
children: [
|
||||
HomeScreen(),
|
||||
PosSaleScreen(),
|
||||
DashboardScreen(),
|
||||
Reports(),
|
||||
SettingScreen(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _tabIndex,
|
||||
backgroundColor: Colors.white,
|
||||
// onTap: (i) => _handleNavigation(i, context, visibility),
|
||||
onTap: (i) => _handleNavigation(
|
||||
i,
|
||||
context,
|
||||
),
|
||||
items: [
|
||||
_buildNavItem(index: 0, activeIcon: 'cHome', icon: 'home', label: lang.S.of(context).home),
|
||||
_buildNavItem(
|
||||
index: 1,
|
||||
activeIcon: 'cPos',
|
||||
icon: 'pos',
|
||||
label: lang.S.of(context).pos,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 2,
|
||||
activeIcon: 'dashbord1',
|
||||
icon: 'dashbord',
|
||||
label: lang.S.of(context).dashboard,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 3,
|
||||
activeIcon: 'cFile',
|
||||
icon: 'file',
|
||||
label: lang.S.of(context).reports,
|
||||
),
|
||||
_buildNavItem(
|
||||
index: 4,
|
||||
activeIcon: 'cSetting',
|
||||
icon: 'setting',
|
||||
label: lang.S.of(context).setting,
|
||||
),
|
||||
],
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedItemColor: kMainColor,
|
||||
unselectedItemColor: kGreyTextColor,
|
||||
selectedLabelStyle: const TextStyle(fontSize: 14),
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
BottomNavigationBarItem _buildNavItem(
|
||||
{required int index, required String activeIcon, required String icon, required String label}) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: _tabIndex == index
|
||||
? SvgPicture.asset('assets/$activeIcon.svg', height: 28, width: 28, fit: BoxFit.scaleDown)
|
||||
: SvgPicture.asset('assets/$icon.svg',
|
||||
colorFilter: const ColorFilter.mode(kGreyTextColor, BlendMode.srcIn), height: 24, width: 24),
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
}
|
||||
500
lib/Screens/Home/home_screen.dart
Normal file
500
lib/Screens/Home/home_screen.dart
Normal file
@@ -0,0 +1,500 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Repository/check_addon_providers.dart';
|
||||
import 'package:mobile_pos/Screens/DashBoard/dashboard.dart';
|
||||
import 'package:mobile_pos/Screens/Home/components/grid_items.dart';
|
||||
import 'package:mobile_pos/Screens/Profile%20Screen/profile_details.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
|
||||
import '../../Provider/profile_provider.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
import '../Customers/Provider/customer_provider.dart';
|
||||
import '../DashBoard/global_container.dart';
|
||||
import '../Home/Model/banner_model.dart' as b;
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../branch/branch_list.dart';
|
||||
import '../branch/repo/branch_repo.dart';
|
||||
import '../subscription/package_screen.dart';
|
||||
import 'Provider/banner_provider.dart';
|
||||
|
||||
class HomeScreen extends ConsumerStatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
PageController pageController = PageController(initialPage: 0, viewportFraction: 0.8);
|
||||
|
||||
bool _isRefreshing = false;
|
||||
|
||||
Future<void> refreshAllProviders({required WidgetRef ref}) async {
|
||||
if (_isRefreshing) return; // Prevent multiple refresh calls
|
||||
|
||||
_isRefreshing = true;
|
||||
try {
|
||||
ref.refresh(summaryInfoProvider);
|
||||
ref.refresh(bannerProvider);
|
||||
ref.refresh(businessInfoProvider);
|
||||
ref.refresh(partiesProvider);
|
||||
ref.refresh(getExpireDateProvider(ref));
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Consumer(builder: (_, ref, __) {
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
final summaryInfo = ref.watch(summaryInfoProvider);
|
||||
final banner = ref.watch(bannerProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
return businessInfo.when(data: (details) {
|
||||
final icons = getFreeIcons(
|
||||
context: context,
|
||||
hrmPermission: (details.data?.addons?.hrmAddon == true),
|
||||
brunchPermission: (((details.data?.addons?.multiBranchAddon == true) &&
|
||||
(details.data?.enrolledPlan?.allowMultibranch == 1) &&
|
||||
(details.data?.user?.branchId == null)))
|
||||
? true
|
||||
: false);
|
||||
return Scaffold(
|
||||
backgroundColor: kBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
titleSpacing: 5,
|
||||
surfaceTintColor: kWhite,
|
||||
actions: [
|
||||
if ((details.data?.addons?.multiBranchAddon ?? false) && (details.data?.user?.activeBranch != null))
|
||||
TextButton.icon(
|
||||
label: Text(
|
||||
'${details.data?.user?.activeBranch?.name}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: kTitleColor),
|
||||
),
|
||||
style: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusGeometry.circular(2),
|
||||
),
|
||||
),
|
||||
textStyle: WidgetStatePropertyAll(
|
||||
theme.textTheme.bodyMedium?.copyWith(color: kTitleColor),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (details.data?.user?.branchId != null) {
|
||||
return;
|
||||
}
|
||||
bool switchBranch = await BranchListScreen.switchDialog(context: context, isLogin: false);
|
||||
if (switchBranch) {
|
||||
EasyLoading.show();
|
||||
|
||||
final switched =
|
||||
await BranchRepo().exitBranch(id: details.data?.user?.activeBranchId.toString() ?? '');
|
||||
|
||||
if (switched) {
|
||||
Restart.restartApp();
|
||||
}
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
},
|
||||
icon: SvgPicture.asset(
|
||||
'assets/branch_icon.svg',
|
||||
height: 16,
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
IconButton(onPressed: () async => refreshAllProviders(ref: ref), icon: const Icon(Icons.refresh))
|
||||
],
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
const ProfileDetails().launch(context);
|
||||
},
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: details.data?.pictureUrl == null
|
||||
? BoxDecoration(
|
||||
image:
|
||||
const DecorationImage(image: AssetImage('images/no_shop_image.png'), fit: BoxFit.cover),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
)
|
||||
: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: NetworkImage('${APIConfig.domain}${details.data?.pictureUrl}'),
|
||||
fit: BoxFit.cover),
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
details.data?.user?.role == 'staff'
|
||||
? '${details.data?.companyName ?? ''} [${details.data?.user?.name ?? ''}]'
|
||||
: details.data?.companyName ?? '',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
// onTap: () {
|
||||
// showDialog(
|
||||
// context: context,
|
||||
// builder: (BuildContext context) {
|
||||
// return goToPackagePagePopup(
|
||||
// context: context,
|
||||
// enrolledPlan: details.enrolledPlan);
|
||||
// });
|
||||
// },
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: '${details.data?.enrolledPlan?.plan?.subscriptionName ?? 'No Active'} Plan',
|
||||
children: [
|
||||
// if (details.enrolledPlan?.duration != null &&
|
||||
// details.enrolledPlan!.duration! <= 7)
|
||||
// TextSpan(
|
||||
// text: ' (${getDayLeftInExpiring(
|
||||
// expireDate: details.willExpire,
|
||||
// shortMSG: false,
|
||||
// )})',
|
||||
// style: theme.textTheme.bodySmall?.copyWith(
|
||||
// fontSize: 13,
|
||||
// color: kPeraColor,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 13,
|
||||
color: kPeraColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: RefreshIndicator.adaptive(
|
||||
onRefresh: () async => refreshAllProviders(ref: ref),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (permissionService.hasPermission(Permit.dashboardRead.value)) ...{
|
||||
summaryInfo.when(data: (summary) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
lang.S.of(context).quickOver,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
color: kWhite,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context, MaterialPageRoute(builder: (context) => DashboardScreen())),
|
||||
child: Text(
|
||||
lang.S.of(context).viewAll,
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: kWhite, fontSize: 16),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
minVerticalPadding: 0,
|
||||
minTileHeight: 0,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
// isShadow: true,
|
||||
textColor: true,
|
||||
title: lang.S.of(context).sales,
|
||||
subtitle: '$currency${formatAmount(summary.data!.sales.toString())}',
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
minVerticalPadding: 0,
|
||||
minTileHeight: 0,
|
||||
// isShadow: true,
|
||||
textColor: true,
|
||||
alainRight: true,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: lang.S.of(context).purchased,
|
||||
subtitle: '$currency${formatAmount(summary.data!.purchase.toString())}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
minVerticalPadding: 0,
|
||||
textColor: true,
|
||||
minTileHeight: 0,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: lang.S.of(context).income,
|
||||
subtitle: '$currency${formatAmount(summary.data!.income.toString())}',
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: GlobalContainer(
|
||||
minVerticalPadding: 0,
|
||||
minTileHeight: 0,
|
||||
textColor: true,
|
||||
alainRight: true,
|
||||
titlePadding: EdgeInsets.zero,
|
||||
title: lang.S.of(context).expense,
|
||||
subtitle: '$currency${formatAmount(summary.data!.expense.toString())}',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 16),
|
||||
},
|
||||
|
||||
GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
childAspectRatio: 3.0,
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisCount: 2,
|
||||
children: List.generate(
|
||||
icons.length,
|
||||
(index) => HomeGridCards(
|
||||
gridItems: icons[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///________________Banner_______________________________________
|
||||
banner.when(data: (imageData) {
|
||||
List<b.Banner> images = [];
|
||||
if (imageData.isNotEmpty) {
|
||||
images.addAll(imageData.where(
|
||||
(element) => element.status == 1,
|
||||
));
|
||||
}
|
||||
|
||||
if (images.isNotEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).whatNew,
|
||||
textAlign: TextAlign.start,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Container(
|
||||
height: 150,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: images.length,
|
||||
itemBuilder: (_, index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
const PackageScreen().launch(context);
|
||||
},
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 10), // Spacing between items
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
child: Image.network(
|
||||
"${APIConfig.domain}${images[index].imageUrl}",
|
||||
width: MediaQuery.of(context).size.width * 0.7, // 80% width
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: Container(
|
||||
height: 150,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: AssetImage('images/banner1.png'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, error: (e, stack) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Center(
|
||||
child: Text(
|
||||
lang.S.of(context).noDataFound,
|
||||
style: theme.textTheme.titleMedium,
|
||||
//'No Data Found'
|
||||
),
|
||||
),
|
||||
);
|
||||
}, loading: () {
|
||||
return const CircularProgressIndicator();
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}, error: (e, stack) {
|
||||
return Center(child: Text(e.toString()));
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class HomeGridCards extends StatefulWidget {
|
||||
const HomeGridCards({
|
||||
super.key,
|
||||
required this.gridItems,
|
||||
// this.visibility,
|
||||
});
|
||||
|
||||
final GridItems gridItems;
|
||||
// final business.Visibility? visibility;
|
||||
|
||||
@override
|
||||
State<HomeGridCards> createState() => _HomeGridCardsState();
|
||||
}
|
||||
|
||||
class _HomeGridCardsState extends State<HomeGridCards> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(builder: (context, ref, __) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
bool result = await checkActionWhenNoBranch(context: context, actionName: widget.gridItems.title, ref: ref);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pushNamed('/${widget.gridItems.route}');
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8), color: kWhite, boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xff171717).withOpacity(0.07),
|
||||
offset: const Offset(0, 3),
|
||||
blurRadius: 50,
|
||||
spreadRadius: -4)
|
||||
]),
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
widget.gridItems.icon.toString(),
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.gridItems.title.toString(),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: DAppColors.kNeutral700),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String getSubscriptionExpiring({required String? expireDate, required bool shortMSG}) {
|
||||
if (expireDate == null) {
|
||||
return shortMSG ? 'N/A' : lang.S.current.subscribeNow;
|
||||
}
|
||||
DateTime expiringDay = DateTime.parse(expireDate).add(const Duration(days: 1));
|
||||
if (expiringDay.isBefore(DateTime.now())) {
|
||||
return lang.S.current.expired;
|
||||
}
|
||||
if (expiringDay.difference(DateTime.now()).inDays < 1) {
|
||||
return shortMSG
|
||||
? '${expiringDay.difference(DateTime.now()).inHours}\n${lang.S.current.hoursLeft}'
|
||||
: '${expiringDay.difference(DateTime.now()).inHours} ${lang.S.current.hoursLeft}';
|
||||
} else {
|
||||
return shortMSG
|
||||
? '${expiringDay.difference(DateTime.now()).inDays}\n${lang.S.current.daysLeft}'
|
||||
: '${expiringDay.difference(DateTime.now()).inDays} ${lang.S.current.daysLeft}';
|
||||
}
|
||||
}
|
||||
41
lib/Screens/Income/Model/income_category.dart
Normal file
41
lib/Screens/Income/Model/income_category.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
class IncomeCategory {
|
||||
IncomeCategory({
|
||||
this.id,
|
||||
this.categoryName,
|
||||
this.businessId,
|
||||
this.categoryDescription,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
IncomeCategory.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
categoryName = json['categoryName'];
|
||||
businessId = json['business_id'];
|
||||
categoryDescription = json['categoryDescription'];
|
||||
status = json['status'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? categoryName;
|
||||
num? businessId;
|
||||
String? categoryDescription;
|
||||
bool? status;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['categoryName'] = categoryName;
|
||||
map['business_id'] = businessId;
|
||||
map['categoryDescription'] = categoryDescription;
|
||||
map['status'] = status;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
96
lib/Screens/Income/Model/income_modle.dart
Normal file
96
lib/Screens/Income/Model/income_modle.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
class Income {
|
||||
Income({
|
||||
this.id,
|
||||
this.account,
|
||||
this.amount,
|
||||
this.incomeCategoryId,
|
||||
this.userId,
|
||||
this.businessId,
|
||||
this.incomeFor,
|
||||
this.paymentTypeId,
|
||||
this.paymentType,
|
||||
this.referenceNo,
|
||||
this.note,
|
||||
this.incomeDate,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.category,
|
||||
});
|
||||
|
||||
Income.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
account = json['account'];
|
||||
amount = json['amount'] as num;
|
||||
incomeCategoryId = json['income_category_id'];
|
||||
userId = json['user_id'];
|
||||
businessId = json['business_id'];
|
||||
incomeFor = json['incomeFor'];
|
||||
paymentTypeId = json["payment_type_id"];
|
||||
paymentType = json['paymentType'];
|
||||
referenceNo = json['referenceNo'];
|
||||
note = json['note'];
|
||||
incomeDate = json['incomeDate'];
|
||||
createdAt = json['created_at'];
|
||||
updatedAt = json['updated_at'];
|
||||
category = json['category'] != null ? Category.fromJson(json['category']) : null;
|
||||
}
|
||||
|
||||
num? id;
|
||||
num? account;
|
||||
num? amount;
|
||||
num? incomeCategoryId;
|
||||
num? userId;
|
||||
num? businessId;
|
||||
String? incomeFor;
|
||||
int? paymentTypeId;
|
||||
String? paymentType;
|
||||
String? referenceNo;
|
||||
String? note;
|
||||
String? incomeDate;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
Category? category;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['account'] = account;
|
||||
map['amount'] = amount;
|
||||
map['expense_category_id'] = incomeCategoryId;
|
||||
map['user_id'] = userId;
|
||||
map['business_id'] = businessId;
|
||||
map['expanseFor'] = incomeFor;
|
||||
map['paymentType'] = paymentType;
|
||||
map['referenceNo'] = referenceNo;
|
||||
map['note'] = note;
|
||||
map['incomeDate'] = incomeDate;
|
||||
map['created_at'] = createdAt;
|
||||
map['updated_at'] = updatedAt;
|
||||
if (category != null) {
|
||||
map['category'] = category?.toJson();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
class Category {
|
||||
Category({
|
||||
this.id,
|
||||
this.categoryName,
|
||||
});
|
||||
|
||||
Category.fromJson(dynamic json) {
|
||||
id = json['id'];
|
||||
categoryName = json['categoryName'];
|
||||
}
|
||||
|
||||
num? id;
|
||||
String? categoryName;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = id;
|
||||
map['categoryName'] = categoryName;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
21
lib/Screens/Income/Providers/all_income_provider.dart
Normal file
21
lib/Screens/Income/Providers/all_income_provider.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../Provider/transactions_provider.dart';
|
||||
import '../Model/income_modle.dart';
|
||||
import '../Repo/income_repo.dart';
|
||||
|
||||
final incomeRepoProvider = Provider<IncomeRepo>(
|
||||
(ref) => IncomeRepo(),
|
||||
);
|
||||
|
||||
final filteredIncomeProvider = FutureProvider.family.autoDispose<List<Income>, FilterModel>(
|
||||
(ref, filter) {
|
||||
final repo = ref.read(incomeRepoProvider);
|
||||
|
||||
return repo.fetchAllIncome(
|
||||
type: filter.duration,
|
||||
fromDate: filter.fromDate,
|
||||
toDate: filter.toDate,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../Model/income_category.dart';
|
||||
import '../Repo/income_category_repo.dart';
|
||||
|
||||
IncomeCategoryRepo incomeCategoryRepo = IncomeCategoryRepo();
|
||||
final incomeCategoryProvider = FutureProvider.autoDispose<List<IncomeCategory>>((ref) => incomeCategoryRepo.fetchAllIncomeCategory());
|
||||
70
lib/Screens/Income/Repo/income_category_repo.dart
Normal file
70
lib/Screens/Income/Repo/income_category_repo.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
//ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Screens/Income/Providers/income_category_provider.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../Model/income_category.dart';
|
||||
|
||||
class IncomeCategoryRepo {
|
||||
Future<List<IncomeCategory>> fetchAllIncomeCategory() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/income-categories');
|
||||
|
||||
try {
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final categoryList = parsedData['data'] as List<dynamic>;
|
||||
return categoryList.map((category) => IncomeCategory.fromJson(category)).toList();
|
||||
} else {
|
||||
// Handle specific error cases based on response codes
|
||||
throw Exception('Failed to fetch categories: ${response.statusCode}');
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
rethrow; // Re-throw to allow further handling upstream
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addIncomeCategory({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String categoryName,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/income-categories');
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: {
|
||||
'categoryName': categoryName,
|
||||
},
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
try {
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
|
||||
var data1 = ref.refresh(incomeCategoryProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Category creation failed: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
}
|
||||
120
lib/Screens/Income/Repo/income_repo.dart
Normal file
120
lib/Screens/Income/Repo/income_repo.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../../widgets/multipal payment mathods/multi_payment_widget.dart';
|
||||
import '../Model/income_modle.dart';
|
||||
|
||||
class IncomeRepo {
|
||||
Future<List<Income>> fetchAllIncome({
|
||||
String? type,
|
||||
String? fromDate,
|
||||
String? toDate,
|
||||
}) async {
|
||||
final client = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
final Map<String, String> queryParams = {};
|
||||
|
||||
if (type != null && type.isNotEmpty) {
|
||||
queryParams['duration'] = type;
|
||||
}
|
||||
|
||||
if (type == 'custom_date') {
|
||||
if (fromDate != null && fromDate.isNotEmpty) {
|
||||
queryParams['from_date'] = fromDate;
|
||||
}
|
||||
if (toDate != null && toDate.isNotEmpty) {
|
||||
queryParams['to_date'] = toDate;
|
||||
}
|
||||
}
|
||||
|
||||
final Uri uri = Uri.parse('${APIConfig.url}/incomes').replace(
|
||||
queryParameters: queryParams.isNotEmpty ? queryParams : null,
|
||||
);
|
||||
|
||||
print('Request URI: $uri');
|
||||
|
||||
final response = await client.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsed = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final list = parsed['data'] as List<dynamic>;
|
||||
return list.map((json) => Income.fromJson(json)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch Due List. Status code: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createIncome({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num amount,
|
||||
required num incomeCategoryId,
|
||||
required String incomeFor, // Renamed from expanseFor
|
||||
required String referenceNo,
|
||||
required String incomeDate, // Renamed from expenseDate
|
||||
required String note,
|
||||
required List<PaymentEntry> payments, // <<< Updated parameter
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/incomes');
|
||||
|
||||
// Build the request body as a Map<String, String> for form-data
|
||||
Map<String, String> requestBody = {
|
||||
'amount': amount.toString(),
|
||||
'income_category_id': incomeCategoryId.toString(),
|
||||
'incomeFor': incomeFor,
|
||||
'referenceNo': referenceNo,
|
||||
'incomeDate': incomeDate,
|
||||
'note': note,
|
||||
};
|
||||
|
||||
// Add payments in the format: payments[index][key]
|
||||
for (int i = 0; i < payments.length; i++) {
|
||||
final payment = payments[i];
|
||||
final paymentAmount = num.tryParse(payment.amountController.text) ?? 0;
|
||||
|
||||
if (payment.type != null && paymentAmount > 0) {
|
||||
requestBody['payments[$i][type]'] = payment.type!;
|
||||
requestBody['payments[$i][amount]'] = paymentAmount.toString();
|
||||
|
||||
if (payment.type == 'cheque' && payment.chequeNumberController.text.isNotEmpty) {
|
||||
requestBody['payments[$i][cheque_number]'] = payment.chequeNumberController.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: requestBody, // Send the Map directly
|
||||
// Set to false to send as x-www-form-urlencoded
|
||||
addContentTypeInHeader: false,
|
||||
);
|
||||
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (responseData.statusCode == 200 || responseData.statusCode == 201) {
|
||||
// Refresh income-related providers
|
||||
|
||||
Navigator.pop(context, true);
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(parsedData['message'] ?? 'Income created successfully')));
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text('Income creation failed: ${parsedData['message']}')));
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
}
|
||||
316
lib/Screens/Income/add_income.dart
Normal file
316
lib/Screens/Income/add_income.dart
Normal file
@@ -0,0 +1,316 @@
|
||||
// ignore_for_file: unused_result
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Income/Model/income_category.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../../widgets/multipal payment mathods/multi_payment_widget.dart';
|
||||
import 'Repo/income_repo.dart';
|
||||
import 'income_category_list.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class AddIncome extends ConsumerStatefulWidget {
|
||||
const AddIncome({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_AddIncomeState createState() => _AddIncomeState();
|
||||
}
|
||||
|
||||
class _AddIncomeState extends ConsumerState<AddIncome> {
|
||||
IncomeCategory? selectedCategory;
|
||||
final dateController = TextEditingController();
|
||||
TextEditingController incomeForNameController = TextEditingController();
|
||||
TextEditingController incomeAmountController = TextEditingController();
|
||||
TextEditingController incomeNoteController = TextEditingController();
|
||||
TextEditingController incomeRefController = TextEditingController();
|
||||
|
||||
final GlobalKey<MultiPaymentWidgetState> _paymentKey = GlobalKey<MultiPaymentWidgetState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
dateController.dispose();
|
||||
incomeForNameController.dispose();
|
||||
incomeAmountController.dispose();
|
||||
incomeNoteController.dispose();
|
||||
incomeRefController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
DateTime selectedDate = DateTime.now();
|
||||
|
||||
Future<void> _selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context, initialDate: selectedDate, firstDate: DateTime(2015, 8), lastDate: DateTime(2101));
|
||||
if (picked != null && picked != selectedDate) {
|
||||
setState(() {
|
||||
selectedDate = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
bool validateAndSave() {
|
||||
final form = formKey.currentState;
|
||||
if (form!.validate()) {
|
||||
form.save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissionService = PermissionService(ref);
|
||||
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).addIncome,
|
||||
),
|
||||
centerTitle: true,
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: context.width(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
///_______date________________________________
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: FormField(
|
||||
builder: (FormFieldState<dynamic> field) {
|
||||
return InputDecorator(
|
||||
decoration: kInputDecoration.copyWith(
|
||||
suffixIcon: const Icon(IconlyLight.calendar, color: kGreyTextColor),
|
||||
contentPadding: const EdgeInsets.all(8),
|
||||
labelText: lang.S.of(context).incomeDate,
|
||||
hintText: lang.S.of(context).enterExpenseDate,
|
||||
),
|
||||
child: Text(
|
||||
'${DateFormat.d().format(selectedDate)} ${DateFormat.MMM().format(selectedDate)} ${DateFormat.y().format(selectedDate)}',
|
||||
),
|
||||
);
|
||||
},
|
||||
).onTap(() => _selectDate(context)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_________category_______________________________________________
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
controller: TextEditingController(
|
||||
text: selectedCategory?.categoryName ?? lang.S.of(context).selectCategory),
|
||||
onTap: () async {
|
||||
selectedCategory = await const IncomeCategoryList().launch(context);
|
||||
setState(() {});
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 14.0, horizontal: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(color: kBorderColor, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderSide: BorderSide(color: kBorderColor, width: 1),
|
||||
),
|
||||
suffixIcon: const Icon(Icons.keyboard_arrow_down),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///________Income_for_______________________________________________
|
||||
TextFormField(
|
||||
showCursor: true,
|
||||
controller: incomeForNameController,
|
||||
validator: (value) {
|
||||
if (value.isEmptyOrNull) {
|
||||
return lang.S.of(context).pleaseEnterName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
incomeForNameController.text = value!;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).incomeFor,
|
||||
hintText: lang.S.of(context).enterName,
|
||||
),
|
||||
),
|
||||
const SizedBox.square(dimension: 20),
|
||||
|
||||
///_________________Total Amount_____________________________
|
||||
TextFormField(
|
||||
controller: incomeAmountController,
|
||||
readOnly: (_paymentKey.currentState?.getPaymentEntries().length ?? 1) > 1,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
validator: (value) {
|
||||
if (value.isEmptyOrNull) {
|
||||
return lang.S.of(context).pleaseEnterAmount;
|
||||
}
|
||||
final total = double.tryParse(value ?? '') ?? 0.0;
|
||||
if (total <= 0) {
|
||||
return lang.S.of(context).amountMustBeGreaterThanZero;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
fillColor: (_paymentKey.currentState?.getPaymentEntries().length ?? 1) > 1
|
||||
? Colors.grey.shade100
|
||||
: Colors.white,
|
||||
filled: true,
|
||||
border: const OutlineInputBorder(),
|
||||
errorBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.red),
|
||||
),
|
||||
labelText: lang.S.of(context).amount,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
hintText: '0.00',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_______reference_________________________________
|
||||
TextFormField(
|
||||
showCursor: true,
|
||||
controller: incomeRefController,
|
||||
validator: (value) {
|
||||
return null; // Reference is optional
|
||||
},
|
||||
onSaved: (value) {
|
||||
incomeRefController.text = value!;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).referenceNo,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
hintText: lang.S.of(context).enterRefNumber,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_________note____________________________________________________
|
||||
TextFormField(
|
||||
showCursor: true,
|
||||
controller: incomeNoteController,
|
||||
validator: (value) {
|
||||
return null; // Note is optional
|
||||
},
|
||||
onSaved: (value) {
|
||||
incomeNoteController.text = value!;
|
||||
},
|
||||
decoration: kInputDecoration.copyWith(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: lang.S.of(context).note,
|
||||
hintText: lang.S.of(context).enterNote,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
MultiPaymentWidget(
|
||||
key: _paymentKey,
|
||||
totalAmountController: incomeAmountController,
|
||||
showChequeOption: true,
|
||||
onPaymentListChanged: () {
|
||||
setState(() {
|
||||
// Rebuild to update readOnly status of amount field
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
///_______button_________________________________
|
||||
ElevatedButton.icon(
|
||||
iconAlignment: IconAlignment.end,
|
||||
onPressed: () async {
|
||||
if (validateAndSave()) {
|
||||
if (!permissionService.hasPermission(Permit.incomesCreate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDonNotHavePermissionToCreateIncome),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final totalIncome = double.tryParse(incomeAmountController.text) ?? 0.0;
|
||||
final payments = _paymentKey.currentState?.getPaymentEntries();
|
||||
|
||||
if (selectedCategory == null) {
|
||||
EasyLoading.showError(lang.S.of(context).pleaseSelectACategory);
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalIncome <= 0) {
|
||||
EasyLoading.showError(lang.S.of(context).amountMustBeGreaterThanZero);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payments == null || payments.isEmpty) {
|
||||
EasyLoading.showError(lang.S.of(context).canNotRetrievePaymentDetails);
|
||||
return;
|
||||
}
|
||||
|
||||
EasyLoading.show();
|
||||
IncomeRepo repo = IncomeRepo();
|
||||
|
||||
await repo.createIncome(
|
||||
ref: ref,
|
||||
context: context,
|
||||
amount: totalIncome,
|
||||
incomeCategoryId: selectedCategory?.id ?? 0,
|
||||
incomeFor: incomeForNameController.text,
|
||||
referenceNo: incomeRefController.text,
|
||||
incomeDate: selectedDate.toString(),
|
||||
note: incomeNoteController.text,
|
||||
payments: payments, // Pass the payment list
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: Text(lang.S.of(context).continueButton),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
114
lib/Screens/Income/add_income_category.dart
Normal file
114
lib/Screens/Income/add_income_category.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
// ignore_for_file: unused_result
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Income/Repo/income_category_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
|
||||
class AddIncomeCategory extends StatefulWidget {
|
||||
const AddIncomeCategory({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_AddIncomeCategoryState createState() => _AddIncomeCategoryState();
|
||||
}
|
||||
|
||||
class _AddIncomeCategoryState extends State<AddIncomeCategory> {
|
||||
bool showProgress = false;
|
||||
|
||||
TextEditingController nameController = TextEditingController();
|
||||
GlobalKey<FormState> key = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer(builder: (context, ref, __) {
|
||||
//final allCategory = ref.watch(expanseCategoryProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
icon: const Icon(Icons.close)),
|
||||
title: Text(
|
||||
lang.S.of(context).addIncomeCategory,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: showProgress,
|
||||
child: const CircularProgressIndicator(
|
||||
color: kMainColor,
|
||||
strokeWidth: 5.0,
|
||||
),
|
||||
),
|
||||
Form(
|
||||
key: key,
|
||||
child: TextFormField(
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmptyOrNull ?? true) {
|
||||
//return 'Enter expanse category name';
|
||||
return lang.S.of(context).enterIncomeCategoryName;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).categoryName,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (!permissionService.hasPermission(Permit.incomeCategoriesCreate.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text(lang.S.of(context).youDoNotHavePermissionToCreateIncomeCategory),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (key.currentState?.validate() ?? false) {
|
||||
EasyLoading.show();
|
||||
final incomeRepo = IncomeCategoryRepo();
|
||||
await incomeRepo.addIncomeCategory(
|
||||
ref: ref,
|
||||
context: context,
|
||||
categoryName: nameController.text.trim(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
154
lib/Screens/Income/income_category_list.dart
Normal file
154
lib/Screens/Income/income_category_list.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import 'Providers/income_category_provider.dart';
|
||||
import 'add_income_category.dart';
|
||||
|
||||
class IncomeCategoryList extends StatefulWidget {
|
||||
const IncomeCategoryList({Key? key, this.mainContext}) : super(key: key);
|
||||
|
||||
final BuildContext? mainContext;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_IncomeCategoryListState createState() => _IncomeCategoryListState();
|
||||
}
|
||||
|
||||
class _IncomeCategoryListState extends State<IncomeCategoryList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Consumer(builder: (context, ref, _) {
|
||||
final data = ref.watch(incomeCategoryProvider);
|
||||
final permissionService = PermissionService(ref);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).incomeCategories,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: AppTextField(
|
||||
textFieldType: TextFieldType.NAME,
|
||||
decoration: InputDecoration(
|
||||
hintText: lang.S.of(context).search,
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: kGreyTextColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10.0,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
const AddIncomeCategory().launch(context);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 20.0, right: 20.0),
|
||||
height: 48.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(color: kBorderColor),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
data.when(data: (data) {
|
||||
if (!permissionService.hasPermission(Permit.incomeCategoriesRead.value)) {
|
||||
return Center(child: PermitDenyWidget());
|
||||
}
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemCount: data.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
data[index].categoryName ?? '',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kBackgroundColor,
|
||||
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 12),
|
||||
minimumSize: Size(
|
||||
50,
|
||||
25,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
lang.S.of(context).select,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
//'Select',
|
||||
onPressed: () {
|
||||
// const AddExpense().launch(context);
|
||||
Navigator.pop(
|
||||
context,
|
||||
data[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}, error: (error, stackTrace) {
|
||||
return Text(error.toString());
|
||||
}, loading: () {
|
||||
return const CircularProgressIndicator();
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
349
lib/Screens/Income/income_list.dart
Normal file
349
lib/Screens/Income/income_list.dart
Normal file
@@ -0,0 +1,349 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||
// import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
// import 'package:intl/intl.dart';
|
||||
// import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
// import 'package:mobile_pos/Screens/Income/Providers/all_income_provider.dart';
|
||||
// import 'package:mobile_pos/Screens/Income/Providers/income_category_provider.dart';
|
||||
// import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// import 'package:nb_utils/nb_utils.dart';
|
||||
//
|
||||
// import '../../GlobalComponents/glonal_popup.dart';
|
||||
// import '../../constant.dart';
|
||||
// import '../../currency.dart';
|
||||
// import '../../http_client/custome_http_client.dart';
|
||||
// import '../../service/check_actions_when_no_branch.dart';
|
||||
// import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
// import '../../service/check_user_role_permission_provider.dart';
|
||||
// import 'add_income.dart';
|
||||
//
|
||||
// class IncomeList extends StatefulWidget {
|
||||
// const IncomeList({super.key});
|
||||
//
|
||||
// static bool isDateInRange({
|
||||
// required String? incomeDate,
|
||||
// required DateTime fromDate,
|
||||
// required DateTime toDate,
|
||||
// }) {
|
||||
// try {
|
||||
// final parsedDate = DateTime.tryParse(incomeDate?.substring(0, 10) ?? '');
|
||||
// if (parsedDate == null) return false;
|
||||
// final toDateOnly = DateTime.parse(toDate.toString().substring(0, 10));
|
||||
//
|
||||
// final isAfterOrSameFrom = parsedDate.isAfter(fromDate) || parsedDate.isAtSameMomentAs(fromDate);
|
||||
//
|
||||
// final isBeforeOrSameTo = parsedDate.isBefore(toDateOnly) || parsedDate.isAtSameMomentAs(toDateOnly);
|
||||
//
|
||||
// return isAfterOrSameFrom && isBeforeOrSameTo;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// _IncomeListState createState() => _IncomeListState();
|
||||
// }
|
||||
//
|
||||
// class _IncomeListState extends State<IncomeList> {
|
||||
// final dateController = TextEditingController();
|
||||
// TextEditingController fromDateTextEditingController = TextEditingController(text: DateFormat.yMMMd().format(DateTime(2021)));
|
||||
// TextEditingController toDateTextEditingController = TextEditingController(text: DateFormat.yMMMd().format(DateTime.now()));
|
||||
// DateTime fromDate = DateTime(2021);
|
||||
// DateTime toDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day);
|
||||
// num totalExpense = 0;
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// dateController.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
//
|
||||
// bool _isRefreshing = false;
|
||||
//
|
||||
// Future<void> refreshData(WidgetRef ref) async {
|
||||
// if (_isRefreshing) return;
|
||||
// _isRefreshing = true;
|
||||
//
|
||||
// ref.refresh(incomeDurationProvider);
|
||||
// ref.refresh(incomeCategoryProvider);
|
||||
//
|
||||
// await Future.delayed(const Duration(seconds: 1));
|
||||
// _isRefreshing = false;
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// totalExpense = 0;
|
||||
// return Consumer(builder: (context, ref, __) {
|
||||
// final incomeData = ref.watch(incomeProvider);
|
||||
// final businessInfoData = ref.watch(businessInfoProvider);
|
||||
// final permissionService = PermissionService(ref);
|
||||
// return GlobalPopup(
|
||||
// child: Scaffold(
|
||||
// backgroundColor: kWhite,
|
||||
// appBar: AppBar(
|
||||
// title: Text(
|
||||
// lang.S.of(context).incomeReport,
|
||||
// ),
|
||||
// iconTheme: const IconThemeData(color: Colors.black),
|
||||
// centerTitle: true,
|
||||
// backgroundColor: Colors.white,
|
||||
// elevation: 0.0,
|
||||
// ),
|
||||
// body: RefreshIndicator(
|
||||
// onRefresh: () => refreshData(ref),
|
||||
// child: SingleChildScrollView(
|
||||
// physics: const AlwaysScrollableScrollPhysics(),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// if (permissionService.hasPermission(Permit.incomesRead.value)) ...{
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.only(right: 10.0, left: 10.0, top: 10, bottom: 10),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: AppTextField(
|
||||
// textFieldType: TextFieldType.NAME,
|
||||
// readOnly: true,
|
||||
// controller: fromDateTextEditingController,
|
||||
// decoration: InputDecoration(
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).fromDate,
|
||||
// border: const OutlineInputBorder(),
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () async {
|
||||
// final DateTime? picked = await showDatePicker(
|
||||
// initialDate: DateTime.now(),
|
||||
// firstDate: DateTime(2015, 8),
|
||||
// lastDate: DateTime(2101),
|
||||
// context: context,
|
||||
// );
|
||||
// setState(() {
|
||||
// fromDateTextEditingController.text = DateFormat.yMMMd().format(picked ?? DateTime.now());
|
||||
// fromDate = picked!;
|
||||
// totalExpense = 0;
|
||||
// });
|
||||
// },
|
||||
// icon: const Icon(FeatherIcons.calendar),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
// Expanded(
|
||||
// child: AppTextField(
|
||||
// textFieldType: TextFieldType.NAME,
|
||||
// readOnly: true,
|
||||
// controller: toDateTextEditingController,
|
||||
// decoration: InputDecoration(
|
||||
// floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
// labelText: lang.S.of(context).toDate,
|
||||
// border: const OutlineInputBorder(),
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () async {
|
||||
// final DateTime? picked = await showDatePicker(
|
||||
// initialDate: toDate,
|
||||
// firstDate: DateTime(2015, 8),
|
||||
// lastDate: DateTime(2101),
|
||||
// context: context,
|
||||
// );
|
||||
//
|
||||
// setState(() {
|
||||
// toDateTextEditingController.text = DateFormat.yMMMd().format(picked ?? DateTime.now());
|
||||
// picked!.isToday ? toDate = DateTime.now() : toDate = picked;
|
||||
// totalExpense = 0;
|
||||
// });
|
||||
// },
|
||||
// icon: const Icon(FeatherIcons.calendar),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// ///__________expense_data_table____________________________________________
|
||||
// Container(
|
||||
// width: context.width(),
|
||||
// height: 50,
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// decoration: const BoxDecoration(color: kDarkWhite),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: 130,
|
||||
// child: Text(
|
||||
// lang.S.of(context).incomeFor,
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 100,
|
||||
// child: Text(lang.S.of(context).date),
|
||||
// ),
|
||||
// Container(
|
||||
// alignment: Alignment.centerRight,
|
||||
// width: 70,
|
||||
// child: Text(lang.S.of(context).amount),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
//
|
||||
// incomeData.when(data: (mainData) {
|
||||
// if (mainData.isNotEmpty) {
|
||||
// totalExpense = 0;
|
||||
// for (var income in mainData) {
|
||||
// final result = IncomeList.isDateInRange(
|
||||
// incomeDate: income.incomeDate,
|
||||
// fromDate: fromDate,
|
||||
// toDate: toDate,
|
||||
// );
|
||||
// if (result) {
|
||||
// totalExpense += income.amount ?? 0;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return SizedBox(
|
||||
// width: context.width(),
|
||||
// child: ListView.builder(
|
||||
// shrinkWrap: true,
|
||||
// itemCount: mainData.length,
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// itemBuilder: (BuildContext context, int index) {
|
||||
// return Visibility(
|
||||
// visible: IncomeList.isDateInRange(
|
||||
// incomeDate: mainData[index].incomeDate ?? (mainData[index].createdAt ?? ''),
|
||||
// fromDate: fromDate,
|
||||
// toDate: toDate,
|
||||
// ),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// width: 130,
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Text(
|
||||
// mainData[index].incomeFor ?? '',
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// ),
|
||||
// const SizedBox(height: 5),
|
||||
// Text(
|
||||
// mainData[index].category?.categoryName ?? '',
|
||||
// maxLines: 2,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// style: const TextStyle(color: Colors.grey, fontSize: 11),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 100,
|
||||
// child: Text(
|
||||
// DateTime.tryParse(mainData[index].incomeDate ?? '') == null
|
||||
// ? ""
|
||||
// : DateFormat.yMMMd().format(DateTime.parse(mainData[index].incomeDate ?? '')),
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// alignment: Alignment.centerRight,
|
||||
// width: 70,
|
||||
// child: Text("$currency${mainData[index].amount.toString()}"),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// Container(
|
||||
// height: 1,
|
||||
// color: Colors.black12,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(20),
|
||||
// child: Center(
|
||||
// child: Text(lang.S.of(context).noData),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }, error: (Object error, StackTrace? stackTrace) {
|
||||
// return Text(error.toString());
|
||||
// }, loading: () {
|
||||
// return const Center(child: CircularProgressIndicator());
|
||||
// }),
|
||||
// } else
|
||||
// Center(child: PermitDenyWidget()),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// bottomNavigationBar: Padding(
|
||||
// padding: const EdgeInsets.all(10.0),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// ///_________total______________________________________________
|
||||
// if (permissionService.hasPermission(Permit.incomesRead.value))
|
||||
// Container(
|
||||
// height: 50,
|
||||
// padding: const EdgeInsets.all(10),
|
||||
// decoration: const BoxDecoration(color: kDarkWhite),
|
||||
// child: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text(
|
||||
// lang.S.of(context).totalIncome,
|
||||
// ),
|
||||
// Text('$currency$totalExpense')
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 10),
|
||||
//
|
||||
// ///________button________________________________________________
|
||||
// businessInfoData.when(data: (details) {
|
||||
// return ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
// if (!result) {
|
||||
// return;
|
||||
// }
|
||||
// const AddIncome().launch(context);
|
||||
// },
|
||||
// child: Text(lang.S.of(context).addIncome),
|
||||
// );
|
||||
// }, error: (e, stack) {
|
||||
// return Text(e.toString());
|
||||
// }, loading: () {
|
||||
// return const Center(
|
||||
// child: CircularProgressIndicator(),
|
||||
// );
|
||||
// })
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
662
lib/Screens/Loss_Profit/loss_profit_screen.dart
Normal file
662
lib/Screens/Loss_Profit/loss_profit_screen.dart
Normal file
@@ -0,0 +1,662 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Provider/transactions_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:mobile_pos/pdf_report/loss_profit_report/loss_profit_pdf.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
import '../../../Provider/profile_provider.dart';
|
||||
import '../../../constant.dart';
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../core/theme/_app_colors.dart';
|
||||
import '../../currency.dart';
|
||||
import '../Home/home.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
|
||||
class LossProfitScreen extends ConsumerStatefulWidget {
|
||||
const LossProfitScreen({super.key, this.fromReport});
|
||||
|
||||
final bool? fromReport;
|
||||
|
||||
@override
|
||||
ConsumerState<LossProfitScreen> createState() => _LossProfitScreenState();
|
||||
}
|
||||
|
||||
class _LossProfitScreenState extends ConsumerState<LossProfitScreen> {
|
||||
final TextEditingController fromDateController = TextEditingController();
|
||||
final TextEditingController toDateController = TextEditingController();
|
||||
|
||||
final Map<String, String> dateOptions = {
|
||||
'today': l.S.current.today,
|
||||
'yesterday': l.S.current.yesterday,
|
||||
'last_seven_days': l.S.current.last7Days,
|
||||
'last_thirty_days': l.S.current.last30Days,
|
||||
'current_month': l.S.current.currentMonth,
|
||||
'last_month': l.S.current.lastMonth,
|
||||
'current_year': l.S.current.currentYear,
|
||||
'custom_date': l.S.current.customerDate,
|
||||
};
|
||||
|
||||
String selectedTime = 'today';
|
||||
bool _isRefreshing = false;
|
||||
bool _showCustomDatePickers = false;
|
||||
|
||||
DateTime? fromDate;
|
||||
DateTime? toDate;
|
||||
String searchCustomer = '';
|
||||
|
||||
/// Generates the date range string for the provider
|
||||
FilterModel _getDateRangeFilter() {
|
||||
if (_showCustomDatePickers && fromDate != null && toDate != null) {
|
||||
return FilterModel(
|
||||
duration: 'custom_date',
|
||||
fromDate: DateFormat('yyyy-MM-dd', 'en_US').format(fromDate!),
|
||||
toDate: DateFormat('yyyy-MM-dd', 'en_US').format(toDate!),
|
||||
);
|
||||
} else {
|
||||
return FilterModel(duration: selectedTime.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectDate({
|
||||
required BuildContext context,
|
||||
required bool isFrom,
|
||||
}) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
firstDate: DateTime(2021),
|
||||
lastDate: DateTime.now(),
|
||||
initialDate: isFrom ? fromDate ?? DateTime.now() : toDate ?? DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
if (isFrom) {
|
||||
fromDate = picked;
|
||||
fromDateController.text = DateFormat('yyyy-MM-dd').format(picked);
|
||||
} else {
|
||||
toDate = picked;
|
||||
toDateController.text = DateFormat('yyyy-MM-dd').format(picked);
|
||||
}
|
||||
});
|
||||
|
||||
if (fromDate != null && toDate != null) _refreshFilteredProvider();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshFilteredProvider() async {
|
||||
if (_isRefreshing) return;
|
||||
_isRefreshing = true;
|
||||
try {
|
||||
final filter = _getDateRangeFilter();
|
||||
ref.refresh(filteredSaleProvider(filter));
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // small delay
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
fromDateController.dispose();
|
||||
toDateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateDateUI(DateTime? from, DateTime? to) {
|
||||
setState(() {
|
||||
fromDate = from;
|
||||
toDate = to;
|
||||
|
||||
fromDateController.text = from != null ? DateFormat('yyyy-MM-dd').format(from) : '';
|
||||
|
||||
toDateController.text = to != null ? DateFormat('yyyy-MM-dd').format(to) : '';
|
||||
});
|
||||
}
|
||||
|
||||
void _setDateRangeFromDropdown(String value) {
|
||||
final now = DateTime.now();
|
||||
|
||||
switch (value) {
|
||||
case 'today':
|
||||
_updateDateUI(now, now);
|
||||
break;
|
||||
|
||||
case 'yesterday':
|
||||
final y = now.subtract(const Duration(days: 1));
|
||||
_updateDateUI(y, y);
|
||||
break;
|
||||
|
||||
case 'last_seven_days':
|
||||
_updateDateUI(
|
||||
now.subtract(const Duration(days: 6)),
|
||||
now,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'last_thirty_days':
|
||||
_updateDateUI(
|
||||
now.subtract(const Duration(days: 29)),
|
||||
now,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'current_month':
|
||||
_updateDateUI(
|
||||
DateTime(now.year, now.month, 1),
|
||||
now,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'last_month':
|
||||
final first = DateTime(now.year, now.month - 1, 1);
|
||||
final last = DateTime(now.year, now.month, 0);
|
||||
_updateDateUI(first, last);
|
||||
break;
|
||||
|
||||
case 'current_year':
|
||||
_updateDateUI(
|
||||
DateTime(now.year, 1, 1),
|
||||
now,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'custom_date':
|
||||
// Custom: User will select manually
|
||||
_updateDateUI(null, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
// Set initial From and To date = TODAY
|
||||
fromDate = now;
|
||||
toDate = now;
|
||||
|
||||
fromDateController.text = DateFormat('yyyy-MM-dd').format(now);
|
||||
toDateController.text = DateFormat('yyyy-MM-dd').format(now);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return await const Home().launch(context, isNewTask: true);
|
||||
},
|
||||
child: Consumer(
|
||||
builder: (_, ref, watch) {
|
||||
final providerData = ref.watch(filteredLossProfitProvider(_getDateRangeFilter()));
|
||||
final personalData = ref.watch(businessInfoProvider);
|
||||
return personalData.when(
|
||||
data: (business) {
|
||||
return providerData.when(
|
||||
data: (transaction) {
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
(widget.fromReport ?? false) ? _lang.profitAndLoss : _lang.profitAndLoss,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if ((transaction.expenseSummary?.isNotEmpty == true) ||
|
||||
(transaction.incomeSummary?.isNotEmpty == true)) {
|
||||
generateLossProfitReportPdf(context, transaction, business, fromDate, toDate);
|
||||
} else {
|
||||
EasyLoading.showError(_lang.listIsEmpty);
|
||||
}
|
||||
},
|
||||
icon: HugeIcon(icon: HugeIcons.strokeRoundedPdf02, color: kSecondayColor),
|
||||
),
|
||||
|
||||
/*
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (!permissionService.hasPermission(Permit.lossProfitsRead.value)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.red,
|
||||
content: Text('You do not have permission of loss profit.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if ((transaction.expenseSummary?.isNotEmpty == true) ||
|
||||
(transaction.incomeSummary?.isNotEmpty == true)) {
|
||||
generateLossProfitReportExcel(context, transaction, business, fromDate, toDate);
|
||||
} else {
|
||||
EasyLoading.showError('List is empty');
|
||||
}
|
||||
},
|
||||
icon: SvgPicture.asset('assets/excel.svg'),
|
||||
),
|
||||
*/
|
||||
SizedBox(width: 8),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(50),
|
||||
child: Column(
|
||||
children: [
|
||||
Divider(thickness: 1, color: kBottomBorder, height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(IconlyLight.calendar, color: kPeraColor, size: 20),
|
||||
SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (_showCustomDatePickers) {
|
||||
_selectDate(context: context, isFrom: true);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
fromDate != null ? DateFormat('dd MMM yyyy').format(fromDate!) : 'From',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
l.S.of(context).to,
|
||||
style: _theme.textTheme.titleSmall,
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (_showCustomDatePickers) {
|
||||
_selectDate(context: context, isFrom: false);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
toDate != null ? DateFormat('dd MMM yyyy').format(toDate!) : 'To',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2),
|
||||
RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Container(
|
||||
height: 1,
|
||||
width: 20,
|
||||
color: kSubPeraColor,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 2),
|
||||
Expanded(
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
iconSize: 20,
|
||||
value: selectedTime,
|
||||
isExpanded: true,
|
||||
items: dateOptions.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
entry.value,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
|
||||
setState(() {
|
||||
selectedTime = value;
|
||||
_showCustomDatePickers = value == 'custom_date';
|
||||
});
|
||||
|
||||
if (value != 'custom_date') {
|
||||
_setDateRangeFromDropdown(value);
|
||||
_refreshFilteredProvider();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(thickness: 1, color: kBottomBorder, height: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refreshFilteredProvider,
|
||||
child: Column(
|
||||
children: [
|
||||
// Overview Containers
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 77,
|
||||
width: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: kSuccessColor.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"$currency${formatPointNumber(transaction.cartGrossProfit ?? 0, addComma: true)}",
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_lang.grossProfit,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Container(
|
||||
height: 77,
|
||||
width: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: DAppColors.kError.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"$currency${formatPointNumber(transaction.totalCardExpense ?? 0, addComma: true)}",
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_lang.expense,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Container(
|
||||
height: 77,
|
||||
width: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: DAppColors.kError.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"$currency${formatPointNumber(transaction.cardNetProfit ?? 0, addComma: true)}",
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_lang.netProfit,
|
||||
style: _theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Data
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
// Income Type
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF7F7F7),
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(_lang.name)),
|
||||
Flexible(flex: 0, child: Text(_lang.amount, textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Sub Header
|
||||
DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Text(_lang.incomeType),
|
||||
),
|
||||
),
|
||||
|
||||
// Item
|
||||
...?transaction.incomeSummary?.map((incomeType) {
|
||||
return DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(incomeType.type ?? 'N/A')),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Text(
|
||||
"$currency${formatPointNumber(incomeType.totalIncome ?? 0, addComma: true)}",
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Footer
|
||||
DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xff06A82F),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff06A82F).withValues(alpha: 0.15),
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(_lang.grossProfit)),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Text(
|
||||
"$currency${formatPointNumber(transaction.grossIncomeProfit ?? 0, addComma: true)}",
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Expense Type
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Sub Header
|
||||
DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Text(_lang.expensesType),
|
||||
),
|
||||
),
|
||||
|
||||
// Item
|
||||
...?transaction.expenseSummary?.map((incomeType) {
|
||||
return DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(incomeType.type ?? 'N/A')),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Text(
|
||||
"$currency${formatPointNumber(incomeType.totalExpense ?? 0, addComma: true)}",
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Footer
|
||||
DefaultTextStyle.merge(
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: const Color(0xffC52127),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffC52127).withValues(alpha: 0.15),
|
||||
border: Border(bottom: Divider.createBorderSide(context)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(_lang.totalExpense)),
|
||||
Flexible(
|
||||
flex: 0,
|
||||
child: Text(
|
||||
"$currency${formatPointNumber(transaction.totalExpenses ?? 0, addComma: true)}",
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Text(
|
||||
'${_lang.netProfit} (${_lang.income} - ${_lang.expense}) =$currency${formatPointNumber(transaction.netProfit ?? 0, addComma: true)}',
|
||||
textAlign: TextAlign.center,
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
loading: () => Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
},
|
||||
error: (e, stack) {
|
||||
print('-----------------${'I Found the error'}-----------------');
|
||||
return Center(child: Text(e.toString()));
|
||||
},
|
||||
loading: () => Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
316
lib/Screens/Loss_Profit/single_loss_profit_screen.dart
Normal file
316
lib/Screens/Loss_Profit/single_loss_profit_screen.dart
Normal file
@@ -0,0 +1,316 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/add%20product/add_product.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
import '../../currency.dart';
|
||||
import '../../http_client/custome_http_client.dart';
|
||||
import '../../model/sale_transaction_model.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../Products/add product/modle/create_product_model.dart';
|
||||
|
||||
class SingleLossProfitScreen extends ConsumerStatefulWidget {
|
||||
const SingleLossProfitScreen({
|
||||
super.key,
|
||||
required this.transactionModel,
|
||||
});
|
||||
|
||||
final SalesTransactionModel transactionModel;
|
||||
|
||||
@override
|
||||
ConsumerState<SingleLossProfitScreen> createState() => _SingleLossProfitScreenState();
|
||||
}
|
||||
|
||||
class _SingleLossProfitScreenState extends ConsumerState<SingleLossProfitScreen> {
|
||||
double getTotalProfit() {
|
||||
double totalProfit = 0;
|
||||
for (var element in widget.transactionModel.salesDetails!) {
|
||||
if (!element.lossProfit!.isNegative) {
|
||||
totalProfit = totalProfit + element.lossProfit!;
|
||||
}
|
||||
}
|
||||
|
||||
return totalProfit;
|
||||
}
|
||||
|
||||
double getTotalLoss() {
|
||||
double totalLoss = 0;
|
||||
for (var element in widget.transactionModel.salesDetails!) {
|
||||
if (element.lossProfit!.isNegative) {
|
||||
totalLoss = totalLoss + element.lossProfit!.abs();
|
||||
}
|
||||
}
|
||||
|
||||
return totalLoss;
|
||||
}
|
||||
|
||||
num getTotalQuantity() {
|
||||
num total = 0;
|
||||
for (var element in widget.transactionModel.salesDetails!) {
|
||||
total += element.quantities ?? 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final permissionService = PermissionService(ref);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
lang.S.of(context).lpDetails,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (permissionService.hasPermission(Permit.lossProfitsDetailsRead.value)) ...{
|
||||
Text('${lang.S.of(context).invoice} #${widget.transactionModel.invoiceNumber}'),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.transactionModel.party?.name ?? '',
|
||||
maxLines: 2,
|
||||
)),
|
||||
Text(
|
||||
"${lang.S.of(context).dates} ${DateFormat.yMMMd().format(
|
||||
DateTime.parse(widget.transactionModel.saleDate ?? ''),
|
||||
)}",
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${lang.S.of(context).mobile}${widget.transactionModel.party?.phone ?? ''}",
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
Text(
|
||||
DateFormat.jm().format(DateTime.parse(widget.transactionModel.saleDate ?? '')),
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
color: kMainColor.withOpacity(0.2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
lang.S.of(context).product,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
lang.S.of(context).quantity,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
lang.S.of(context).profit,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).loss,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
itemCount: widget.transactionModel.salesDetails!.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'${widget.transactionModel.salesDetails?[index].product?.productName.toString() ?? ''}${widget.transactionModel.salesDetails?[index].product?.productType == ProductType.variant.name ? ' [${widget.transactionModel.salesDetails?[index].stock?.batchNo}]' : ''}',
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.transactionModel.salesDetails?[index].quantities.toString() ?? '',
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Center(
|
||||
child: Text(
|
||||
!(widget.transactionModel.salesDetails?[index].lossProfit?.isNegative ?? false)
|
||||
? "$currency${widget.transactionModel.salesDetails?[index].lossProfit!.abs().toString()}"
|
||||
: '0',
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
(widget.transactionModel.salesDetails?[index].lossProfit?.isNegative ?? false)
|
||||
? "$currency${widget.transactionModel.salesDetails?[index].lossProfit!.abs().toString()}"
|
||||
: '0',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
} else
|
||||
Center(child: PermitDenyWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Visibility(
|
||||
visible: permissionService.hasPermission(Permit.lossProfitsDetailsRead.value),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withOpacity(0.2),
|
||||
border: const Border(bottom: BorderSide(width: 1, color: Colors.grey))),
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 15, right: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
lang.S.of(context).total,
|
||||
textAlign: TextAlign.start,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
formatPointNumber(getTotalQuantity()),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
"$currency${getTotalProfit()}",
|
||||
)),
|
||||
Text(
|
||||
"$currency${getTotalLoss()}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withOpacity(0.2),
|
||||
border: const Border(bottom: BorderSide(width: 1, color: Colors.grey))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 15, right: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
lang.S.of(context).discount,
|
||||
textAlign: TextAlign.start,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"$currency${widget.transactionModel.discountAmount ?? 0}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withOpacity(0.2),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 15, right: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
widget.transactionModel.detailsSumLossProfit!.isNegative
|
||||
? lang.S.of(context).totalLoss
|
||||
: lang.S.of(context).totalProfit,
|
||||
textAlign: TextAlign.start,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.transactionModel.detailsSumLossProfit!.isNegative
|
||||
? "$currency${widget.transactionModel.detailsSumLossProfit!.toInt().abs()}"
|
||||
: "$currency${widget.transactionModel.detailsSumLossProfit!.toInt()}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
132
lib/Screens/Marketing/edit_social_media.dart
Normal file
132
lib/Screens/Marketing/edit_social_media.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../constant.dart';
|
||||
|
||||
class EditSocialmedia extends StatefulWidget {
|
||||
const EditSocialmedia({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_EditSocialmediaState createState() => _EditSocialmediaState();
|
||||
}
|
||||
|
||||
class _EditSocialmediaState extends State<EditSocialmedia> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).editSocailMedia,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaEditCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/fb.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).facebook,
|
||||
//'Facebook',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaEditCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/twitter.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).twitter,
|
||||
// 'Twitter',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaEditCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/insta.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).instagram,
|
||||
//'Instagram',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaEditCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/linkedin.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).linkedIN,
|
||||
//'LinkedIN',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class SocialMediaEditCard extends StatelessWidget {
|
||||
SocialMediaEditCard({
|
||||
Key? key,
|
||||
required this.iconWidget,
|
||||
required this.socialMediaName,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget iconWidget;
|
||||
final String socialMediaName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
iconWidget,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
socialMediaName,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 95,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(top: 5.0, bottom: 5.0),
|
||||
decoration: kButtonDecoration.copyWith(color: kMainColor),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.add,
|
||||
color: Colors.white,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).link,
|
||||
//'Link',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 30.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
160
lib/Screens/Marketing/marketing_screen.dart
Normal file
160
lib/Screens/Marketing/marketing_screen.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/Screens/Marketing/edit_social_media.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../constant.dart';
|
||||
|
||||
class MarketingScreen extends StatefulWidget {
|
||||
const MarketingScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_MarketingScreenState createState() => _MarketingScreenState();
|
||||
}
|
||||
|
||||
class _MarketingScreenState extends State<MarketingScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).socialMarketing,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 15.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
const EditSocialmedia().launch(context);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.edit,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 5.0,
|
||||
),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10.0,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/fb.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).facebook,
|
||||
//'Facebook',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/twitter.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).twitter,
|
||||
//'Twitter',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/insta.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).instagram,
|
||||
//'Instagram',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10.0),
|
||||
child: SocialMediaCard(
|
||||
iconWidget: const Image(
|
||||
image: AssetImage('images/linkedin.png'),
|
||||
),
|
||||
socialMediaName: lang.S.of(context).linkedIN,
|
||||
// 'LinkedIN',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class SocialMediaCard extends StatelessWidget {
|
||||
SocialMediaCard({
|
||||
Key? key,
|
||||
required this.iconWidget,
|
||||
required this.socialMediaName,
|
||||
}) : super(key: key);
|
||||
|
||||
Widget iconWidget;
|
||||
final String socialMediaName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
iconWidget,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
socialMediaName,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 95,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(top: 5.0, bottom: 5.0),
|
||||
decoration: kButtonDecoration.copyWith(color: kMainColor),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).share,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: Colors.white),
|
||||
),
|
||||
const Icon(
|
||||
Icons.share,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 30.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
136
lib/Screens/Notifications/notification_screen.dart
Normal file
136
lib/Screens/Notifications/notification_screen.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../constant.dart';
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
const NotificationScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_NotificationScreenState createState() => _NotificationScreenState();
|
||||
}
|
||||
|
||||
class _NotificationScreenState extends State<NotificationScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).notification,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
NotificationCard(
|
||||
title: lang.S.of(context).purchaseAlarm,
|
||||
iconColor: Colors.orange,
|
||||
icons: Icons.alarm,
|
||||
time: 'June 23, 2021',
|
||||
description: lang.S.of(context).lorem,
|
||||
// 'Lorem ipsum dolor sit amet, consectetur adip gravi iscing elit. Ultricies gravida scelerisque arcu facilisis duis in.',
|
||||
),
|
||||
NotificationCard(
|
||||
title: lang.S.of(context).purchaseConfirmed,
|
||||
iconColor: Colors.purple,
|
||||
icons: Icons.notifications_none_outlined,
|
||||
time: 'June 23, 2021',
|
||||
description: lang.S.of(context).lorem,
|
||||
// 'Lorem ipsum dolor sit amet, consectetur adip gravi iscing elit. Ultricies gravida scelerisque arcu facilisis duis in.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationCard extends StatelessWidget {
|
||||
const NotificationCard({
|
||||
Key? key,
|
||||
required this.icons,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.time,
|
||||
required this.iconColor,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData icons;
|
||||
final String title;
|
||||
final String description;
|
||||
final String time;
|
||||
final Color iconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Card(
|
||||
elevation: 0.0,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 40.0,
|
||||
width: 40.0,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
color: iconColor.withOpacity(0.2),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
icons,
|
||||
color: iconColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20.0,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
time,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 60.0),
|
||||
child: Text(
|
||||
description,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/Screens/PDF/pdf.dart
Normal file
75
lib/Screens/PDF/pdf.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class PDFViewerPage extends StatefulWidget {
|
||||
final String path;
|
||||
|
||||
const PDFViewerPage({super.key, required this.path});
|
||||
|
||||
@override
|
||||
PDFViewerPageState createState() => PDFViewerPageState();
|
||||
}
|
||||
|
||||
class PDFViewerPageState extends State<PDFViewerPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
lang.S.of(context).invoiceViewr,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: Colors.black),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: PDFView(
|
||||
filePath: widget.path,
|
||||
// onViewCreated: (PDFViewController controller) {
|
||||
// _pdfViewController = controller;
|
||||
// },
|
||||
// onPageChanged: (int page, int total) {
|
||||
// setState(() {
|
||||
// _currentPage = page;
|
||||
// _pages = total;
|
||||
// });
|
||||
// },
|
||||
),
|
||||
),
|
||||
// Container(
|
||||
// padding: EdgeInsets.all(8.0),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: <Widget>[
|
||||
// IconButton(
|
||||
// icon: Icon(Icons.chevron_left),
|
||||
// // onPressed: () {
|
||||
// // _pdfViewController.previousPage(
|
||||
// // duration: Duration(milliseconds: 250),
|
||||
// // curve: Curves.ease,
|
||||
// // );
|
||||
// // },
|
||||
// ),
|
||||
// Text('$_currentPage/$_pages'),
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.chevron_right),
|
||||
// onPressed: () {
|
||||
// _pdfViewController.setPage(
|
||||
//
|
||||
// duration: Duration(milliseconds: 250),
|
||||
// curve: Curves.ease,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
382
lib/Screens/Products/Model/product_model.dart
Normal file
382
lib/Screens/Products/Model/product_model.dart
Normal file
@@ -0,0 +1,382 @@
|
||||
// --- Nested Helper Models ---
|
||||
|
||||
import 'package:mobile_pos/Screens/product%20racks/model/product_racks_model.dart';
|
||||
import 'package:mobile_pos/Screens/shelfs/model/shelf_list_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
|
||||
class Vat {
|
||||
final int? id;
|
||||
final num? rate; // Changed to num
|
||||
|
||||
Vat({this.id, this.rate});
|
||||
|
||||
factory Vat.fromJson(Map<String, dynamic> json) {
|
||||
return Vat(
|
||||
id: json['id'],
|
||||
rate: json['rate'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Unit {
|
||||
final int? id;
|
||||
final String? unitName;
|
||||
|
||||
Unit({this.id, this.unitName});
|
||||
|
||||
factory Unit.fromJson(Map<String, dynamic> json) {
|
||||
return Unit(
|
||||
id: json['id'],
|
||||
unitName: json['unitName'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Brand {
|
||||
final int? id;
|
||||
final String? brandName;
|
||||
|
||||
Brand({this.id, this.brandName});
|
||||
|
||||
factory Brand.fromJson(Map<String, dynamic> json) {
|
||||
return Brand(
|
||||
id: json['id'],
|
||||
brandName: json['brandName'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Category {
|
||||
final int? id;
|
||||
final String? categoryName;
|
||||
|
||||
Category({this.id, this.categoryName});
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json) {
|
||||
return Category(
|
||||
id: json['id'],
|
||||
categoryName: json['categoryName'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProductModel {
|
||||
final int? id;
|
||||
final String? name;
|
||||
|
||||
ProductModel({this.id, this.name});
|
||||
|
||||
factory ProductModel.fromJson(Map<String, dynamic> json) {
|
||||
return ProductModel(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WarrantyGuaranteeInfo {
|
||||
final String? warrantyDuration;
|
||||
final String? warrantyUnit;
|
||||
final String? guaranteeDuration;
|
||||
final String? guaranteeUnit;
|
||||
|
||||
WarrantyGuaranteeInfo({
|
||||
this.warrantyDuration,
|
||||
this.warrantyUnit,
|
||||
this.guaranteeDuration,
|
||||
this.guaranteeUnit,
|
||||
});
|
||||
|
||||
factory WarrantyGuaranteeInfo.fromJson(Map<String, dynamic> json) {
|
||||
return WarrantyGuaranteeInfo(
|
||||
warrantyDuration: json['warranty_duration'],
|
||||
warrantyUnit: json['warranty_unit'],
|
||||
guaranteeDuration: json['guarantee_duration'],
|
||||
guaranteeUnit: json['guarantee_unit'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a specific stock/batch of a product, potentially with variants.
|
||||
class Stock {
|
||||
final int? id;
|
||||
final int? businessId;
|
||||
final int? branchId;
|
||||
final int? warehouseId;
|
||||
final int? productId;
|
||||
final String? batchNo;
|
||||
final num? productStock; // Changed to num
|
||||
final num? productPurchasePrice;
|
||||
final num? profitPercent; // Changed to num
|
||||
final num? productSalePrice;
|
||||
final num? productWholeSalePrice;
|
||||
final num? productDealerPrice;
|
||||
final String? serialNumbers;
|
||||
// Variation data is an array of maps
|
||||
final List<Map<String, dynamic>>? variationData;
|
||||
final String? variantName;
|
||||
final String? mfgDate;
|
||||
final String? expireDate;
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
final String? deletedAt;
|
||||
final Product? product;
|
||||
final WarehouseData? warehouse;
|
||||
|
||||
Stock({
|
||||
this.id,
|
||||
this.businessId,
|
||||
this.branchId,
|
||||
this.warehouseId,
|
||||
this.productId,
|
||||
this.batchNo,
|
||||
this.productStock,
|
||||
this.productPurchasePrice,
|
||||
this.profitPercent,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.productDealerPrice,
|
||||
this.serialNumbers,
|
||||
this.variationData,
|
||||
this.variantName,
|
||||
this.mfgDate,
|
||||
this.expireDate,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.deletedAt,
|
||||
this.product,
|
||||
this.warehouse,
|
||||
});
|
||||
|
||||
factory Stock.fromJson(Map<String, dynamic> json) {
|
||||
return Stock(
|
||||
id: json['id'],
|
||||
businessId: json['business_id'],
|
||||
branchId: json['branch_id'],
|
||||
warehouseId: json['warehouse_id'],
|
||||
productId: json['product_id'],
|
||||
batchNo: json['batch_no'],
|
||||
productStock: json['productStock'],
|
||||
productPurchasePrice: json['productPurchasePrice'],
|
||||
profitPercent: json['profit_percent'],
|
||||
productSalePrice: json['productSalePrice'],
|
||||
productWholeSalePrice: json['productWholeSalePrice'],
|
||||
productDealerPrice: json['productDealerPrice'],
|
||||
serialNumbers: json['serial_numbers'],
|
||||
variationData: (json['variation_data'] as List?)?.cast<Map<String, dynamic>>(),
|
||||
variantName: json['variant_name'],
|
||||
mfgDate: json['mfg_date'],
|
||||
expireDate: json['expire_date'],
|
||||
createdAt: json['created_at'],
|
||||
updatedAt: json['updated_at'],
|
||||
deletedAt: json['deleted_at'],
|
||||
product: json['product'] != null ? Product.fromJson(json['product']) : null,
|
||||
warehouse: json['warehouse'] != null ? WarehouseData.fromJson(json['warehouse']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a component product within a 'combo' product.
|
||||
class ComboProductComponent {
|
||||
final int? id;
|
||||
final int? productId;
|
||||
final int? stockId;
|
||||
final num? purchasePrice;
|
||||
final num? quantity; // Changed to num
|
||||
final Stock? stock;
|
||||
|
||||
ComboProductComponent({
|
||||
this.id,
|
||||
this.productId,
|
||||
this.stockId,
|
||||
this.purchasePrice,
|
||||
this.quantity,
|
||||
this.stock,
|
||||
});
|
||||
|
||||
factory ComboProductComponent.fromJson(Map<String, dynamic> json) {
|
||||
return ComboProductComponent(
|
||||
id: json['id'],
|
||||
productId: json['product_id'],
|
||||
stockId: json['stock_id'],
|
||||
purchasePrice: json['purchase_price'],
|
||||
quantity: json['quantity'],
|
||||
stock: json['stock'] != null ? Stock.fromJson(json['stock']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Product Model ---
|
||||
|
||||
/// Represents a single product entity.
|
||||
class Product {
|
||||
final int? id;
|
||||
final String? productName;
|
||||
final int? businessId;
|
||||
final int? rackId;
|
||||
final int? shelfId;
|
||||
final int? unitId;
|
||||
final int? brandId;
|
||||
final int? categoryId;
|
||||
final String? productCode;
|
||||
final WarrantyGuaranteeInfo? warrantyGuaranteeInfo;
|
||||
// variation_ids is a List<String> or null
|
||||
final List<String>? variationIds;
|
||||
final String? productPicture;
|
||||
final String? productType;
|
||||
final num? productDealerPrice;
|
||||
final num? totalLossProfit;
|
||||
final num? productPurchasePrice;
|
||||
final num? totalSaleAmount;
|
||||
final num? productSalePrice;
|
||||
final num? saleCount;
|
||||
final num? purchaseCount;
|
||||
final num? productWholeSalePrice;
|
||||
final num? productStock; // Changed to num
|
||||
final String? expireDate;
|
||||
final num? alertQty; // Changed to num
|
||||
final num? profitPercent; // Changed to num
|
||||
final num? vatAmount;
|
||||
final String? vatType;
|
||||
final String? size;
|
||||
final String? type;
|
||||
final String? color;
|
||||
final String? weight;
|
||||
final String? capacity;
|
||||
final String? productManufacturer;
|
||||
final dynamic meta; // Use 'dynamic' for unstructured JSON
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
final int? vatId;
|
||||
final int? modelId;
|
||||
final int? warehouseId;
|
||||
final num? stocksSumProductStock; // Changed to num
|
||||
|
||||
// Relationships (Nested Objects/Lists)
|
||||
final Unit? unit;
|
||||
final Vat? vat;
|
||||
final Brand? brand;
|
||||
final Category? category;
|
||||
final ProductModel? productModel;
|
||||
final List<Stock>? stocks;
|
||||
final RackData? rack;
|
||||
final ShelfData? shelf;
|
||||
final List<ComboProductComponent>? comboProducts;
|
||||
|
||||
Product({
|
||||
this.id,
|
||||
this.productName,
|
||||
this.businessId,
|
||||
this.rackId,
|
||||
this.shelfId,
|
||||
this.unitId,
|
||||
this.brandId,
|
||||
this.categoryId,
|
||||
this.productCode,
|
||||
this.totalLossProfit,
|
||||
this.warrantyGuaranteeInfo,
|
||||
this.variationIds,
|
||||
this.productPicture,
|
||||
this.productType,
|
||||
this.productDealerPrice,
|
||||
this.saleCount,
|
||||
this.purchaseCount,
|
||||
this.productPurchasePrice,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.totalSaleAmount,
|
||||
this.productStock,
|
||||
this.expireDate,
|
||||
this.alertQty,
|
||||
this.profitPercent,
|
||||
this.vatAmount,
|
||||
this.vatType,
|
||||
this.size,
|
||||
this.type,
|
||||
this.color,
|
||||
this.weight,
|
||||
this.capacity,
|
||||
this.productManufacturer,
|
||||
this.meta,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.vatId,
|
||||
this.modelId,
|
||||
this.warehouseId,
|
||||
this.stocksSumProductStock,
|
||||
this.unit,
|
||||
this.vat,
|
||||
this.brand,
|
||||
this.category,
|
||||
this.productModel,
|
||||
this.stocks,
|
||||
this.comboProducts,
|
||||
this.rack,
|
||||
this.shelf,
|
||||
});
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
// Helper function to safely map lists, returning null if the source is null
|
||||
List<T>? _mapList<T>(List? list, T Function(Map<String, dynamic>) fromJson) {
|
||||
return list?.map((i) => fromJson(i as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
return Product(
|
||||
id: json['id'],
|
||||
productName: json['productName'],
|
||||
businessId: json['business_id'],
|
||||
rackId: json['rack_id'],
|
||||
shelfId: json['shelf_id'],
|
||||
unitId: json['unit_id'],
|
||||
brandId: json['brand_id'],
|
||||
categoryId: json['category_id'],
|
||||
productCode: json['productCode'],
|
||||
warrantyGuaranteeInfo: json['warranty_guarantee_info'] != null
|
||||
? WarrantyGuaranteeInfo.fromJson(json['warranty_guarantee_info'])
|
||||
: null,
|
||||
variationIds: (json['variation_ids'] as List?)?.cast<String>(),
|
||||
productPicture: json['productPicture'],
|
||||
totalLossProfit: json['total_profit_loss'],
|
||||
productType: json['product_type'],
|
||||
productDealerPrice: json['productDealerPrice'],
|
||||
totalSaleAmount: json['total_sale_amount'],
|
||||
saleCount: json['sale_details_sum_quantities'],
|
||||
purchaseCount: json['purchase_details_sum_quantities'],
|
||||
productPurchasePrice: json['productPurchasePrice'],
|
||||
productSalePrice: json['productSalePrice'],
|
||||
productWholeSalePrice: json['productWholeSalePrice'],
|
||||
productStock: json['productStock'],
|
||||
expireDate: json['expire_date'],
|
||||
alertQty: json['alert_qty'],
|
||||
profitPercent: json['profit_percent'],
|
||||
vatAmount: json['vat_amount'],
|
||||
vatType: json['vat_type'],
|
||||
size: json['size'],
|
||||
type: json['type'],
|
||||
color: json['color'],
|
||||
weight: json['weight'],
|
||||
capacity: json['capacity'],
|
||||
productManufacturer: json['productManufacturer'],
|
||||
meta: json['meta'],
|
||||
createdAt: json['created_at'],
|
||||
updatedAt: json['updated_at'],
|
||||
vatId: json['vat_id'],
|
||||
modelId: json['model_id'],
|
||||
warehouseId: json['warehouse_id'],
|
||||
stocksSumProductStock: json['stocks_sum_product_stock'],
|
||||
|
||||
// Nested Relationships
|
||||
unit: json['unit'] != null ? Unit.fromJson(json['unit']) : null,
|
||||
shelf: json['shelf'] != null ? ShelfData.fromJson(json['shelf']) : null,
|
||||
rack: json['rack'] != null ? RackData.fromJson(json['rack']) : null,
|
||||
vat: json['vat'] != null ? Vat.fromJson(json['vat']) : null,
|
||||
brand: json['brand'] != null ? Brand.fromJson(json['brand']) : null,
|
||||
category: json['category'] != null ? Category.fromJson(json['category']) : null,
|
||||
productModel: json['product_model'] != null ? ProductModel.fromJson(json['product_model']) : null,
|
||||
|
||||
// Lists of Nested Objects
|
||||
stocks: _mapList<Stock>(json['stocks'] as List?, Stock.fromJson),
|
||||
comboProducts: _mapList<ComboProductComponent>(json['combo_products'] as List?, ComboProductComponent.fromJson),
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/Screens/Products/Model/product_total_stock_model.dart
Normal file
18
lib/Screens/Products/Model/product_total_stock_model.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
|
||||
class ProductListResponse {
|
||||
final double totalStockValue;
|
||||
final List<Product> products;
|
||||
|
||||
ProductListResponse({
|
||||
required this.totalStockValue,
|
||||
required this.products,
|
||||
});
|
||||
|
||||
factory ProductListResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ProductListResponse(
|
||||
totalStockValue: (json['total_stock_value'] as num).toDouble(),
|
||||
products: (json['data'] as List).map((item) => Product.fromJson(item)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
lib/Screens/Products/Providers/product_provider.dart
Normal file
11
lib/Screens/Products/Providers/product_provider.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_total_stock_model.dart';
|
||||
|
||||
import '../Repo/product_repo.dart';
|
||||
|
||||
ProductRepo productRepo = ProductRepo();
|
||||
final productListProvider = FutureProvider<ProductListResponse>((ref) async {
|
||||
final response = await productRepo.fetchProducts();
|
||||
return response;
|
||||
});
|
||||
448
lib/Screens/Products/Repo/product_repo.dart
Normal file
448
lib/Screens/Products/Repo/product_repo.dart
Normal file
@@ -0,0 +1,448 @@
|
||||
//ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mobile_pos/Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_total_stock_model.dart';
|
||||
import 'package:mobile_pos/service/check_user_role_permission_provider.dart';
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../constant.dart';
|
||||
import '../../../core/constant_variables/local_data_saving_keys.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../Purchase/Repo/purchase_repo.dart';
|
||||
import '../Model/product_model.dart';
|
||||
import '../add product/modle/create_product_model.dart';
|
||||
|
||||
class ProductRepo {
|
||||
// ==============================================================================
|
||||
// NEW CREATE PRODUCT FUNCTION
|
||||
// ==============================================================================
|
||||
Future<bool> createProduct({required CreateProductModel data, required BuildContext context, required WidgetRef ref}) async {
|
||||
return _submitProductData(data: data, isUpdate: false, context: context, ref: ref);
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// NEW UPDATE PRODUCT FUNCTION
|
||||
// ==============================================================================
|
||||
Future<bool> updateProduct({required CreateProductModel data, required BuildContext context, required WidgetRef ref}) async {
|
||||
return _submitProductData(data: data, isUpdate: true, context: context, ref: ref);
|
||||
}
|
||||
|
||||
/// Shared Logic for Create and Update to avoid code duplication
|
||||
Future<bool> _submitProductData({required CreateProductModel data, required bool isUpdate, required BuildContext context, required WidgetRef ref}) async {
|
||||
EasyLoading.show(status: isUpdate ? 'Updating Product...' : 'Creating Product...');
|
||||
|
||||
final url = Uri.parse(isUpdate ? '${APIConfig.url}/products/${data.productId}' : '${APIConfig.url}/products');
|
||||
|
||||
var request = http.MultipartRequest('POST', url);
|
||||
|
||||
request.headers.addAll({
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
});
|
||||
|
||||
// Helper to safely add simple string fields
|
||||
void addField(String key, dynamic value) {
|
||||
if (value != null && value.toString().isNotEmpty && value.toString() != 'null') {
|
||||
request.fields[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 1. Standard Fields ---
|
||||
if (isUpdate) addField('_method', 'put');
|
||||
|
||||
addField('productName', data.name);
|
||||
addField('category_id', data.categoryId);
|
||||
addField('unit_id', data.unitId);
|
||||
addField('productCode', data.productCode);
|
||||
addField('brand_id', data.brandId);
|
||||
addField('model_id', data.modelId);
|
||||
addField('rack_id', data.rackId);
|
||||
addField('shelf_id', data.shelfId);
|
||||
addField('alert_qty', data.alertQty);
|
||||
|
||||
// Serial logic (1 or 0)
|
||||
// addField('has_serial', (data.hasSerial == '1' || data.hasSerial == 'true') ? '1' : '0');
|
||||
|
||||
addField('product_type', data.productType); // single, variant, combo
|
||||
addField('vat_type', data.vatType);
|
||||
addField('vat_id', data.vatId);
|
||||
// Optional: vat_amount if backend calculates it or needs it
|
||||
if (data.vatAmount != null) addField('vat_amount', data.vatAmount);
|
||||
|
||||
// Extra info
|
||||
addField('productManufacturer', data.productManufacturer);
|
||||
addField('productDiscount', data.productDiscount);
|
||||
|
||||
// --- 2. Complex Fields (JSON Encoded) ---
|
||||
|
||||
// A. STOCKS
|
||||
// This handles Single (1 item in list) and Variant (multiple items in list)
|
||||
if (data.stocks != null && data.stocks!.isNotEmpty) {
|
||||
// Convert list of StockDataModel to List of Maps
|
||||
List<Map<String, dynamic>> stockListJson = data.stocks!.map((stock) => stock.toJson()).toList();
|
||||
// Encode to JSON String
|
||||
request.fields['stocks'] = jsonEncode(stockListJson);
|
||||
}
|
||||
|
||||
// B. VARIATION IDs (Only for variant type)
|
||||
if (data.productType?.toLowerCase() == 'variant' && (data.variationIds?.isNotEmpty ?? false)) {
|
||||
request.fields['variation_ids'] = jsonEncode(data.variationIds);
|
||||
}
|
||||
|
||||
// C. COMBO PRODUCTS (Only for combo type)
|
||||
if (data.productType?.toLowerCase() == 'combo' && (data.comboProducts?.isNotEmpty ?? false)) {
|
||||
request.fields['combo_products'] = jsonEncode(data.comboProducts);
|
||||
addField('profit_percent', data.comboProfitPercent);
|
||||
addField('productSalePrice', data.comboProductSalePrice);
|
||||
}
|
||||
|
||||
// D. WARRANTY & GUARANTEE
|
||||
Map<String, String> warrantyInfo = {};
|
||||
if (data.warrantyDuration != null && data.warrantyDuration!.isNotEmpty) {
|
||||
warrantyInfo['warranty_duration'] = data.warrantyDuration!;
|
||||
warrantyInfo['warranty_unit'] = data.warrantyPeriod ?? 'days';
|
||||
}
|
||||
if (data.guaranteeDuration != null && data.guaranteeDuration!.isNotEmpty) {
|
||||
warrantyInfo['guarantee_duration'] = data.guaranteeDuration!;
|
||||
warrantyInfo['guarantee_unit'] = data.guaranteePeriod ?? 'days';
|
||||
}
|
||||
|
||||
if (warrantyInfo.isNotEmpty) {
|
||||
request.fields['warranty_guarantee_info'] = jsonEncode(warrantyInfo);
|
||||
}
|
||||
|
||||
// --- 3. File Upload ---
|
||||
if (data.image != null) {
|
||||
request.files.add(await http.MultipartFile.fromPath(
|
||||
'productPicture',
|
||||
data.image!.path,
|
||||
filename: data.image!.path.split('/').last,
|
||||
));
|
||||
}
|
||||
|
||||
// --- Debugging Logs ---
|
||||
print('URL: $url');
|
||||
print('--- Fields ---');
|
||||
|
||||
request.fields.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
print('--- Fields ---');
|
||||
print(request.fields);
|
||||
|
||||
// --- 4. Execute ---
|
||||
try {
|
||||
// var response = await request.send();
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), ref: ref, context: context);
|
||||
print('Product image: ${data.image?.path}');
|
||||
final response = await customHttpClient.uploadFile(
|
||||
url: url,
|
||||
file: data.image,
|
||||
fileFieldName: 'productPicture',
|
||||
fields: request.fields,
|
||||
);
|
||||
var responseData = await http.Response.fromStream(response);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
print("Response Status: ${response.statusCode}");
|
||||
print("Response Body: ${responseData.body}");
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showSuccess(body['message'] ?? (isUpdate ? 'Updated successfully!' : 'Created successfully!'));
|
||||
return true;
|
||||
} catch (e) {
|
||||
// If JSON parsing fails but status is 200
|
||||
EasyLoading.showSuccess(isUpdate ? 'Product updated!' : 'Product created!');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showError(body['message'] ?? 'Failed to process product');
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Failed with status: ${response.statusCode}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Network Error: ${e.toString()}');
|
||||
print(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> generateProductCode() async {
|
||||
final uri = Uri.parse('${APIConfig.url}/product/generate-code');
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
try {
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final jsonResponse = json.decode(response.body);
|
||||
return jsonResponse['data'].toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Product>> fetchAllProducts() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/products');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final partyList = parsedData['data'] as List<dynamic>;
|
||||
return partyList.map((category) => Product.fromJson(category)).toList();
|
||||
// Parse into Party objects
|
||||
} else {
|
||||
throw Exception('Failed to fetch Products');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProductListResponse> fetchProducts() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/products');
|
||||
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
return ProductListResponse.fromJson(parsedData);
|
||||
} else {
|
||||
throw Exception('Failed to fetch products');
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Product Details
|
||||
Future<Product> fetchProductDetails({required String productID}) async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
|
||||
final url = Uri.parse('${APIConfig.url}/products/$productID');
|
||||
|
||||
try {
|
||||
var response = await clientGet.get(url: url);
|
||||
EasyLoading.dismiss();
|
||||
print(response.statusCode);
|
||||
print(response.body);
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
return Product.fromJson(jsonData['data']);
|
||||
} else {
|
||||
var data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Failed to fetch details');
|
||||
throw Exception(data['message'] ?? 'Failed to fetch details');
|
||||
}
|
||||
} catch (e) {
|
||||
// Hide loading indicator and show error
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
throw Exception('Error: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteProduct({
|
||||
required String id,
|
||||
required BuildContext context,
|
||||
required WidgetRef ref,
|
||||
}) async {
|
||||
final String apiUrl = '${APIConfig.url}/products/$id';
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(
|
||||
ref: ref,
|
||||
context: context,
|
||||
client: http.Client(),
|
||||
);
|
||||
|
||||
final response = await customHttpClient.delete(
|
||||
url: Uri.parse(apiUrl),
|
||||
permission: Permit.productsDelete.value,
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
// 👇 Print full response info
|
||||
print('Delete Product Response:');
|
||||
print('Status Code: ${response.statusCode}');
|
||||
print('Body: ${response.body}');
|
||||
print('Headers: ${response.headers}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Product deleted successfully')),
|
||||
);
|
||||
|
||||
ref.refresh(productProvider);
|
||||
} else {
|
||||
final parsedData = jsonDecode(response.body);
|
||||
final errorMessage = parsedData['error'].toString().replaceFirst('Exception: ', '');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorMessage),
|
||||
backgroundColor: kMainColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('rrrr');
|
||||
EasyLoading.dismiss();
|
||||
print('Exception during product delete: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> addStock({required String id, required String qty}) async {
|
||||
final url = Uri.parse('${APIConfig.url}/stocks');
|
||||
String token = await getAuthToken() ?? '';
|
||||
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token,
|
||||
};
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
"stock_id": id,
|
||||
"productStock": qty,
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await http.post(url, headers: headers, body: requestBody);
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
final data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Unknown error');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateVariation({required CartProductModelPurchase data}) async {
|
||||
EasyLoading.show(status: 'Updating Product...');
|
||||
final url = Uri.parse('${APIConfig.url}/stocks/${data.variantName}');
|
||||
var request = http.MultipartRequest('POST', url);
|
||||
|
||||
request.headers.addAll({
|
||||
'Accept': 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
});
|
||||
|
||||
void addField(String key, dynamic value) {
|
||||
if (value != null && value.toString().isNotEmpty && value.toString() != 'null') {
|
||||
request.fields[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Add standard fields
|
||||
addField('_method', 'put');
|
||||
addField('batch_no', data.batchNumber);
|
||||
addField('productStock', data.quantities);
|
||||
addField('productPurchasePrice', data.productPurchasePrice);
|
||||
addField('profit_percent', data.profitPercent);
|
||||
addField('productSalePrice', data.productSalePrice);
|
||||
addField('productWholeSalePrice', data.productWholeSalePrice);
|
||||
addField('productDealerPrice', data.productDealerPrice);
|
||||
addField('mfg_date', data.mfgDate);
|
||||
addField('expire_date', data.expireDate);
|
||||
|
||||
print('--- Product Data Fields ---');
|
||||
print('Total fields: ${request.fields.length}');
|
||||
print(data.mfgDate);
|
||||
request.fields.forEach((key, value) {
|
||||
print('$key: $value');
|
||||
});
|
||||
|
||||
try {
|
||||
var response = await request.send();
|
||||
var responseData = await http.Response.fromStream(response);
|
||||
|
||||
print('Response Status Code: ${response.statusCode}');
|
||||
print('Response Body: ${responseData.body}');
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showSuccess(body['message'] ?? 'Product update successfully!');
|
||||
return true;
|
||||
} catch (e) {
|
||||
EasyLoading.showSuccess('Product update successfully!');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
var body = jsonDecode(responseData.body);
|
||||
EasyLoading.showError(body['message'] ?? 'Failed to update product');
|
||||
print('Error Response: ${responseData.body}');
|
||||
} catch (e) {
|
||||
EasyLoading.showError('Failed to update product. Status: ${response.statusCode}');
|
||||
print('Error Response (non-JSON): ${responseData.body}');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Network Error: ${e.toString()}');
|
||||
print('Network Error: ${e.toString()}');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteStock({required String id}) async {
|
||||
EasyLoading.show(status: 'Processing');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String token = prefs.getString(LocalDataBaseSavingKey.tokenKey) ?? '';
|
||||
final url = Uri.parse('${APIConfig.url}/stocks/$id');
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
try {
|
||||
var response = await http.delete(
|
||||
url,
|
||||
headers: headers,
|
||||
);
|
||||
EasyLoading.dismiss();
|
||||
print(response.statusCode);
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
var data = jsonDecode(response.body);
|
||||
EasyLoading.showError(data['message'] ?? 'Failed to delete');
|
||||
print(data['message']);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
EasyLoading.showError('Error: ${e.toString()}');
|
||||
print(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
lib/Screens/Products/Repo/unit_repo.dart
Normal file
154
lib/Screens/Products/Repo/unit_repo.dart
Normal file
@@ -0,0 +1,154 @@
|
||||
// ignore_for_file: file_names, unused_element, unused_local_variable
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../Const/api_config.dart';
|
||||
import '../../../Repository/constant_functions.dart';
|
||||
import '../../../http_client/custome_http_client.dart';
|
||||
import '../../../http_client/customer_http_client_get.dart';
|
||||
import '../../product_unit/model/unit_model.dart';
|
||||
import '../../product_unit/provider/product_unit_provider.dart';
|
||||
|
||||
class UnitsRepo {
|
||||
Future<List<Unit>> fetchAllUnits() async {
|
||||
CustomHttpClientGet clientGet = CustomHttpClientGet(client: http.Client());
|
||||
final uri = Uri.parse('${APIConfig.url}/units');
|
||||
|
||||
try {
|
||||
final response = await clientGet.get(url: uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final parsedData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final categoryList = parsedData['data'] as List<dynamic>;
|
||||
return categoryList.map((unit) => Unit.fromJson(unit)).toList();
|
||||
} else {
|
||||
throw Exception('Failed to fetch units: ${response.statusCode}');
|
||||
}
|
||||
} catch (error) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addUnit({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/units');
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: {
|
||||
'unitName': name,
|
||||
},
|
||||
// addContentTypeInHeader: true,
|
||||
);
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
|
||||
var data1 = ref.refresh(unitsProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Unit creation failed: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
|
||||
Future<num?> addUnitForBulk({
|
||||
required String name,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/units');
|
||||
|
||||
try {
|
||||
var responseData = await http.post(uri, headers: {
|
||||
"Accept": 'application/json',
|
||||
'Authorization': await getAuthToken(),
|
||||
}, body: {
|
||||
'unitName': name,
|
||||
});
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
return parsedData['data']['id'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
///_______Edit_Add_________________________________________
|
||||
Future<void> editUnit({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required num id,
|
||||
required String name,
|
||||
}) async {
|
||||
final uri = Uri.parse('${APIConfig.url}/units/$id');
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
var responseData = await customHttpClient.post(
|
||||
url: uri,
|
||||
body: {
|
||||
'unitName': name,
|
||||
'_method': 'put',
|
||||
},
|
||||
);
|
||||
final parsedData = jsonDecode(responseData.body);
|
||||
|
||||
if (responseData.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('update successful!')));
|
||||
var data1 = ref.refresh(unitsProvider);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Unit creation failed: ${parsedData['message']}')));
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle unexpected errors gracefully
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('An error occurred: $error')));
|
||||
}
|
||||
}
|
||||
|
||||
///_________delete_unit________________________
|
||||
Future<bool> deleteUnit({required BuildContext context, required num unitId, required WidgetRef ref}) async {
|
||||
final String apiUrl = '${APIConfig.url}/units/$unitId'; // Replace with your API URL
|
||||
|
||||
try {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(ref: ref, context: context, client: http.Client());
|
||||
final response = await customHttpClient.delete(
|
||||
url: Uri.parse(apiUrl),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = json.decode(response.body);
|
||||
final String message = responseData['message'];
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete unit.')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('An error occurred.')),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
217
lib/Screens/Products/Widgets/acnoo_multiple_select_dropdown.dart
Normal file
217
lib/Screens/Products/Widgets/acnoo_multiple_select_dropdown.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'dart:ui';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Widgets/selected_button.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class AcnooMultiSelectDropdown<T> extends StatefulWidget {
|
||||
AcnooMultiSelectDropdown({
|
||||
super.key,
|
||||
this.decoration,
|
||||
this.menuItemStyleData,
|
||||
this.buttonStyleData,
|
||||
this.iconStyleData,
|
||||
this.dropdownStyleData,
|
||||
required this.items,
|
||||
this.values,
|
||||
this.onChanged,
|
||||
required this.labelText,
|
||||
}) : assert(
|
||||
items.isEmpty ||
|
||||
values == null ||
|
||||
items.where((item) {
|
||||
return values.contains(item.value);
|
||||
}).length ==
|
||||
values.length,
|
||||
"There should be exactly one item with [AcnooMultiSelectDropdown]'s value in the items list. "
|
||||
'Either zero or 2 or more [MultiSelectDropdownMenuItem]s were detected with the same value',
|
||||
);
|
||||
|
||||
final List<MultiSelectDropdownMenuItem<T?>> items;
|
||||
final List<T?>? values;
|
||||
final void Function(List<T>? values)? onChanged;
|
||||
|
||||
final InputDecoration? decoration;
|
||||
final MenuItemStyleData? menuItemStyleData;
|
||||
final ButtonStyleData? buttonStyleData;
|
||||
final IconStyleData? iconStyleData;
|
||||
final DropdownStyleData? dropdownStyleData;
|
||||
|
||||
final String labelText;
|
||||
|
||||
@override
|
||||
State<AcnooMultiSelectDropdown<T>> createState() => _AcnooMultiSelectDropdownState<T>();
|
||||
}
|
||||
|
||||
class _AcnooMultiSelectDropdownState<T> extends State<AcnooMultiSelectDropdown<T>> {
|
||||
bool isOpen = false;
|
||||
void listenMenuChange(bool value) {
|
||||
setState(() {
|
||||
isOpen = value;
|
||||
if (!value) {
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
late List<MultiSelectDropdownMenuItem<T?>> selectedItems;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
selectedItems = widget.items.where((element) => widget.values?.contains(element.value) ?? false).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return DropdownButtonFormField2<T>(
|
||||
decoration: (widget.decoration ?? const InputDecoration()).copyWith(
|
||||
labelText: widget.labelText,
|
||||
hintText: '',
|
||||
),
|
||||
menuItemStyleData: widget.menuItemStyleData ?? const MenuItemStyleData(),
|
||||
buttonStyleData: widget.buttonStyleData ?? const ButtonStyleData(),
|
||||
iconStyleData: widget.iconStyleData ?? const IconStyleData(),
|
||||
dropdownStyleData: widget.dropdownStyleData ?? const DropdownStyleData(),
|
||||
onMenuStateChange: listenMenuChange,
|
||||
customButton: _buildCustomButton(context),
|
||||
items: widget.items.map((item) {
|
||||
return DropdownMenuItem<T>(
|
||||
value: item.value,
|
||||
enabled: false,
|
||||
child: _buildMenuItem(context, item, _theme),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (_) {},
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- CHANGE IS HERE ---------------- //
|
||||
Widget _buildCustomButton(BuildContext context) {
|
||||
const _itemPadding = EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
);
|
||||
if (selectedItems.isEmpty) {
|
||||
final _iconWidget = widget.iconStyleData?.icon ?? Icon(Icons.keyboard_arrow_down_outlined);
|
||||
return Padding(
|
||||
padding: _itemPadding,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.decoration?.hintText ?? lang.S.of(context).selectItems,
|
||||
style: widget.decoration?.hintStyle,
|
||||
),
|
||||
_iconWidget,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.touch,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: selectedItems.reversed.map((item) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: SelectedItemButton(
|
||||
padding: _itemPadding,
|
||||
labelText: item.labelText,
|
||||
onTap: () {
|
||||
// 1. Remove item from local state
|
||||
setState(() {
|
||||
selectedItems.remove(item);
|
||||
});
|
||||
|
||||
// 2. Trigger the onChanged callback immediately
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// ----------------------------------------------- //
|
||||
|
||||
Widget _buildMenuItem(
|
||||
BuildContext context,
|
||||
MultiSelectDropdownMenuItem<T?> item,
|
||||
ThemeData _theme,
|
||||
) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, itemState) {
|
||||
final _isSelected = selectedItems.contains(item);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_isSelected ? selectedItems.remove(item) : selectedItems.add(item);
|
||||
widget.onChanged?.call(
|
||||
selectedItems.map((e) => e.value!).toList(),
|
||||
);
|
||||
setState(() {});
|
||||
itemState(() {});
|
||||
},
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
alignment: AlignmentDirectional.center,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(item.labelText),
|
||||
),
|
||||
if (_isSelected)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSelectDropdownMenuItem<T> {
|
||||
MultiSelectDropdownMenuItem({
|
||||
required this.labelText,
|
||||
this.value,
|
||||
});
|
||||
final String labelText;
|
||||
final T? value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MultiSelectDropdownMenuItem<T> &&
|
||||
runtimeType == other.runtimeType &&
|
||||
labelText == other.labelText &&
|
||||
value == other.value;
|
||||
|
||||
@override
|
||||
int get hashCode => labelText.hashCode ^ value.hashCode;
|
||||
}
|
||||
228
lib/Screens/Products/Widgets/dropdown_styles.dart
Normal file
228
lib/Screens/Products/Widgets/dropdown_styles.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class AcnooDropdownStyle {
|
||||
AcnooDropdownStyle(this.context);
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
// Theme
|
||||
ThemeData get _theme => Theme.of(context);
|
||||
bool get _isDark => _theme.brightness == Brightness.dark;
|
||||
|
||||
// Button Style
|
||||
ButtonStyleData get buttonStyle => const ButtonStyleData(width: 0);
|
||||
|
||||
// Dropdown Style
|
||||
AcnooDropdownStyleData get dropdownStyle {
|
||||
return AcnooDropdownStyleData(
|
||||
maxHeight: 300,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: _theme.colorScheme.primaryContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Icon Style
|
||||
AcnooDropdownIconData get iconStyle {
|
||||
return AcnooDropdownIconData(
|
||||
icon: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: _isDark ? Colors.white : kMainColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Menu Style
|
||||
AcnooDropdownMenuItemStyleData get menuItemStyle {
|
||||
return AcnooDropdownMenuItemStyleData(
|
||||
overlayColor: WidgetStateProperty.all<Color>(
|
||||
kMainColor.withValues(alpha: 0.25),
|
||||
),
|
||||
selectedMenuItemBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.125),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
MenuItemStyleData get multiSelectMenuItemStyle {
|
||||
return MenuItemStyleData(
|
||||
overlayColor: WidgetStateProperty.all<Color>(
|
||||
kMainColor.withValues(alpha: 0.25),
|
||||
),
|
||||
selectedMenuItemBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.125),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
child,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: kMainColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Text Style
|
||||
TextStyle? get textStyle => _theme.textTheme.bodyLarge;
|
||||
|
||||
/*
|
||||
DropdownMenuItem<T> firstItem<T>({
|
||||
required String title,
|
||||
required String actionTitle,
|
||||
void Function()? onTap,
|
||||
T? value,
|
||||
bool enabled = false,
|
||||
}) {
|
||||
return DropdownMenuItem(
|
||||
value: value,
|
||||
enabled: enabled,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: AcnooTextStyle.kSubtitleText.copyWith(
|
||||
fontSize: 16,
|
||||
color: AcnooAppColors.k03,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: actionTitle,
|
||||
recognizer: TapGestureRecognizer()..onTap = onTap,
|
||||
),
|
||||
style: AcnooTextStyle.kSubtitleText.copyWith(
|
||||
fontSize: 14,
|
||||
color: AcnooAppColors.kPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownStyleData extends DropdownStyleData {
|
||||
const AcnooDropdownStyleData({
|
||||
super.maxHeight,
|
||||
super.width,
|
||||
super.padding,
|
||||
super.scrollPadding,
|
||||
super.decoration,
|
||||
super.elevation,
|
||||
super.direction,
|
||||
super.offset,
|
||||
super.isOverButton,
|
||||
super.useSafeArea,
|
||||
super.isFullScreen,
|
||||
super.useRootNavigator,
|
||||
super.scrollbarTheme,
|
||||
super.openInterval,
|
||||
});
|
||||
|
||||
AcnooDropdownStyleData copyWith({
|
||||
double? maxHeight,
|
||||
double? width,
|
||||
EdgeInsetsGeometry? padding,
|
||||
EdgeInsetsGeometry? scrollPadding,
|
||||
BoxDecoration? decoration,
|
||||
int? elevation,
|
||||
DropdownDirection? direction,
|
||||
Offset? offset,
|
||||
bool? isOverButton,
|
||||
bool? useSafeArea,
|
||||
bool? isFullScreen,
|
||||
bool? useRootNavigator,
|
||||
ScrollbarThemeData? scrollbarTheme,
|
||||
Interval? openInterval,
|
||||
}) {
|
||||
return AcnooDropdownStyleData(
|
||||
maxHeight: maxHeight ?? this.maxHeight,
|
||||
width: width ?? this.width,
|
||||
padding: padding ?? this.padding,
|
||||
scrollPadding: scrollPadding ?? this.scrollPadding,
|
||||
decoration: decoration ?? this.decoration,
|
||||
elevation: elevation ?? this.elevation,
|
||||
direction: direction ?? this.direction,
|
||||
offset: offset ?? this.offset,
|
||||
isOverButton: isOverButton ?? this.isOverButton,
|
||||
useSafeArea: useSafeArea ?? this.useSafeArea,
|
||||
isFullScreen: isFullScreen ?? this.useRootNavigator,
|
||||
useRootNavigator: useRootNavigator ?? this.useRootNavigator,
|
||||
scrollbarTheme: scrollbarTheme ?? this.scrollbarTheme,
|
||||
openInterval: openInterval ?? this.openInterval,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownIconData extends IconStyleData {
|
||||
const AcnooDropdownIconData({
|
||||
super.icon,
|
||||
super.iconDisabledColor,
|
||||
super.iconEnabledColor,
|
||||
super.iconSize,
|
||||
super.openMenuIcon,
|
||||
});
|
||||
|
||||
AcnooDropdownIconData copyWith({
|
||||
Widget? icon,
|
||||
Color? iconDisabledColor,
|
||||
Color? iconEnabledColor,
|
||||
double? iconSize,
|
||||
Widget? openMenuIcon,
|
||||
}) {
|
||||
return AcnooDropdownIconData(
|
||||
icon: icon ?? this.icon,
|
||||
iconDisabledColor: iconDisabledColor ?? this.iconDisabledColor,
|
||||
iconEnabledColor: iconEnabledColor ?? this.iconEnabledColor,
|
||||
iconSize: iconSize ?? this.iconSize,
|
||||
openMenuIcon: openMenuIcon ?? this.openMenuIcon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AcnooDropdownMenuItemStyleData extends MenuItemStyleData {
|
||||
const AcnooDropdownMenuItemStyleData({
|
||||
super.customHeights,
|
||||
super.height,
|
||||
super.overlayColor,
|
||||
super.padding,
|
||||
super.selectedMenuItemBuilder,
|
||||
});
|
||||
|
||||
AcnooDropdownMenuItemStyleData copyWith({
|
||||
List<double>? customHeights,
|
||||
double? height,
|
||||
Color? overlayColor,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Widget Function(BuildContext, Widget)? selectedMenuItemBuilder,
|
||||
}) {
|
||||
return AcnooDropdownMenuItemStyleData(
|
||||
customHeights: customHeights ?? this.customHeights,
|
||||
height: height ?? this.height,
|
||||
overlayColor: overlayColor != null ? WidgetStateProperty.all<Color?>(overlayColor) : this.overlayColor,
|
||||
selectedMenuItemBuilder: selectedMenuItemBuilder ?? this.selectedMenuItemBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/Screens/Products/Widgets/selected_button.dart
Normal file
58
lib/Screens/Products/Widgets/selected_button.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
class SelectedItemButton extends StatelessWidget {
|
||||
const SelectedItemButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
this.showCloseButton = true,
|
||||
});
|
||||
final String labelText;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final void Function()? onTap;
|
||||
final bool showCloseButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: padding ??
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: kDarkWhite,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: labelText,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
if (showCloseButton)
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: TextStyle(color: _theme.colorScheme.onPrimary),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/Screens/Products/Widgets/text_field_label_wrappers.dart
Normal file
37
lib/Screens/Products/Widgets/text_field_label_wrappers.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// 🐦 Flutter imports:
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
class TextFieldLabelWrapper extends StatelessWidget {
|
||||
const TextFieldLabelWrapper({
|
||||
super.key,
|
||||
this.labelText,
|
||||
this.label,
|
||||
this.labelStyle,
|
||||
required this.inputField,
|
||||
});
|
||||
final String? labelText;
|
||||
final Widget? label;
|
||||
final TextStyle? labelStyle;
|
||||
final Widget inputField;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Label
|
||||
if (label == null)
|
||||
Text(
|
||||
labelText ?? lang.S.of(context).enterLabelText,
|
||||
style: labelStyle ?? _theme.inputDecorationTheme.floatingLabelStyle,
|
||||
)
|
||||
else
|
||||
label!,
|
||||
const SizedBox(height: 8),
|
||||
inputField,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
724
lib/Screens/Products/add product/add_edit_comboItem.dart
Normal file
724
lib/Screens/Products/add product/add_edit_comboItem.dart
Normal file
@@ -0,0 +1,724 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:icons_plus/icons_plus.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/core/theme/_app_colors.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import '../../../Provider/product_provider.dart';
|
||||
import '../../../constant.dart';
|
||||
import 'combo_product_form.dart';
|
||||
|
||||
class AddOrEditComboItem extends ConsumerStatefulWidget {
|
||||
final ComboItem? existingItem;
|
||||
final Function(ComboItem) onSubmit;
|
||||
|
||||
const AddOrEditComboItem({
|
||||
super.key,
|
||||
this.existingItem,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<AddOrEditComboItem> createState() => _AddOrEditComboItemPopupState();
|
||||
}
|
||||
|
||||
class _AddOrEditComboItemPopupState extends ConsumerState<AddOrEditComboItem> {
|
||||
Product? selectedProduct;
|
||||
Stock? selectedStock;
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
final TextEditingController qtyController = TextEditingController();
|
||||
final TextEditingController unitController = TextEditingController();
|
||||
final TextEditingController priceController = TextEditingController();
|
||||
final TextEditingController totalController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.existingItem != null) {
|
||||
final item = widget.existingItem!;
|
||||
selectedProduct = item.product;
|
||||
selectedStock = item.stockData;
|
||||
|
||||
if (item.product.productType == 'variant' && selectedStock != null) {
|
||||
searchController.text = "${item.product.productName} - ${selectedStock?.variantName}";
|
||||
} else {
|
||||
searchController.text = item.product.productName ?? '';
|
||||
}
|
||||
|
||||
qtyController.text = item.quantity.toString();
|
||||
unitController.text = item.product.unit?.unitName ?? 'Pcs';
|
||||
|
||||
priceController.text = (item.manualPurchasePrice ?? selectedStock?.productPurchasePrice ?? 0).toString();
|
||||
|
||||
_calculateTotal();
|
||||
}
|
||||
|
||||
// if (widget.existingItem != null) {
|
||||
// // Load existing data for Edit Mode
|
||||
// final item = widget.existingItem!;
|
||||
// selectedProduct = item.product;
|
||||
// selectedStock = item.stockData;
|
||||
// searchController.text = item.product.productName ?? '';
|
||||
// qtyController.text = item.quantity.toString();
|
||||
// unitController.text = item.product.unit?.unitName ?? 'Pcs';
|
||||
// priceController.text = item.purchasePrice.toString();
|
||||
// _calculateTotal();
|
||||
// } else {
|
||||
// // Add Mode Defaults
|
||||
// qtyController.text = '1';
|
||||
// unitController.text = 'Pcs';
|
||||
// }
|
||||
}
|
||||
|
||||
void _calculateTotal() {
|
||||
double qty = double.tryParse(qtyController.text) ?? 0;
|
||||
double price = double.tryParse(priceController.text) ?? 0;
|
||||
totalController.text = (qty * price).toStringAsFixed(2);
|
||||
}
|
||||
|
||||
late var _searchController = TextEditingController();
|
||||
// Product? selectedCustomer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productListAsync = ref.watch(productProvider);
|
||||
final _theme = Theme.of(context);
|
||||
final _lang = l.S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.existingItem == null ? _lang.addProduct : _lang.editProduct,
|
||||
style: _theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
centerTitle: false,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
size: 22,
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1.0),
|
||||
child: Divider(height: 1, thickness: 1, color: kBottomBorder),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.existingItem == null) ...[
|
||||
// --------------use typehead---------------------
|
||||
productListAsync.when(
|
||||
data: (products) {
|
||||
// Filter out combos
|
||||
final filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
|
||||
return TypeAheadField<Map<String, dynamic>>(
|
||||
emptyBuilder: (context) => Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(_lang.noItemFound),
|
||||
),
|
||||
builder: (context, controller, focusNode) {
|
||||
_searchController = controller;
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
hintText: selectedProduct != null ? selectedProduct?.productName : _lang.searchProduct,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
selectedProduct = null;
|
||||
selectedStock = null;
|
||||
setState(() {});
|
||||
},
|
||||
icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
suggestionsCallback: (pattern) {
|
||||
final query = pattern.toLowerCase().trim();
|
||||
final List<Map<String, dynamic>> suggestions = [];
|
||||
|
||||
for (var product in filteredProducts) {
|
||||
// Skip combo products (already filtered above)
|
||||
if (product.productType != 'variant') {
|
||||
final productName = (product.productName ?? '').toLowerCase();
|
||||
if (query.isEmpty || productName.contains(query)) {
|
||||
suggestions.add({'type': 'single', 'product': product});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Variant product
|
||||
bool headerAdded = false;
|
||||
final parentName = (product.productName ?? '').toLowerCase();
|
||||
|
||||
for (var s in product.stocks ?? []) {
|
||||
final variantName = (s.variantName ?? '').toLowerCase();
|
||||
|
||||
// Combine parent name + variant name for searching
|
||||
final combinedName = '$parentName $variantName';
|
||||
|
||||
if (query.isEmpty || combinedName.contains(query)) {
|
||||
if (!headerAdded) {
|
||||
suggestions.add({'type': 'header', 'product': product});
|
||||
headerAdded = true;
|
||||
}
|
||||
suggestions.add({
|
||||
'type': 'variant',
|
||||
'product': product,
|
||||
'stock': s,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final query = pattern.toLowerCase().trim();
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// if (product.productType != 'variant') {
|
||||
// // Single product is selectable
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// // Variant parent is only a header
|
||||
// bool headerAdded = false;
|
||||
//
|
||||
// // Check if parent name matches
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// // Check variant names
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// final variantName = (s.variantName ?? '').toLowerCase();
|
||||
// if (query.isEmpty || variantName.contains(query)) {
|
||||
// if (!headerAdded) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
itemBuilder: (context, suggestion) {
|
||||
final type = suggestion['type'] as String;
|
||||
|
||||
if (type == 'header') {
|
||||
final p = suggestion['product'] as Product;
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// Just close the suggestion box without selecting anything
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Icon(Icons.circle, color: Colors.black, size: 10),
|
||||
title: Text(
|
||||
p.productName ?? '',
|
||||
style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (type == 'variant') {
|
||||
final product = suggestion['product'] as Product;
|
||||
final stock = suggestion['stock'] as Stock;
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
subtitle: Text(
|
||||
'${_lang.stock}: ${stock.productStock}, ${_lang.price}: $currency${stock.productPurchasePrice}, ${_lang.batch}: ${stock.batchNo}'),
|
||||
);
|
||||
}
|
||||
|
||||
// single product
|
||||
final product = suggestion['product'] as Product;
|
||||
return ListTile(
|
||||
title: Text(product.productName ?? ''),
|
||||
subtitle: Text(
|
||||
'${_lang.stock}: ${product.stocksSumProductStock ?? 0}, ${_lang.price}: $currency${product.productPurchasePrice}'),
|
||||
);
|
||||
},
|
||||
onSelected: (suggestion) {
|
||||
final type = suggestion['type'] as String;
|
||||
|
||||
if (type == 'variant' || type == 'single') {
|
||||
final product = suggestion['product'] as Product;
|
||||
|
||||
setState(() {
|
||||
selectedProduct = product;
|
||||
|
||||
if (type == 'variant') {
|
||||
selectedStock = suggestion['stock'] as Stock;
|
||||
} else {
|
||||
selectedStock = product.stocks?.isNotEmpty == true ? product.stocks!.first : null;
|
||||
}
|
||||
|
||||
_searchController.text = type == 'variant'
|
||||
? "${product.productName} - ${selectedStock?.variantName}"
|
||||
: product.productName ?? '';
|
||||
|
||||
unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
priceController.text = (selectedStock?.productPurchasePrice ?? 0).toStringAsFixed(2);
|
||||
|
||||
_calculateTotal();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => LinearProgressIndicator(),
|
||||
error: (e, _) => Text("Error: $e"),
|
||||
),
|
||||
// productListAsync.when(
|
||||
// data: (products) {
|
||||
// final List<Product> filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
//
|
||||
// return TypeAheadField<Map<String, dynamic>>(
|
||||
// emptyBuilder: (context) => Padding(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// child: Text("No item found"),
|
||||
// ),
|
||||
// builder: (context, controller, focusNode) {
|
||||
// _searchController = controller;
|
||||
// return TextField(
|
||||
// controller: controller,
|
||||
// focusNode: focusNode,
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
// hintText: selectedProduct != null ? selectedProduct?.productName : 'Search product',
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// controller.clear();
|
||||
// selectedProduct = null;
|
||||
// selectedStock = null;
|
||||
// setState(() {});
|
||||
// },
|
||||
// icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final query = pattern.toLowerCase().trim();
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// final productName = (product.productName ?? '').toLowerCase();
|
||||
// if (product.productType != 'variant') {
|
||||
// if (query.isEmpty || productName.contains(query)) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// bool headerAdded = false;
|
||||
//
|
||||
// if (query.isEmpty) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
//
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (productName.contains(query)) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// for (var s in product.stocks ?? []) {
|
||||
// final variantName = (s.variantName ?? '').toLowerCase();
|
||||
//
|
||||
// if (variantName.contains(query)) {
|
||||
// if (!headerAdded) {
|
||||
// // Only add header once
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// headerAdded = true;
|
||||
// }
|
||||
//
|
||||
// suggestions.add({
|
||||
// 'type': 'variant',
|
||||
// 'product': product,
|
||||
// 'stock': s,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
// itemBuilder: (context, suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// if (type == 'header') {
|
||||
// final p = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.circle, color: Colors.black, size: 10),
|
||||
// title: Text(
|
||||
// p.productName ?? '',
|
||||
// style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// // header is not selectable, so we make it visually disabled
|
||||
// enabled: false,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// final stock = suggestion['stock'] as Stock;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
// title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
// subtitle: Text('Stock: ${stock.productStock}, Price: $currency${stock.productPurchasePrice}, Batch: ${stock.batchNo}'),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// // single product
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// title: Text(product.productName ?? ''),
|
||||
// subtitle: Text('Stock: ${product.stocksSumProductStock ?? 0}, Price: $currency${product.productPurchasePrice}'),
|
||||
// );
|
||||
// },
|
||||
// onSelected: (suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// // Only allow single or variant selection
|
||||
// if (type == 'single' || type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// setState(() {
|
||||
// selectedProduct = product;
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// selectedStock = suggestion['stock'] as Stock;
|
||||
// } else {
|
||||
// selectedStock = product.stocks?.isNotEmpty == true ? product.stocks!.first : null;
|
||||
// }
|
||||
//
|
||||
// // Update search field
|
||||
// _searchController.text = type == 'variant' ? "${product.productName} - ${selectedStock?.variantName}" : product.productName ?? '';
|
||||
//
|
||||
// // Update unit field
|
||||
// unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
//
|
||||
// // Update price field
|
||||
// priceController.text = (selectedStock?.productPurchasePrice ?? 0).toStringAsFixed(2);
|
||||
//
|
||||
// // Recalculate total
|
||||
// _calculateTotal();
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// loading: () => LinearProgressIndicator(),
|
||||
// error: (e, _) => Text("Error: $e"),
|
||||
// ),
|
||||
// --------------use typehead---------------------
|
||||
] else ...[
|
||||
TextFormField(
|
||||
controller: searchController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.product,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// --------previous code-----------------
|
||||
// if (widget.existingItem == null) ...[
|
||||
// // --------------use typehead---------------------
|
||||
// productListAsync.when(
|
||||
// data: (products) {
|
||||
// // Filter out combo products
|
||||
// final filteredProducts = products.where((p) => p.productType != 'combo').toList();
|
||||
//
|
||||
// return TypeAheadField<Map<String, dynamic>>(
|
||||
// builder: (context, controller, focusNode) {
|
||||
// return TextField(
|
||||
// controller: _searchController,
|
||||
// focusNode: focusNode,
|
||||
// decoration: InputDecoration(
|
||||
// prefixIcon: Icon(AntDesign.search_outline, color: kGreyTextColor),
|
||||
// hintText: selectedProduct != null ? selectedProduct?.productName : 'Search product',
|
||||
// suffixIcon: IconButton(
|
||||
// onPressed: () {
|
||||
// _searchController.clear();
|
||||
// selectedProduct = null;
|
||||
// setState(() {});
|
||||
// },
|
||||
// icon: Icon(Icons.close, color: kSubPeraColor),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// suggestionsCallback: (pattern) {
|
||||
// final List<Map<String, dynamic>> suggestions = [];
|
||||
//
|
||||
// for (var product in filteredProducts) {
|
||||
// if (product.productType == 'variant') {
|
||||
// // Show parent product as a header if it matches the search
|
||||
// if ((product.productName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'header', 'product': product});
|
||||
// }
|
||||
//
|
||||
// // Show variant stocks
|
||||
// for (var stock in product.stocks ?? []) {
|
||||
// if ((stock.variantName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'variant', 'product': product, 'stock': stock});
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Single product
|
||||
// if ((product.productName ?? '').toLowerCase().contains(pattern.toLowerCase())) {
|
||||
// suggestions.add({'type': 'single', 'product': product});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return suggestions;
|
||||
// },
|
||||
// itemBuilder: (context, suggestion) {
|
||||
// final type = suggestion['type'] as String;
|
||||
// if (type == 'header') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(
|
||||
// Icons.circle,
|
||||
// color: Colors.black,
|
||||
// size: 10,
|
||||
// ),
|
||||
// title: Text(
|
||||
// product.productName ?? '',
|
||||
// style: _theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// );
|
||||
// } else if (type == 'variant') {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// final stock = suggestion['stock'] as Stock;
|
||||
// return ListTile(
|
||||
// contentPadding: EdgeInsets.symmetric(horizontal: 10),
|
||||
// visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
// leading: Icon(Icons.subdirectory_arrow_right, color: Colors.grey, size: 18),
|
||||
// title: Text("${product.productName} (${stock.variantName ?? 'n/a'})"),
|
||||
// subtitle: Text('Stock: ${stock.productStock}, Price: $currency${stock.productPurchasePrice}, Batch: ${stock.batchNo}'),
|
||||
// );
|
||||
// } else {
|
||||
// final product = suggestion['product'] as Product;
|
||||
// return ListTile(
|
||||
// title: Text(product.productName ?? ''),
|
||||
// subtitle: Text('Stock: ${product.stocksSumProductStock ?? 0}, Price: $currency${product.productPurchasePrice}'),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// onSelected: (suggestion) {
|
||||
// setState(() {
|
||||
// final type = suggestion['type'] as String;
|
||||
// final product = suggestion['product'] as Product;
|
||||
//
|
||||
// selectedProduct = product;
|
||||
//
|
||||
// if (type == 'variant') {
|
||||
// selectedStock = suggestion['stock'] as Stock;
|
||||
// } else {
|
||||
// selectedStock = product.stocks != null && product.stocks!.isNotEmpty ? product.stocks!.first : null;
|
||||
// }
|
||||
//
|
||||
// _searchController.text = type == 'variant' ? "${product.productName} - ${selectedStock?.variantName}" : product.productName ?? '';
|
||||
//
|
||||
// unitController.text = product.unit?.unitName ?? 'Pcs';
|
||||
// priceController.text = (selectedStock?.productPurchasePrice ?? 0).toString();
|
||||
// _calculateTotal();
|
||||
// });
|
||||
//
|
||||
// FocusScope.of(context).unfocus();
|
||||
// },
|
||||
// );
|
||||
// },
|
||||
// loading: () => const Center(child: LinearProgressIndicator()),
|
||||
// error: (e, stack) => Text('Error: $e'),
|
||||
// ),
|
||||
// // --------------use typehead---------------------
|
||||
// ] else ...[
|
||||
// TextFormField(
|
||||
// controller: searchController,
|
||||
// readOnly: true,
|
||||
// decoration: const InputDecoration(
|
||||
// labelText: 'Product',
|
||||
// border: OutlineInputBorder(),
|
||||
// filled: true,
|
||||
// fillColor: Color(0xFFF5F5F5),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
SizedBox(height: 20),
|
||||
// --- Row 1: Quantity & Units ---
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: qtyController,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.quantity,
|
||||
hintText: 'Ex: 1',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _calculateTotal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: unitController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.units,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Row 2: Purchase Price & Total ---
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: priceController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.purchasePrice,
|
||||
hintText: 'Ex: 20',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) => _calculateTotal(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: totalController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.total,
|
||||
border: OutlineInputBorder(),
|
||||
filled: true,
|
||||
fillColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: DAppColors.kWarning),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
_lang.cancel,
|
||||
style: TextStyle(
|
||||
color: DAppColors.kWarning,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
// minimumSize: Size.fromHeight(48),
|
||||
backgroundColor: const Color(0xFFB71C1C), // Red color
|
||||
),
|
||||
onPressed: () {
|
||||
if (selectedProduct != null && selectedStock != null) {
|
||||
final newItem = ComboItem(
|
||||
product: selectedProduct!,
|
||||
stockData: selectedStock!,
|
||||
quantity: int.tryParse(qtyController.text) ?? 1,
|
||||
manualPurchasePrice: double.tryParse(priceController.text),
|
||||
);
|
||||
widget.onSubmit(newItem);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Please select a product")));
|
||||
}
|
||||
},
|
||||
child: Text(_lang.save, style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1225
lib/Screens/Products/add product/add_product.dart
Normal file
1225
lib/Screens/Products/add product/add_product.dart
Normal file
File diff suppressed because it is too large
Load Diff
318
lib/Screens/Products/add product/combo_product_form.dart
Normal file
318
lib/Screens/Products/add product/combo_product_form.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/invoice_constant.dart' hide kMainColor;
|
||||
import '../../../Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as l;
|
||||
import 'add_edit_comboItem.dart';
|
||||
import 'modle/create_product_model.dart';
|
||||
|
||||
// Updated Helper Model to support manual price override
|
||||
class ComboItem {
|
||||
final Product product;
|
||||
final Stock stockData;
|
||||
int quantity;
|
||||
double? manualPurchasePrice; // Added this field
|
||||
|
||||
ComboItem({
|
||||
required this.product,
|
||||
required this.stockData,
|
||||
this.quantity = 1,
|
||||
this.manualPurchasePrice,
|
||||
});
|
||||
|
||||
// Use manual price if set, otherwise stock price
|
||||
double get purchasePrice => manualPurchasePrice ?? (stockData.productPurchasePrice ?? 0).toDouble();
|
||||
double get totalAmount => purchasePrice * quantity;
|
||||
}
|
||||
|
||||
class ComboProductForm extends ConsumerStatefulWidget {
|
||||
final TextEditingController profitController;
|
||||
final TextEditingController saleController;
|
||||
final TextEditingController purchasePriceController;
|
||||
final List<ComboProductModel>? initialComboList;
|
||||
final Function(List<ComboProductModel>) onComboListChanged;
|
||||
|
||||
const ComboProductForm({
|
||||
super.key,
|
||||
required this.profitController,
|
||||
required this.saleController,
|
||||
required this.purchasePriceController,
|
||||
this.initialComboList,
|
||||
required this.onComboListChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<ComboProductForm> createState() => _ComboProductFormState();
|
||||
}
|
||||
|
||||
class _ComboProductFormState extends ConsumerState<ComboProductForm> {
|
||||
List<ComboItem> selectedComboItems = [];
|
||||
bool _isDataLoaded = false;
|
||||
|
||||
// --- Calculation Logic (Same as before) ---
|
||||
void _calculateValues({String? source}) {
|
||||
double totalPurchase = 0;
|
||||
for (var item in selectedComboItems) {
|
||||
totalPurchase += item.totalAmount;
|
||||
}
|
||||
|
||||
if (widget.purchasePriceController.text != totalPurchase.toStringAsFixed(2)) {
|
||||
widget.purchasePriceController.text = totalPurchase.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
double purchase = totalPurchase;
|
||||
double profit = double.tryParse(widget.profitController.text) ?? 0;
|
||||
double sale = double.tryParse(widget.saleController.text) ?? 0;
|
||||
|
||||
if (source == 'margin') {
|
||||
sale = purchase + (purchase * profit / 100);
|
||||
widget.saleController.text = sale.toStringAsFixed(2);
|
||||
} else if (source == 'sale') {
|
||||
if (purchase > 0) {
|
||||
profit = ((sale - purchase) / purchase) * 100;
|
||||
widget.profitController.text = profit.toStringAsFixed(2);
|
||||
}
|
||||
} else {
|
||||
sale = purchase + (purchase * profit / 100);
|
||||
widget.saleController.text = sale.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
List<ComboProductModel> finalApiList = selectedComboItems.map((item) {
|
||||
return ComboProductModel(
|
||||
stockId: item.stockData.id.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
purchasePrice: item.purchasePrice.toString(),
|
||||
);
|
||||
}).toList();
|
||||
|
||||
widget.onComboListChanged(finalApiList);
|
||||
}
|
||||
|
||||
// --- Open the Popup for Add or Edit ---
|
||||
void openProductForm({ComboItem? item, int? index}) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddOrEditComboItem(
|
||||
existingItem: item,
|
||||
onSubmit: (newItem) {
|
||||
setState(() {
|
||||
if (index != null) {
|
||||
// Edit Mode: Replace item
|
||||
selectedComboItems[index] = newItem;
|
||||
} else {
|
||||
// Add Mode: Check duplicate or add new
|
||||
bool exists = false;
|
||||
for (int i = 0; i < selectedComboItems.length; i++) {
|
||||
if (selectedComboItems[i].stockData.id == newItem.stockData.id) {
|
||||
// If same product exists, just update that entry
|
||||
selectedComboItems[i] = newItem;
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exists) selectedComboItems.add(newItem);
|
||||
}
|
||||
_calculateValues(source: 'item_updated');
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productListAsync = ref.watch(productProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
// Load Initial Data Logic
|
||||
productListAsync.whenData((products) {
|
||||
if (!_isDataLoaded && widget.initialComboList != null && widget.initialComboList!.isNotEmpty) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
List<ComboItem> tempLoadedItems = [];
|
||||
for (var initialItem in widget.initialComboList!) {
|
||||
for (var product in products) {
|
||||
if (product.stocks != null) {
|
||||
try {
|
||||
var matchingStock =
|
||||
product.stocks!.firstWhere((s) => s.id.toString() == initialItem.stockId.toString());
|
||||
tempLoadedItems.add(ComboItem(
|
||||
product: product,
|
||||
stockData: matchingStock,
|
||||
quantity: int.tryParse(initialItem.quantity.toString()) ?? 1,
|
||||
manualPurchasePrice: double.tryParse(initialItem.purchasePrice.toString()),
|
||||
));
|
||||
break;
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
selectedComboItems = tempLoadedItems;
|
||||
_isDataLoaded = true;
|
||||
});
|
||||
_calculateValues(source: 'init');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Add Product Button
|
||||
ElevatedButton(
|
||||
onPressed: () => openProductForm(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor50, // Light reddish background
|
||||
minimumSize: Size(131, 36),
|
||||
elevation: 0,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
),
|
||||
child: Text(
|
||||
"+ ${l.S.of(context).addProduct}",
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: kMainColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 2. List of Items (Matching Screenshot 1)
|
||||
if (selectedComboItems.isNotEmpty)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: selectedComboItems.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
height: 1,
|
||||
color: kLineColor,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = selectedComboItems[index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
item.product.productType == 'single'
|
||||
? item.product.productName ?? 'n/a'
|
||||
: ('${item.product.productName ?? ''} (${item.product.stocks?[index].variantName ?? 'n/a'})'),
|
||||
style: _theme.textTheme.bodyLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${l.S.of(context).qty}: ${item.quantity}',
|
||||
style: _theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${l.S.of(context).code} : ${item.product.productCode ?? 'n/a'}, ${l.S.of(context).batchNo}: ${item.stockData.batchNo ?? 'n/a'}',
|
||||
style: _theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${item.totalAmount ?? 'n/a'}',
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
iconColor: kPeraColor,
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
openProductForm(item: item, index: index);
|
||||
} else if (value == 'delete') {
|
||||
setState(() {
|
||||
selectedComboItems.removeAt(index);
|
||||
_calculateValues(source: 'item_removed');
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(value: 'edit', child: Text(l.S.of(context).edit)),
|
||||
PopupMenuItem(
|
||||
value: 'delete', child: Text(l.S.of(context).delete, style: TextStyle(color: Colors.red))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (selectedComboItems.isNotEmpty)
|
||||
const Divider(
|
||||
height: 1,
|
||||
color: kLineColor,
|
||||
),
|
||||
SizedBox(height: 13),
|
||||
// 3. Footer: Net Total, Profit, Sale Price
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text("${l.S.of(context).netTotalAmount}:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
Text("\$${widget.purchasePriceController.text}",
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.profitController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l.S.of(context).profitMargin} (%)',
|
||||
hintText: 'Ex: 25%',
|
||||
border: OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
onChanged: (value) => _calculateValues(source: 'margin'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.saleController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: l.S.of(context).defaultSellingPrice,
|
||||
hintText: 'Ex: 150',
|
||||
border: OutlineInputBorder(),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
),
|
||||
onChanged: (value) => _calculateValues(source: 'sale'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
150
lib/Screens/Products/add product/modle/create_product_model.dart
Normal file
150
lib/Screens/Products/add product/modle/create_product_model.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:io';
|
||||
|
||||
// Enum for clearer logic in UI
|
||||
enum ProductType { single, variant, combo }
|
||||
|
||||
// --- 1. Combo Product Model ---
|
||||
class ComboProductModel {
|
||||
ComboProductModel({
|
||||
this.stockId,
|
||||
this.quantity,
|
||||
this.purchasePrice,
|
||||
});
|
||||
|
||||
String? stockId;
|
||||
String? quantity;
|
||||
String? purchasePrice;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'stock_id': stockId,
|
||||
'quantity': quantity,
|
||||
'purchase_price': purchasePrice,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. Stock Data Model (Existing) ---
|
||||
class StockDataModel {
|
||||
StockDataModel({
|
||||
this.stockId,
|
||||
this.batchNo,
|
||||
this.warehouseId,
|
||||
this.productStock,
|
||||
this.exclusivePrice,
|
||||
this.inclusivePrice,
|
||||
this.profitPercent,
|
||||
this.productSalePrice,
|
||||
this.productWholeSalePrice,
|
||||
this.productDealerPrice,
|
||||
this.mfgDate,
|
||||
this.expireDate,
|
||||
this.serialNumbers,
|
||||
this.variantName,
|
||||
this.variationData,
|
||||
this.subStock,
|
||||
});
|
||||
|
||||
String? stockId;
|
||||
String? batchNo;
|
||||
String? warehouseId;
|
||||
String? productStock;
|
||||
String? exclusivePrice;
|
||||
String? inclusivePrice;
|
||||
String? profitPercent;
|
||||
String? productSalePrice;
|
||||
String? productWholeSalePrice;
|
||||
String? productDealerPrice;
|
||||
String? mfgDate;
|
||||
String? expireDate;
|
||||
List<String>? serialNumbers;
|
||||
bool? subStock;
|
||||
String? variantName;
|
||||
List<Map<String, dynamic>>? variationData;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'stock_id': stockId,
|
||||
'batch_no': batchNo,
|
||||
'warehouse_id': warehouseId,
|
||||
'productStock': productStock,
|
||||
'exclusive_price': exclusivePrice,
|
||||
'inclusive_price': inclusivePrice,
|
||||
'profit_percent': profitPercent == 'Infinity' ? '0' : profitPercent,
|
||||
'productSalePrice': productSalePrice,
|
||||
'productWholeSalePrice': productWholeSalePrice,
|
||||
'productDealerPrice': productDealerPrice,
|
||||
'mfg_date': mfgDate,
|
||||
'expire_date': expireDate,
|
||||
'serial_numbers': serialNumbers,
|
||||
'variant_name': variantName,
|
||||
'variation_data': variationData,
|
||||
};
|
||||
data.removeWhere((key, value) => value == null || value.toString().isEmpty || value == 'null');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Main Create Product Model ---
|
||||
class CreateProductModel {
|
||||
CreateProductModel({
|
||||
this.productId,
|
||||
this.name,
|
||||
this.categoryId,
|
||||
this.brandId,
|
||||
this.productCode,
|
||||
this.modelId,
|
||||
this.rackId,
|
||||
this.shelfId,
|
||||
this.alertQty,
|
||||
this.unitId,
|
||||
this.vatId,
|
||||
this.vatType,
|
||||
this.vatAmount,
|
||||
this.image,
|
||||
this.productType,
|
||||
this.stocks,
|
||||
this.comboProducts,
|
||||
this.variationIds,
|
||||
this.warrantyDuration,
|
||||
this.warrantyPeriod,
|
||||
this.guaranteeDuration,
|
||||
this.guaranteePeriod,
|
||||
this.productManufacturer,
|
||||
this.productDiscount,
|
||||
this.comboProfitPercent,
|
||||
this.comboProductSalePrice,
|
||||
});
|
||||
|
||||
String? productId;
|
||||
String? name;
|
||||
String? categoryId;
|
||||
String? brandId;
|
||||
String? productCode;
|
||||
String? modelId;
|
||||
String? rackId;
|
||||
String? shelfId;
|
||||
String? alertQty;
|
||||
String? unitId;
|
||||
String? vatId;
|
||||
String? vatType;
|
||||
String? vatAmount;
|
||||
File? image;
|
||||
String? productType;
|
||||
String? comboProfitPercent;
|
||||
String? comboProductSalePrice;
|
||||
|
||||
// Lists
|
||||
List<StockDataModel>? stocks;
|
||||
List<ComboProductModel>? comboProducts;
|
||||
List<String?>? variationIds;
|
||||
|
||||
String? productManufacturer;
|
||||
String? productDiscount;
|
||||
|
||||
String? warrantyDuration;
|
||||
String? warrantyPeriod;
|
||||
String? guaranteeDuration;
|
||||
String? guaranteePeriod;
|
||||
}
|
||||
373
lib/Screens/Products/add product/single_product_form.dart
Normal file
373
lib/Screens/Products/add product/single_product_form.dart
Normal file
@@ -0,0 +1,373 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconly/iconly.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/model/get_product_setting_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:nb_utils/nb_utils.dart';
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../warehouse/warehouse_provider/warehouse_provider.dart';
|
||||
|
||||
class SingleProductForm extends ConsumerWidget {
|
||||
const SingleProductForm({
|
||||
super.key,
|
||||
required this.snapShot,
|
||||
required this.batchController,
|
||||
required this.stockController,
|
||||
required this.purchaseExController,
|
||||
required this.purchaseIncController,
|
||||
required this.profitController,
|
||||
required this.saleController,
|
||||
required this.wholesaleController,
|
||||
required this.dealerController,
|
||||
required this.mfgDateController,
|
||||
required this.expDateController,
|
||||
this.selectedWarehouse,
|
||||
required this.onWarehouseChanged,
|
||||
required this.onPriceChanged,
|
||||
required this.onMfgDateSelected,
|
||||
required this.onExpDateSelected,
|
||||
});
|
||||
|
||||
final GetProductSettingModel snapShot;
|
||||
|
||||
// Controllers passed from Parent
|
||||
final TextEditingController batchController;
|
||||
final TextEditingController stockController;
|
||||
final TextEditingController purchaseExController;
|
||||
final TextEditingController purchaseIncController;
|
||||
final TextEditingController profitController;
|
||||
final TextEditingController saleController;
|
||||
final TextEditingController wholesaleController;
|
||||
final TextEditingController dealerController;
|
||||
final TextEditingController mfgDateController;
|
||||
final TextEditingController expDateController;
|
||||
|
||||
// State variables passed from Parent
|
||||
final WarehouseData? selectedWarehouse;
|
||||
|
||||
// Callbacks to update Parent State
|
||||
final Function(WarehouseData?) onWarehouseChanged;
|
||||
final Function(String from) onPriceChanged; // To trigger calculation
|
||||
final Function(String date) onMfgDateSelected;
|
||||
final Function(String date) onExpDateSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final permissionService = PermissionService(ref);
|
||||
final warehouseData = ref.watch(fetchWarehouseListProvider);
|
||||
final modules = snapShot.data?.modules;
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
///-------------Batch No & Warehouse----------------------------------
|
||||
if (modules?.showBatchNo == '1' || modules?.showWarehouse == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showBatchNo == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: batchController,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.batchNo,
|
||||
hintText: _lang.enterBatchNo,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showBatchNo == '1' && modules?.showWarehouse == '1') const SizedBox(width: 14),
|
||||
if (modules?.showWarehouse == '1')
|
||||
Expanded(
|
||||
child: warehouseData.when(
|
||||
data: (dataList) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
DropdownButtonFormField<WarehouseData>(
|
||||
hint: Text(_lang.selectWarehouse),
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: _lang.warehouse,
|
||||
),
|
||||
value: selectedWarehouse,
|
||||
icon: selectedWarehouse != null
|
||||
? IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -4,
|
||||
),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
onWarehouseChanged.call(null);
|
||||
},
|
||||
)
|
||||
: const Icon(Icons.keyboard_arrow_down_outlined),
|
||||
items: dataList.data
|
||||
?.map(
|
||||
(rack) => DropdownMenuItem<WarehouseData>(
|
||||
value: rack,
|
||||
child: Text(
|
||||
rack.name ?? '',
|
||||
style: const TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: onWarehouseChanged,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (e, st) => const Text('Warehouse Load Error'),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
// child: warehouseData.when(
|
||||
// data: (dataList) {
|
||||
// return DropdownButtonFormField<WarehouseData>(
|
||||
// hint: const Text('Select Warehouse'),
|
||||
// isExpanded: true,
|
||||
// decoration: const InputDecoration(labelText: 'Warehouse', border: OutlineInputBorder()),
|
||||
// value: selectedWarehouse,
|
||||
// icon: const Icon(Icons.keyboard_arrow_down_outlined),
|
||||
// items: dataList.data
|
||||
// ?.map(
|
||||
// (rack) => DropdownMenuItem<WarehouseData>(
|
||||
// value: rack,
|
||||
// child: Text(rack.name ?? '', style: const TextStyle(fontWeight: FontWeight.normal)),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// onChanged: onWarehouseChanged,
|
||||
// );
|
||||
// },
|
||||
// error: (e, st) => const Text('Rack Load Error'),
|
||||
// loading: () => const Center(child: CircularProgressIndicator()),
|
||||
// ),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (modules?.showProductStock == '1') const SizedBox(height: 24),
|
||||
|
||||
///-------------Stock--------------------------------------
|
||||
if (modules?.showProductStock == '1')
|
||||
TextFormField(
|
||||
controller: stockController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).stock,
|
||||
hintText: lang.S.of(context).enterStock,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
|
||||
///_________Purchase Price (Exclusive & Inclusive)____________________
|
||||
if ((modules?.showExclusivePrice == '1' || modules?.showInclusivePrice == '1') &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value)) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showExclusivePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: purchaseExController,
|
||||
onChanged: (value) => onPriceChanged('purchase_ex'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).purchaseEx,
|
||||
hintText: lang.S.of(context).enterPurchasePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showExclusivePrice == '1' && modules?.showInclusivePrice == '1') const SizedBox(width: 14),
|
||||
if (modules?.showInclusivePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: purchaseIncController,
|
||||
onChanged: (value) => onPriceChanged('purchase_inc'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).purchaseIn,
|
||||
hintText: lang.S.of(context).enterSaltingPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_________Profit Margin & MRP_____________________
|
||||
if (modules?.showProfitPercent == '1' || modules?.showProductSalePrice == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
(permissionService.hasPermission(Permit.productsPriceView.value)))
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: profitController,
|
||||
onChanged: (value) => onPriceChanged('profit_margin'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).profitMargin,
|
||||
hintText: lang.S.of(context).enterPurchasePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
modules?.showProductSalePrice == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
const SizedBox(width: 14),
|
||||
if (modules?.showProductSalePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: saleController,
|
||||
onChanged: (value) => onPriceChanged('mrp'),
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).mrp,
|
||||
hintText: lang.S.of(context).enterSaltingPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_______Wholesale & Dealer Price_________________
|
||||
if (modules?.showProductWholesalePrice == '1' || modules?.showProductDealerPrice == '1') ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showProductWholesalePrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: wholesaleController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).wholeSalePrice,
|
||||
hintText: lang.S.of(context).enterWholesalePrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showProductWholesalePrice == '1' && modules?.showProductDealerPrice == '1')
|
||||
const SizedBox(width: 14),
|
||||
if (modules?.showProductDealerPrice == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: dealerController,
|
||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: lang.S.of(context).dealerPrice,
|
||||
hintText: lang.S.of(context).enterDealerPrice,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
///_______Dates_________________
|
||||
if ((modules?.showMfgDate == '1') || (modules?.showExpireDate == '1')) ...[
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
if (modules?.showMfgDate == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: mfgDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).manuDate,
|
||||
hintText: lang.S.of(context).selectDate,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
onMfgDateSelected(picked.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (modules?.showMfgDate == '1' && modules?.showExpireDate == '1') const SizedBox(width: 14),
|
||||
if (modules?.showExpireDate == '1')
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
readOnly: true,
|
||||
controller: expDateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).expDate,
|
||||
hintText: lang.S.of(context).selectDate,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2015, 8),
|
||||
lastDate: DateTime(2101),
|
||||
context: context,
|
||||
);
|
||||
if (picked != null) {
|
||||
onExpDateSelected(picked.toString());
|
||||
}
|
||||
},
|
||||
icon: const Icon(IconlyLight.calendar, size: 22),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
898
lib/Screens/Products/add product/variant_product_form.dart
Normal file
898
lib/Screens/Products/add product/variant_product_form.dart
Normal file
@@ -0,0 +1,898 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Screens/Products/product_setting/model/get_product_setting_model.dart';
|
||||
import 'package:mobile_pos/Screens/warehouse/warehouse_model/warehouse_list_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../../constant.dart';
|
||||
import '../../../currency.dart';
|
||||
import '../../../service/check_user_role_permission_provider.dart';
|
||||
import '../../product variation/model/product_variation_model.dart';
|
||||
import '../../product variation/provider/product_variation_provider.dart';
|
||||
import '../../vat_&_tax/model/vat_model.dart';
|
||||
import '../../warehouse/warehouse_provider/warehouse_provider.dart';
|
||||
import '../Widgets/acnoo_multiple_select_dropdown.dart';
|
||||
import '../Widgets/dropdown_styles.dart';
|
||||
import 'modle/create_product_model.dart';
|
||||
|
||||
class VariantProductForm extends ConsumerStatefulWidget {
|
||||
const VariantProductForm({
|
||||
super.key,
|
||||
required this.initialStocks,
|
||||
required this.onStocksUpdated,
|
||||
required this.snapShot,
|
||||
this.selectedWarehouse,
|
||||
required this.onSelectVariation,
|
||||
this.tax,
|
||||
required this.taxType,
|
||||
this.productVariationIds,
|
||||
this.productCode,
|
||||
});
|
||||
|
||||
final List<StockDataModel> initialStocks;
|
||||
final Function(List<StockDataModel>) onStocksUpdated;
|
||||
final Function(List<String?>) onSelectVariation;
|
||||
final GetProductSettingModel snapShot;
|
||||
final VatModel? tax;
|
||||
final String taxType;
|
||||
final List<String>? productVariationIds;
|
||||
final String? productCode;
|
||||
// State variables passed from Parent
|
||||
final WarehouseData? selectedWarehouse; // Received from parent
|
||||
|
||||
@override
|
||||
ConsumerState<VariantProductForm> createState() => _VariantProductFormState();
|
||||
}
|
||||
|
||||
class _VariantProductFormState extends ConsumerState<VariantProductForm> {
|
||||
List<int?> selectedVariation = [];
|
||||
List<VariationData> variationList = [];
|
||||
Map<num?, List<String>?> selectedVariationValues = {};
|
||||
List<StockDataModel> localVariantStocks = [];
|
||||
|
||||
bool isDataInitialized = false;
|
||||
|
||||
final kLoader = const Center(child: CircularProgressIndicator(strokeWidth: 2));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
localVariantStocks = widget.initialStocks;
|
||||
}
|
||||
|
||||
void generateVariants({bool? changeState}) {
|
||||
if (selectedVariation.isEmpty) {
|
||||
setState(() => localVariantStocks.clear());
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
return;
|
||||
}
|
||||
// 1. Gather active Variations (No Change)
|
||||
List<VariationData> activeVariations = [];
|
||||
List<List<String>> activeValues = [];
|
||||
|
||||
for (var id in selectedVariation) {
|
||||
if (id != null &&
|
||||
selectedVariationValues.containsKey(id) &&
|
||||
selectedVariationValues[id] != null &&
|
||||
selectedVariationValues[id]!.isNotEmpty) {
|
||||
var vData = variationList.firstWhere((element) => element.id == id, orElse: () => VariationData());
|
||||
if (vData.id != null) {
|
||||
activeVariations.add(vData);
|
||||
activeValues.add(selectedVariationValues[id]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeVariations.isEmpty || activeValues.length != activeVariations.length) {
|
||||
setState(() => localVariantStocks = []);
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
return;
|
||||
}
|
||||
;
|
||||
|
||||
// 2. Calculate Cartesian Product (No Change)
|
||||
List<List<String>> cartesian(List<List<String>> lists) {
|
||||
List<List<String>> result = [[]];
|
||||
for (var list in lists) {
|
||||
result = [
|
||||
for (var a in result)
|
||||
for (var b in list) [...a, b]
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<List<String>> combinations = cartesian(activeValues);
|
||||
List<StockDataModel> newStocks = [];
|
||||
|
||||
String baseCode = widget.productCode ?? "";
|
||||
int counter = 1;
|
||||
for (var combo in combinations) {
|
||||
String variantName = combo.join(" - ");
|
||||
List<Map<String, String>> vData = [];
|
||||
for (int i = 0; i < combo.length; i++) {
|
||||
vData.add({activeVariations[i].name ?? '': combo[i]});
|
||||
}
|
||||
|
||||
// Check if this ROOT variant already exists (to preserve edits)
|
||||
var existingIndex = localVariantStocks.indexWhere((element) => element.variantName == variantName);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
StockDataModel parent = localVariantStocks[existingIndex];
|
||||
|
||||
// Updating batch no according to new code structure
|
||||
if (baseCode.isNotEmpty) {
|
||||
parent.batchNo = "$baseCode-$counter";
|
||||
}
|
||||
newStocks.add(parent);
|
||||
} else {
|
||||
// C. New Root Variant
|
||||
String autoBatchNo = baseCode.isNotEmpty ? "$baseCode-$counter" : "";
|
||||
|
||||
newStocks.add(StockDataModel(
|
||||
profitPercent: '0',
|
||||
variantName: variantName,
|
||||
batchNo: autoBatchNo, // NEW LOGIC: 1002-1
|
||||
variationData: vData,
|
||||
productStock: "0",
|
||||
exclusivePrice: "0",
|
||||
inclusivePrice: "0",
|
||||
productSalePrice: "0",
|
||||
));
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
setState(() => localVariantStocks = newStocks);
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
// --- Logic to Initialize Data from Edit Mode ---
|
||||
void _initializeEditData(List<VariationData> allVariations) {
|
||||
if (isDataInitialized) return;
|
||||
if (localVariantStocks.isEmpty && (widget.productVariationIds == null || widget.productVariationIds!.isEmpty))
|
||||
return;
|
||||
|
||||
// 1. Set Selected Variation Types (Example: Size, Color IDs)
|
||||
if (widget.productVariationIds != null) {
|
||||
selectedVariation = widget.productVariationIds!.map((e) => int.tryParse(e)).where((e) => e != null).toList();
|
||||
}
|
||||
|
||||
for (final stock in localVariantStocks) {
|
||||
print('Pioewruwr------------------------> ${stock.variationData}');
|
||||
if (stock.variationData != null) {
|
||||
for (Map<String, dynamic> vMap in stock.variationData!) {
|
||||
print('$vMap');
|
||||
// vMap looks like {"Size": "M"}
|
||||
vMap.forEach((keyName, value) {
|
||||
// Find the ID associated with this Name (e.g., "Size" -> ID 1)
|
||||
final variationObj = allVariations.firstWhere(
|
||||
(element) => element.name?.toLowerCase() == keyName.toLowerCase(),
|
||||
orElse: () => VariationData(),
|
||||
);
|
||||
|
||||
if (variationObj.id != null) {
|
||||
num vId = variationObj.id!;
|
||||
|
||||
// Add value to the list if not exists
|
||||
if (!selectedVariationValues.containsKey(vId)) {
|
||||
selectedVariationValues[vId] = [];
|
||||
}
|
||||
|
||||
if (value is String && !selectedVariationValues[vId]!.contains(value)) {
|
||||
selectedVariationValues[vId]!.add(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDataInitialized = true;
|
||||
Future.microtask(() => setState(() {}));
|
||||
}
|
||||
|
||||
void _addSubVariation(int parentIndex) {
|
||||
final parentStock = localVariantStocks[parentIndex];
|
||||
|
||||
// Ensure parent has a batch number
|
||||
if (parentStock.batchNo == null || parentStock.batchNo!.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Parent must have a Batch No first")));
|
||||
return;
|
||||
}
|
||||
|
||||
// Count existing children to generate ID (e.g., 1001-1, 1001-2)
|
||||
final String parentBatch = parentStock.batchNo!;
|
||||
int childCount = localVariantStocks
|
||||
.where((element) => element.batchNo != null && element.batchNo!.startsWith("$parentBatch-"))
|
||||
.length;
|
||||
|
||||
String newSubBatch = "$parentBatch-${childCount + 1}";
|
||||
|
||||
// Create Child Stock (Copying basic data from parent if needed, or blank)
|
||||
StockDataModel childStock = StockDataModel(
|
||||
variantName: "${parentStock.variantName} (Sub ${childCount + 1})", // Indicating it's a sub
|
||||
batchNo: '',
|
||||
variationData: parentStock.variationData, // Inherit variation traits
|
||||
profitPercent: parentStock.profitPercent ?? '0',
|
||||
productStock: "0",
|
||||
exclusivePrice: parentStock.exclusivePrice ?? "0",
|
||||
inclusivePrice: parentStock.inclusivePrice ?? "0",
|
||||
productSalePrice: parentStock.productSalePrice ?? "0",
|
||||
warehouseId: parentStock.warehouseId,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
// Insert immediately after the parent (and its existing children)
|
||||
// We insert at parentIndex + 1 + childCount to keep them grouped
|
||||
localVariantStocks.insert(parentIndex + 1 + childCount, childStock);
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
void _removeVariation(int index) {
|
||||
final stockToRemove = localVariantStocks[index];
|
||||
final String? batchNo = stockToRemove.batchNo;
|
||||
|
||||
setState(() {
|
||||
localVariantStocks.removeAt(index);
|
||||
|
||||
// If it was a parent, remove all its children (Sub-variations)
|
||||
if (batchNo != null && !batchNo.contains('-')) {
|
||||
localVariantStocks
|
||||
.removeWhere((element) => element.batchNo != null && element.batchNo!.startsWith("$batchNo-"));
|
||||
}
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _dropdownStyle = AcnooDropdownStyle(context);
|
||||
final variationData = ref.watch(variationListProvider);
|
||||
final _theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
|
||||
//------- Variation Type Selection --------------------
|
||||
variationData.when(
|
||||
data: (variation) {
|
||||
variationList = variation.data ?? [];
|
||||
|
||||
// -----------------------------------------
|
||||
// HERE IS THE FIX: Initialize Data Once
|
||||
// -----------------------------------------
|
||||
if (!isDataInitialized && variationList.isNotEmpty) {
|
||||
_initializeEditData(variationList);
|
||||
}
|
||||
|
||||
return AcnooMultiSelectDropdown(
|
||||
menuItemStyleData: _dropdownStyle.multiSelectMenuItemStyle,
|
||||
buttonStyleData: _dropdownStyle.buttonStyle,
|
||||
iconStyleData: _dropdownStyle.iconStyle,
|
||||
dropdownStyleData: _dropdownStyle.dropdownStyle,
|
||||
labelText: lang.S.of(context).selectVariations,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.all(8),
|
||||
hintText: lang.S.of(context).selectItems,
|
||||
),
|
||||
values: selectedVariation,
|
||||
items: variationList.map((item) {
|
||||
return MultiSelectDropdownMenuItem(value: item.id, labelText: item.name ?? '');
|
||||
}).toList(),
|
||||
onChanged: (values) {
|
||||
setState(() {
|
||||
selectedVariation = values?.map((e) => e as int?).toList() ?? [];
|
||||
|
||||
selectedVariationValues.removeWhere((key, value) => !selectedVariation.contains(key));
|
||||
});
|
||||
|
||||
widget.onSelectVariation(values?.map((e) => e.toString()).toList() ?? []);
|
||||
if (selectedVariation.isEmpty) {
|
||||
setState(() => localVariantStocks.clear());
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
} else {
|
||||
generateVariants();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (e, stack) => Center(child: Text(e.toString())),
|
||||
loading: () => kLoader,
|
||||
),
|
||||
|
||||
//----------- Variation Values Selection ---------------
|
||||
if (selectedVariation.isNotEmpty) const SizedBox(height: 24),
|
||||
if (selectedVariation.isNotEmpty)
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: variationList.where((item) => selectedVariation.contains(item.id)).length,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, mainAxisSpacing: 8, crossAxisSpacing: 8, childAspectRatio: 2.8),
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems = variationList.where((item) => selectedVariation.contains(item.id)).toList();
|
||||
final varItem = filteredItems[index];
|
||||
return AcnooMultiSelectDropdown<String>(
|
||||
key: GlobalKey(debugLabel: varItem.name),
|
||||
labelText: varItem.name ?? '',
|
||||
values: selectedVariationValues[varItem.id] ?? [],
|
||||
items: (varItem.values ?? []).map((value) {
|
||||
return MultiSelectDropdownMenuItem(value: value, labelText: value);
|
||||
}).toList(),
|
||||
onChanged: (values) {
|
||||
selectedVariationValues[varItem.id?.toInt()] = values != null && values.isNotEmpty ? values : null;
|
||||
|
||||
generateVariants(changeState: false);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (selectedVariation.isEmpty) const SizedBox(height: 24),
|
||||
|
||||
// ================= GENERATED VARIANT LIST =================
|
||||
if (localVariantStocks.isNotEmpty) ...[
|
||||
// const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${lang.S.of(context).selectVariations} (${localVariantStocks.length})",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: localVariantStocks.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final stock = localVariantStocks[index];
|
||||
// Check if this is a Sub-Variation (contains '-')
|
||||
bool isSubVariation = stock.batchNo != null && stock.variantName!.contains('Sub');
|
||||
|
||||
return Container(
|
||||
color: isSubVariation ? Colors.grey.shade50 : Colors.transparent, // Light bg for sub items
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
showVariantEditSheet(
|
||||
context: context,
|
||||
stock: localVariantStocks[index],
|
||||
snapShot: widget.snapShot,
|
||||
tax: widget.tax,
|
||||
taxType: widget.taxType,
|
||||
onSave: (updatedStock) {
|
||||
setState(() {
|
||||
localVariantStocks[index] = updatedStock;
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
},
|
||||
);
|
||||
},
|
||||
contentPadding: !isSubVariation ? EdgeInsets.zero : EdgeInsetsDirectional.only(start: 30),
|
||||
// (+) Button only for Parent items
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
leading: !isSubVariation
|
||||
? IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
),
|
||||
icon: const Icon(Icons.add, color: kTitleColor),
|
||||
tooltip: lang.S.of(context).addSubVariation,
|
||||
onPressed: () => _addSubVariation(index),
|
||||
)
|
||||
: Icon(Icons.subdirectory_arrow_right,
|
||||
color: Colors.grey, size: 18), // Visual indicator for child
|
||||
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
stock.variantName ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text.rich(TextSpan(
|
||||
text: '${lang.S.of(context).stock}: ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: stock.productStock ?? 'n/a',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
color: kPeraColor),
|
||||
)
|
||||
])),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${lang.S.of(context).batchNo}: ${stock.batchNo ?? 'N/A'}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: _theme.textTheme.bodyMedium
|
||||
?.copyWith(fontSize: isSubVariation ? 13 : 14, color: kPeraColor),
|
||||
),
|
||||
),
|
||||
Text.rich(TextSpan(
|
||||
text: '${lang.S.of(context).sale}: ',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$currency${stock.productSalePrice ?? 'n/a'}',
|
||||
style: _theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: isSubVariation ? FontWeight.normal : FontWeight.w500,
|
||||
fontSize: isSubVariation ? 13 : 14,
|
||||
color: kTitleColor,
|
||||
),
|
||||
)
|
||||
])),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
showVariantEditSheet(
|
||||
context: context,
|
||||
stock: localVariantStocks[index],
|
||||
snapShot: widget.snapShot,
|
||||
tax: widget.tax,
|
||||
taxType: widget.taxType,
|
||||
onSave: (updatedStock) {
|
||||
setState(() {
|
||||
localVariantStocks[index] = updatedStock;
|
||||
});
|
||||
widget.onStocksUpdated(localVariantStocks);
|
||||
},
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
_removeVariation(index);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedPencilEdit02,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Show delete only if sub-variation
|
||||
if (isSubVariation)
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete03,
|
||||
color: kGreyTextColor,
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
lang.S.of(context).edit,
|
||||
style: _theme.textTheme.titleSmall?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showVariantEditSheet({
|
||||
required BuildContext context,
|
||||
required StockDataModel stock,
|
||||
required GetProductSettingModel snapShot,
|
||||
VatModel? tax,
|
||||
required String taxType,
|
||||
required Function(StockDataModel updatedStock) onSave,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
builder: (context) =>
|
||||
VariantEditSheet(stock: stock, snapShot: snapShot, tax: tax, taxType: taxType, onSave: onSave),
|
||||
);
|
||||
}
|
||||
|
||||
class VariantEditSheet extends ConsumerStatefulWidget {
|
||||
const VariantEditSheet(
|
||||
{super.key,
|
||||
required this.stock,
|
||||
required this.snapShot,
|
||||
required this.tax,
|
||||
required this.taxType,
|
||||
required this.onSave});
|
||||
final StockDataModel stock;
|
||||
final GetProductSettingModel snapShot;
|
||||
final VatModel? tax;
|
||||
final String taxType;
|
||||
final Function(StockDataModel) onSave;
|
||||
@override
|
||||
ConsumerState<VariantEditSheet> createState() => _VariantEditSheetState();
|
||||
}
|
||||
|
||||
class _VariantEditSheetState extends ConsumerState<VariantEditSheet> {
|
||||
late TextEditingController productBatchNumberController;
|
||||
late TextEditingController productStockController;
|
||||
late TextEditingController purchaseExclusivePriceController;
|
||||
late TextEditingController purchaseInclusivePriceController;
|
||||
late TextEditingController profitMarginController;
|
||||
late TextEditingController salePriceController;
|
||||
late TextEditingController wholeSalePriceController;
|
||||
late TextEditingController dealerPriceController;
|
||||
late TextEditingController expireDateController;
|
||||
late TextEditingController manufactureDateController;
|
||||
|
||||
String? selectedExpireDate;
|
||||
String? selectedManufactureDate;
|
||||
String? selectedWarehouseId; // Added variable for Warehouse
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
productBatchNumberController = TextEditingController(text: widget.stock.batchNo ?? '');
|
||||
productStockController = TextEditingController(text: widget.stock.productStock ?? '');
|
||||
purchaseExclusivePriceController = TextEditingController(text: widget.stock.exclusivePrice ?? '');
|
||||
purchaseInclusivePriceController = TextEditingController(text: widget.stock.inclusivePrice ?? '');
|
||||
profitMarginController = TextEditingController(text: widget.stock.profitPercent ?? '');
|
||||
salePriceController = TextEditingController(text: widget.stock.productSalePrice ?? '');
|
||||
wholeSalePriceController = TextEditingController(text: widget.stock.productWholeSalePrice ?? '');
|
||||
dealerPriceController = TextEditingController(text: widget.stock.productDealerPrice ?? '');
|
||||
selectedExpireDate = widget.stock.expireDate;
|
||||
selectedManufactureDate = widget.stock.mfgDate;
|
||||
|
||||
// Initialize Warehouse ID
|
||||
selectedWarehouseId = widget.stock.warehouseId;
|
||||
|
||||
expireDateController = TextEditingController(
|
||||
text: selectedExpireDate != null && selectedExpireDate!.isNotEmpty
|
||||
? DateFormat.yMd().format(DateTime.parse(selectedExpireDate!))
|
||||
: '');
|
||||
manufactureDateController = TextEditingController(
|
||||
text: selectedManufactureDate != null && selectedManufactureDate!.isNotEmpty
|
||||
? DateFormat.yMd().format(DateTime.parse(selectedManufactureDate!))
|
||||
: '');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
productBatchNumberController.dispose();
|
||||
productStockController.dispose();
|
||||
purchaseExclusivePriceController.dispose();
|
||||
purchaseInclusivePriceController.dispose();
|
||||
profitMarginController.dispose();
|
||||
salePriceController.dispose();
|
||||
wholeSalePriceController.dispose();
|
||||
dealerPriceController.dispose();
|
||||
expireDateController.dispose();
|
||||
manufactureDateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void calculatePurchaseAndMrp({String? from}) {
|
||||
num taxRate = widget.tax?.rate ?? 0;
|
||||
num purchaseExc = num.tryParse(purchaseExclusivePriceController.text) ?? 0;
|
||||
num purchaseInc = num.tryParse(purchaseInclusivePriceController.text) ?? 0;
|
||||
num profitMargin = num.tryParse(profitMarginController.text) ?? 0;
|
||||
num salePrice = num.tryParse(salePriceController.text) ?? 0;
|
||||
|
||||
if (from == 'purchase_inc') {
|
||||
purchaseExc = (taxRate != 0) ? purchaseInc / (1 + taxRate / 100) : purchaseInc;
|
||||
purchaseExclusivePriceController.text = purchaseExc.toStringAsFixed(2);
|
||||
} else {
|
||||
purchaseInc = purchaseExc + (purchaseExc * taxRate / 100);
|
||||
purchaseInclusivePriceController.text = purchaseInc.toStringAsFixed(2);
|
||||
}
|
||||
purchaseExc = num.tryParse(purchaseExclusivePriceController.text) ?? 0;
|
||||
purchaseInc = num.tryParse(purchaseInclusivePriceController.text) ?? 0;
|
||||
num basePrice = widget.taxType.toLowerCase() == 'exclusive' ? purchaseExc : purchaseInc;
|
||||
|
||||
if (from == 'mrp') {
|
||||
salePrice = num.tryParse(salePriceController.text) ?? 0;
|
||||
if (basePrice > 0) {
|
||||
profitMargin = ((salePrice - basePrice) / basePrice) * 100;
|
||||
profitMarginController.text = profitMargin.toStringAsFixed(2);
|
||||
}
|
||||
} else {
|
||||
if (basePrice > 0) {
|
||||
salePrice = basePrice + (basePrice * profitMargin / 100);
|
||||
salePriceController.text = salePrice.toStringAsFixed(2);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final permissionService = PermissionService(ref);
|
||||
final theme = Theme.of(context);
|
||||
final modules = widget.snapShot.data?.modules;
|
||||
|
||||
// 1. Fetch Warehouse List from Provider
|
||||
final warehouseData = ref.watch(fetchWarehouseListProvider);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration:
|
||||
const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
Flexible(
|
||||
child: Text('${lang.S.of(context).edit} ${widget.stock.variantName}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, fontSize: 18)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close, size: 20, color: Colors.grey))
|
||||
])),
|
||||
const Divider(height: 1, color: kBorderColor),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(children: [
|
||||
// 2. Display Warehouse Dropdown
|
||||
warehouseData.when(
|
||||
data: (data) => DropdownButtonFormField<String>(
|
||||
value: selectedWarehouseId,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).warehouse,
|
||||
hintText: lang.S.of(context).selectWarehouse,
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 12)),
|
||||
items: data.data
|
||||
?.map((WarehouseData w) =>
|
||||
DropdownMenuItem<String>(value: w.id.toString(), child: Text(w.name ?? '')))
|
||||
.toList(),
|
||||
onChanged: (v) => setState(() => selectedWarehouseId = v)),
|
||||
error: (e, s) => const Text('Failed to load warehouse'),
|
||||
loading: () => const Center(child: LinearProgressIndicator())),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (modules?.showBatchNo == '1' || modules?.showProductStock == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showBatchNo == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: productBatchNumberController,
|
||||
label: lang.S.of(context).batchNo,
|
||||
hint: "Ex: B-001")),
|
||||
if (modules?.showBatchNo == '1' && modules?.showProductStock == '1') const SizedBox(width: 12),
|
||||
if (modules?.showProductStock == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: productStockController,
|
||||
label: lang.S.of(context).stock,
|
||||
isNumber: true,
|
||||
hint: "Ex: 50"))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if ((modules?.showExclusivePrice == '1' || modules?.showInclusivePrice == '1') &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value)) ...[
|
||||
Row(children: [
|
||||
if (modules?.showExclusivePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: purchaseExclusivePriceController,
|
||||
label: lang.S.of(context).purchaseEx,
|
||||
isNumber: true,
|
||||
hint: "Ex: 100.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp())),
|
||||
if (modules?.showExclusivePrice == '1' && modules?.showInclusivePrice == '1')
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showInclusivePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: purchaseInclusivePriceController,
|
||||
label: lang.S.of(context).purchaseIn,
|
||||
isNumber: true,
|
||||
hint: "Ex: 115.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp(from: "purchase_inc")))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showProfitPercent == '1' || modules?.showProductSalePrice == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: profitMarginController,
|
||||
label: lang.S.of(context).profitMargin,
|
||||
isNumber: true,
|
||||
hint: "Ex: 20%",
|
||||
onChanged: (v) => calculatePurchaseAndMrp())),
|
||||
if (modules?.showProfitPercent == '1' &&
|
||||
modules?.showProductSalePrice == '1' &&
|
||||
permissionService.hasPermission(Permit.productsPriceView.value))
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showProductSalePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: salePriceController,
|
||||
label: lang.S.of(context).mrp,
|
||||
isNumber: true,
|
||||
hint: "Ex: 150.00",
|
||||
onChanged: (v) => calculatePurchaseAndMrp(from: 'mrp')))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showProductWholesalePrice == '1' || modules?.showProductDealerPrice == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showProductWholesalePrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: wholeSalePriceController,
|
||||
label: lang.S.of(context).wholeSalePrice,
|
||||
isNumber: true,
|
||||
hint: "Ex: 130.00")),
|
||||
if (modules?.showProductWholesalePrice == '1' && modules?.showProductDealerPrice == '1')
|
||||
const SizedBox(width: 12),
|
||||
if (modules?.showProductDealerPrice == '1')
|
||||
Expanded(
|
||||
child: _buildField(
|
||||
controller: dealerPriceController,
|
||||
label: lang.S.of(context).dealerPrice,
|
||||
isNumber: true,
|
||||
hint: "Ex: 120.00"))
|
||||
]),
|
||||
const SizedBox(height: 16)
|
||||
],
|
||||
if (modules?.showMfgDate == '1' || modules?.showExpireDate == '1') ...[
|
||||
Row(children: [
|
||||
if (modules?.showMfgDate == '1')
|
||||
Expanded(
|
||||
child: _buildDateField(
|
||||
controller: manufactureDateController,
|
||||
label: lang.S.of(context).manufactureDate,
|
||||
isExpire: false,
|
||||
hint: lang.S.of(context).selectDate)),
|
||||
if (modules?.showMfgDate == '1' && modules?.showExpireDate == '1') const SizedBox(width: 12),
|
||||
if (modules?.showExpireDate == '1')
|
||||
Expanded(
|
||||
child: _buildDateField(
|
||||
controller: expireDateController,
|
||||
label: lang.S.of(context).expDate,
|
||||
isExpire: true,
|
||||
hint: lang.S.of(context).selectDate,
|
||||
))
|
||||
]),
|
||||
const SizedBox(height: 24)
|
||||
],
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 3. Set the selected warehouse ID to the stock object
|
||||
widget.stock.warehouseId = selectedWarehouseId;
|
||||
|
||||
widget.stock.batchNo = productBatchNumberController.text;
|
||||
widget.stock.productStock = productStockController.text;
|
||||
widget.stock.exclusivePrice = purchaseExclusivePriceController.text;
|
||||
widget.stock.inclusivePrice = purchaseInclusivePriceController.text;
|
||||
widget.stock.profitPercent = profitMarginController.text;
|
||||
widget.stock.productSalePrice = salePriceController.text;
|
||||
widget.stock.productWholeSalePrice = wholeSalePriceController.text;
|
||||
widget.stock.productDealerPrice = dealerPriceController.text;
|
||||
widget.stock.expireDate = selectedExpireDate;
|
||||
widget.stock.mfgDate = selectedManufactureDate;
|
||||
widget.onSave(widget.stock);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(lang.S.of(context).saveVariant))),
|
||||
const SizedBox(height: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildField(
|
||||
{required TextEditingController controller,
|
||||
required String label,
|
||||
String? hint,
|
||||
bool isNumber = false,
|
||||
Function(String)? onChanged}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: isNumber ? TextInputType.number : TextInputType.text,
|
||||
inputFormatters: isNumber ? [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}'))] : [],
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12)));
|
||||
}
|
||||
|
||||
Widget _buildDateField(
|
||||
{required TextEditingController controller, required String label, String? hint, required bool isExpire}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
suffixIcon: const Icon(Icons.calendar_today, size: 18)),
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context, initialDate: DateTime.now(), firstDate: DateTime(2015, 8), lastDate: DateTime(2101));
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
controller.text = DateFormat.yMd().format(picked);
|
||||
if (isExpire) {
|
||||
selectedExpireDate = picked.toString();
|
||||
} else {
|
||||
selectedManufactureDate = picked.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:io';
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Provider/profile_provider.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/Screens/Products/bulk%20product%20upload/repo/bulk_upload_repo.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
|
||||
import '../../../GlobalComponents/glonal_popup.dart';
|
||||
|
||||
class BulkUploader extends StatefulWidget {
|
||||
const BulkUploader({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BulkUploader> createState() => _BulkUploaderState();
|
||||
}
|
||||
|
||||
class _BulkUploaderState extends State<BulkUploader> {
|
||||
File? file;
|
||||
|
||||
String getFileExtension(String fileName) {
|
||||
return fileName.split('/').last;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
return GlobalPopup(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_lang.excelUploader),
|
||||
),
|
||||
body: Consumer(builder: (context, ref, __) {
|
||||
final businessInfo = ref.watch(businessInfoProvider);
|
||||
return businessInfo.when(data: (details) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: file != null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
height: 40,
|
||||
width: 40,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
),
|
||||
child: const Image(image: AssetImage('images/excel.png'))),
|
||||
title: Text(
|
||||
getFileExtension(file?.path ?? ''),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
file = null;
|
||||
});
|
||||
},
|
||||
child: Text(_lang.remove)))),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: file == null,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: Image(
|
||||
height: 100,
|
||||
width: 100,
|
||||
image: AssetImage('images/file-upload.png'),
|
||||
)),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: const ButtonStyle(backgroundColor: WidgetStatePropertyAll(kMainColor)),
|
||||
onPressed: () async {
|
||||
if (file == null) {
|
||||
await pickAndUploadFile(ref: ref);
|
||||
} else {
|
||||
EasyLoading.show(status: _lang.uploading);
|
||||
await BulkUpLoadRepo().uploadBulkFile(file: file!, ref: ref, context: context);
|
||||
EasyLoading.dismiss();
|
||||
}
|
||||
},
|
||||
child: Text(file == null ? _lang.pickAndUploadFile : _lang.upload,
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await BulkUpLoadRepo().downloadFile(context);
|
||||
},
|
||||
child: Text(_lang.downloadExcelFormat),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
Future<void> pickAndUploadFile({required WidgetRef ref}) async {
|
||||
XTypeGroup typeGroup = XTypeGroup(
|
||||
label: lang.S.of(context).excelFiles,
|
||||
extensions: ['xlsx'],
|
||||
);
|
||||
final XFile? fileResult = await openFile(acceptedTypeGroups: [typeGroup]);
|
||||
|
||||
if (fileResult != null) {
|
||||
final File files = File(fileResult.path);
|
||||
setState(() {
|
||||
file = files;
|
||||
});
|
||||
} else {
|
||||
print(lang.S.of(context).noFileSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mobile_pos/Provider/product_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product_brand/product_brand_provider/product_brand_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product_category/provider/product_category_provider/product_unit_provider.dart';
|
||||
import 'package:mobile_pos/Screens/product_unit/provider/product_unit_provider.dart';
|
||||
|
||||
import '../../../../Const/api_config.dart';
|
||||
import '../../../../Repository/constant_functions.dart';
|
||||
import '../../../../http_client/custome_http_client.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class BulkUpLoadRepo {
|
||||
Future<void> uploadBulkFile({
|
||||
required WidgetRef ref,
|
||||
required BuildContext context,
|
||||
required File file,
|
||||
}) async {
|
||||
CustomHttpClient customHttpClient = CustomHttpClient(client: http.Client(), context: context, ref: ref);
|
||||
final uri = Uri.parse('${APIConfig.url}/bulk-uploads');
|
||||
|
||||
var request = http.MultipartRequest('POST', uri)
|
||||
..headers['Accept'] = 'application/json'
|
||||
..headers['Authorization'] = await getAuthToken();
|
||||
|
||||
request.files.add(http.MultipartFile.fromBytes('file', file.readAsBytesSync(), filename: file.path));
|
||||
|
||||
final response = await customHttpClient.uploadFile(url: uri, fileFieldName: 'file', file: file, fields: request.fields);
|
||||
final responseData = await response.stream.bytesToString();
|
||||
final parsedData = jsonDecode(responseData);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Added successful!')));
|
||||
ref.refresh(productProvider);
|
||||
ref.refresh(categoryProvider);
|
||||
ref.refresh(brandsProvider);
|
||||
ref.refresh(unitsProvider);
|
||||
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed: ${parsedData['message']}')));
|
||||
}
|
||||
}
|
||||
|
||||
final String fileUrl = '${APIConfig.domain}assets/POSpro_bulk_product_upload.xlsx';
|
||||
|
||||
Future<void> downloadFile(BuildContext context) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse(fileUrl));
|
||||
if (response.statusCode != 200) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to download file!')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final downloadPath = '/storage/emulated/0/Download';
|
||||
final file = File('$downloadPath/POSpro_bulk_product_upload.xlsx');
|
||||
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('File saved to: ${file.path}')),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Download error: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
985
lib/Screens/Products/product_details.dart
Normal file
985
lib/Screens/Products/product_details.dart
Normal file
@@ -0,0 +1,985 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hugeicons/hugeicons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Screens/Products/Model/product_model.dart';
|
||||
import 'package:mobile_pos/Screens/Products/add%20product/modle/create_product_model.dart';
|
||||
import 'package:mobile_pos/constant.dart';
|
||||
import 'package:mobile_pos/currency.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
|
||||
import '../../GlobalComponents/glonal_popup.dart';
|
||||
import '../../Provider/product_provider.dart';
|
||||
import '../../service/check_actions_when_no_branch.dart';
|
||||
import '../../service/check_user_role_permission_provider.dart';
|
||||
import '../../widgets/empty_widget/_empty_widget.dart';
|
||||
import '../../widgets/key_values/key_values_widget.dart';
|
||||
import '../Purchase/Repo/purchase_repo.dart';
|
||||
import '../Purchase/purchase_product_buttom_sheet.dart';
|
||||
import '../hrm/widgets/deleteing_alart_dialog.dart';
|
||||
import 'Repo/product_repo.dart';
|
||||
import 'add product/add_edit_comboItem.dart';
|
||||
import 'add product/add_product.dart';
|
||||
import 'add product/combo_product_form.dart';
|
||||
|
||||
class ProductDetails extends ConsumerStatefulWidget {
|
||||
const ProductDetails({
|
||||
super.key,
|
||||
required this.details,
|
||||
});
|
||||
|
||||
final Product details;
|
||||
|
||||
@override
|
||||
ConsumerState<ProductDetails> createState() => _ProductDetailsState();
|
||||
}
|
||||
|
||||
class _ProductDetailsState extends ConsumerState<ProductDetails> {
|
||||
TextEditingController productStockController = TextEditingController();
|
||||
TextEditingController salePriceController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final providerData = ref.watch(fetchProductDetails(widget.details.id.toString()));
|
||||
final permissionService = PermissionService(ref);
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
return GlobalPopup(
|
||||
child: providerData.when(data: (snapshot) {
|
||||
return Scaffold(
|
||||
backgroundColor: kWhite,
|
||||
appBar: AppBar(
|
||||
backgroundColor: kWhite,
|
||||
surfaceTintColor: kWhite,
|
||||
title: Text(
|
||||
lang.S.of(context).productDetails,
|
||||
//'Product Details',
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () async {
|
||||
bool result = await checkActionWhenNoBranch(ref: ref, context: context);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AddProduct(
|
||||
productModel: snapshot,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Colors.green,
|
||||
size: 22,
|
||||
)),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: () async {
|
||||
bool confirmDelete = await showDeleteConfirmationDialog(context: context, itemName: 'product');
|
||||
if (confirmDelete) {
|
||||
EasyLoading.show(
|
||||
status: lang.S.of(context).deleting,
|
||||
);
|
||||
ProductRepo productRepo = ProductRepo();
|
||||
await productRepo.deleteProduct(id: snapshot.id.toString(), context: context, ref: ref);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
icon: HugeIcon(
|
||||
icon: HugeIcons.strokeRoundedDelete02,
|
||||
color: kMainColor,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
],
|
||||
centerTitle: true,
|
||||
// iconTheme: const IconThemeData(color: Colors.white),
|
||||
elevation: 0.0,
|
||||
),
|
||||
body: Container(
|
||||
alignment: Alignment.topCenter,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(topRight: Radius.circular(30), topLeft: Radius.circular(30))),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (permissionService.hasPermission(Permit.productsRead.value)) ...{
|
||||
Container(
|
||||
height: 256,
|
||||
padding: EdgeInsets.all(8),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Color(0xffF5F3F3),
|
||||
),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF5F3F3),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
image: snapshot.productPicture == null
|
||||
? DecorationImage(fit: BoxFit.cover, image: AssetImage(noProductImageUrl))
|
||||
: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage('${APIConfig.domain}${snapshot.productPicture}'))),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
snapshot.productName.toString(),
|
||||
//'Smart watch',
|
||||
style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600, fontSize: 18),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
snapshot.category?.categoryName.toString() ?? 'n/a',
|
||||
//'Apple Watch',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kGreyTextColor,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: const Color(0xffFEF0F1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
//Single product details-------------------------
|
||||
if (snapshot.productType == 'single')
|
||||
...{
|
||||
_lang.skuOrCode: snapshot.productCode ?? 'n/a',
|
||||
_lang.brand: snapshot.brand?.brandName ?? 'n/a',
|
||||
_lang.model: snapshot.productModel?.name ?? 'n/a',
|
||||
_lang.units: snapshot.unit?.unitName ?? 'n/a',
|
||||
_lang.rack: snapshot.rack?.name ?? 'n/a',
|
||||
_lang.shelf: snapshot.shelf?.name ?? 'n/a',
|
||||
// 'Test': snapshot.shelfId ?? 'n/a',
|
||||
_lang.stock: snapshot.stocksSumProductStock?.toString() ?? '0',
|
||||
_lang.lowStockAlert: snapshot.alertQty?.toString() ?? 'n/a',
|
||||
_lang.warehouse: snapshot.stocks?.first.warehouse?.name?.toString() ?? 'n/a',
|
||||
_lang.taxType: snapshot.vatType ?? 'n/a',
|
||||
_lang.tax: snapshot.vatAmount?.toString() ?? 'n/a',
|
||||
_lang.costExclusionTax: (snapshot.vatType != 'exclusive')
|
||||
? (snapshot.stocks != null &&
|
||||
snapshot.stocks!.isNotEmpty &&
|
||||
snapshot.stocks!.first.productPurchasePrice != null &&
|
||||
snapshot.vatAmount != null
|
||||
? '${snapshot.stocks!.first.productPurchasePrice! - snapshot.vatAmount!}'
|
||||
: '0')
|
||||
: ('$currency${snapshot.stocks?.isNotEmpty == true ? snapshot.stocks!.first.productPurchasePrice ?? '0' : '0'}'),
|
||||
_lang.costInclusionTax: (snapshot.vatType == 'exclusive')
|
||||
? (snapshot.stocks != null &&
|
||||
snapshot.stocks!.isNotEmpty &&
|
||||
snapshot.stocks!.first.productPurchasePrice != null &&
|
||||
snapshot.vatAmount != null
|
||||
? '$currency${snapshot.stocks!.first.productPurchasePrice! + snapshot.vatAmount!}'
|
||||
: '0')
|
||||
: ('$currency${snapshot.stocks?.isNotEmpty == true ? snapshot.stocks!.first.productPurchasePrice ?? '0' : '0'}'),
|
||||
'${_lang.profitMargin} (%)': (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.profitPercent != null
|
||||
? snapshot.stocks!.first.profitPercent.toString()
|
||||
: '0'),
|
||||
_lang.mrpOrSalePrice: (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.productSalePrice != null
|
||||
? '$currency${snapshot.stocks!.first.productSalePrice}'
|
||||
: '0'),
|
||||
_lang.wholeSalePrice: (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.productWholeSalePrice != null
|
||||
? '$currency${snapshot.stocks!.first.productWholeSalePrice}'
|
||||
: '0'),
|
||||
_lang.dealerPrice: (snapshot.stocks?.isNotEmpty == true &&
|
||||
snapshot.stocks!.first.productDealerPrice != null
|
||||
? '$currency${snapshot.stocks?.first.productDealerPrice}'
|
||||
: '0'),
|
||||
_lang.manufactureDate:
|
||||
(snapshot.stocks?.isNotEmpty == true && snapshot.stocks!.first.mfgDate != null)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.parse(snapshot.stocks!.first.mfgDate!))
|
||||
: 'n/a',
|
||||
_lang.expiredDate:
|
||||
(snapshot.stocks?.isNotEmpty == true && snapshot.stocks!.first.expireDate != null)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.parse(snapshot.stocks?.first.expireDate ?? ''))
|
||||
: 'n/a',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.warrantyDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.warrantyUnit?.toString() ?? 'n/a'}',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.guaranteeDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.guaranteeUnit?.toString() ?? 'n/a'}',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
),
|
||||
//---------------variant product----------------
|
||||
if (snapshot.productType == 'variant')
|
||||
...{
|
||||
_lang.skuOrCode: snapshot.productCode ?? 'n/a',
|
||||
_lang.brand: snapshot.brand?.brandName ?? 'n/a',
|
||||
_lang.model: snapshot.productModel?.name ?? 'n/a',
|
||||
_lang.rack: snapshot.shelf?.name ?? 'n/a',
|
||||
_lang.lowStockAlert: snapshot.alertQty?.toString() ?? 'n/a',
|
||||
_lang.taxReport: snapshot.vatType ?? 'n/a',
|
||||
_lang.tax: snapshot.vatAmount?.toString() ?? 'n/a',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.warrantyDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.warrantyUnit?.toString() ?? 'n/a'}',
|
||||
_lang.guarantee:
|
||||
'${snapshot.warrantyGuaranteeInfo?.guaranteeDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.guaranteeUnit?.toString() ?? 'n/a'}',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
),
|
||||
//---------------Combo product----------------
|
||||
if (snapshot.productType == 'combo')
|
||||
...{
|
||||
_lang.skuOrCode: snapshot.productCode ?? 'n/a',
|
||||
_lang.brand: snapshot.brand?.brandName ?? 'n/a',
|
||||
_lang.model: snapshot.productModel?.name ?? 'n/a',
|
||||
_lang.units: snapshot.unit?.unitName ?? 'n/a',
|
||||
_lang.rack: snapshot.rack?.name ?? 'n/a',
|
||||
_lang.shelf: snapshot.shelf?.name ?? 'n/a',
|
||||
_lang.lowStockAlert: snapshot.alertQty?.toString() ?? 'n/a',
|
||||
_lang.type: snapshot.productType ?? 'n/a',
|
||||
_lang.taxType: snapshot.vatType ?? 'n/a',
|
||||
_lang.tax: snapshot.vatAmount?.toString() ?? 'n/a',
|
||||
_lang.netTotalAmount:
|
||||
(snapshot.productSalePrice != null && snapshot.profitPercent != null)
|
||||
? (snapshot.productSalePrice! / (1 + (snapshot.profitPercent! / 100)))
|
||||
.toStringAsFixed(2)
|
||||
: 'n/a',
|
||||
'${_lang.profitMargin} (%)': '${snapshot.profitPercent ?? 0}%',
|
||||
_lang.sellingPrice: '$currency${snapshot.productSalePrice ?? 0}',
|
||||
_lang.warranty:
|
||||
'${snapshot.warrantyGuaranteeInfo?.warrantyDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.warrantyUnit?.toString() ?? 'n/a'}',
|
||||
_lang.guarantee:
|
||||
'${snapshot.warrantyGuaranteeInfo?.guaranteeDuration?.toString() ?? ''} ${snapshot.warrantyGuaranteeInfo?.guaranteeUnit?.toString() ?? 'n/a'}',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//--------------variant product details---------------------------------
|
||||
if (snapshot.productType == 'variant') ...[
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16, end: 16, top: 16, bottom: 6),
|
||||
child: Text(
|
||||
_lang.variationsProduct,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.separated(
|
||||
// padding: EdgeInsetsGeometry.symmetric(vertical: 10, horizontal: 16),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: snapshot.stocks?.length ?? 0,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
thickness: 0.3,
|
||||
color: kBorderColorTextField,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
snapshot.stocks?[index].variantName ?? 'n/a',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
// fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${_lang.sale}: $currency${snapshot.stocks?[index].productSalePrice ?? '0'}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${_lang.batch}: ${snapshot.stocks?[index].batchNo ?? 'N/A'}',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
// fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: '${_lang.stock}: ',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kNeutralColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: snapshot.stocks?[index].productStock.toString() ?? '0',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Color(0xff34C759),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
iconColor: kPeraColor,
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'view':
|
||||
viewModal(context, snapshot, index);
|
||||
break;
|
||||
case 'edit':
|
||||
final stock = snapshot.stocks?[index];
|
||||
|
||||
final cartProduct = CartProductModelPurchase(
|
||||
productId: snapshot.id ?? 0,
|
||||
variantName: stock?.variantName,
|
||||
brandName: snapshot.brand?.brandName,
|
||||
productName: snapshot.productName ?? '',
|
||||
productDealerPrice: stock?.productDealerPrice,
|
||||
productPurchasePrice: stock?.productPurchasePrice,
|
||||
productSalePrice: stock?.productSalePrice,
|
||||
productWholeSalePrice: stock?.productWholeSalePrice,
|
||||
quantities: stock?.productStock,
|
||||
productType: snapshot.productType ?? '',
|
||||
vatAmount: snapshot.vatAmount ?? 0,
|
||||
vatRate: snapshot.vat?.rate ?? 0,
|
||||
vatType: snapshot.vatType ?? 'exclusive',
|
||||
expireDate: stock?.expireDate,
|
||||
mfgDate: stock?.mfgDate,
|
||||
profitPercent: stock?.profitPercent ?? 0,
|
||||
stock: stock?.productStock,
|
||||
batchNumber: stock?.batchNo ?? '',
|
||||
);
|
||||
addProductInPurchaseCartButtomSheet(
|
||||
context: context,
|
||||
product: cartProduct,
|
||||
ref: ref,
|
||||
fromUpdate: false,
|
||||
index: index,
|
||||
fromStock: true,
|
||||
stocks: []);
|
||||
break;
|
||||
case 'add_stock':
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
productStockController.text = '1';
|
||||
salePriceController.text =
|
||||
snapshot.stocks?[index].productSalePrice?.toString() ?? '0.0';
|
||||
addStockPopUp(context, _formKey, theme, snapshot, index);
|
||||
break;
|
||||
case 'delete':
|
||||
showEditDeletePopUp(
|
||||
context: context,
|
||||
data: snapshot.stocks?[index],
|
||||
ref: ref,
|
||||
productId: widget.details.id.toString());
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(value: 'view', child: Text(_lang.view)),
|
||||
PopupMenuItem(value: 'edit', child: Text(_lang.edit)),
|
||||
PopupMenuItem(value: 'add_stock', child: Text(_lang.addStock)),
|
||||
PopupMenuItem(value: 'delete', child: Text(_lang.delete)),
|
||||
],
|
||||
),
|
||||
),
|
||||
visualDensity: VisualDensity(vertical: -4, horizontal: -4),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
//--------------Combo product details---------------------------------
|
||||
if (snapshot.productType == 'combo') ...[
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16, end: 16, top: 16, bottom: 6),
|
||||
child: Text(
|
||||
_lang.comboProducts,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.separated(
|
||||
// padding: EdgeInsetsGeometry.symmetric(vertical: 10, horizontal: 16),
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: snapshot.comboProducts?.length ?? 0,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
thickness: 0.3,
|
||||
color: kBorderColorTextField,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final combo = snapshot.comboProducts![index];
|
||||
return ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${combo.stock?.product?.productName ?? 'n/a'} ${combo.stock?.variantName ?? ''}',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${_lang.qty}: ${combo.quantity ?? '0'}',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_lang.code}: ${combo.stock?.product?.productCode ?? 'n/a'}, ${_lang.batchNo}: ${snapshot.comboProducts?[index].stock?.batchNo ?? 'n/a'}',
|
||||
maxLines: 1,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: kPeraColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$currency${combo.stock?.productSalePrice ?? 0}',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// trailing: SizedBox(
|
||||
// width: 30,
|
||||
// child: PopupMenuButton<String>(
|
||||
// iconColor: kPeraColor,
|
||||
// onSelected: (value) async {
|
||||
// switch (value) {
|
||||
// case 'edit':
|
||||
// // Convert ComboProductComponent → ComboItem
|
||||
// final comboItem = ComboItem(
|
||||
// product: combo.product!,
|
||||
// stockData: combo.stock!,
|
||||
// quantity: combo.quantity ?? combo.stock?.productStock ?? 1,
|
||||
// manualPurchasePrice: combo.purchasePrice?.toDouble(),
|
||||
// );
|
||||
//
|
||||
// // Navigate to edit page
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => AddOrEditComboItem(
|
||||
// existingItem: comboItem,
|
||||
// onSubmit: (updatedItem) {
|
||||
// setState(() {
|
||||
// // Convert ComboItem → ComboProductComponent after edit
|
||||
// snapshot.comboProducts![index] = ComboProductComponent(
|
||||
// id: combo.id,
|
||||
// productId: updatedItem.product.id,
|
||||
// stockId: updatedItem.stockData.id,
|
||||
// purchasePrice: updatedItem.manualPurchasePrice,
|
||||
// quantity: updatedItem.quantity,
|
||||
// stock: updatedItem.stockData,
|
||||
// product: updatedItem.product,
|
||||
// );
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// break;
|
||||
//
|
||||
// case 'delete':
|
||||
// final confirmDelete = await showDialog<bool>(
|
||||
// context: context,
|
||||
// builder: (context) => AlertDialog(
|
||||
// title: const Text('Delete Combo Product'),
|
||||
// content: const Text('Are you sure you want to delete this item?'),
|
||||
// actions: [
|
||||
// TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
|
||||
// TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// if (confirmDelete == true) {
|
||||
// setState(() {
|
||||
// snapshot.comboProducts!.removeAt(index);
|
||||
// });
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// itemBuilder: (BuildContext context) => [
|
||||
// const PopupMenuItem(value: 'edit', child: Text('Edit')),
|
||||
// const PopupMenuItem(
|
||||
// value: 'delete',
|
||||
// child: Text('Delete', style: TextStyle(color: Colors.red)),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
visualDensity: VisualDensity(vertical: -4, horizontal: -4),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
} else
|
||||
Center(child: PermitDenyWidget()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}, error: (e, stack) {
|
||||
return Text(e.toString());
|
||||
}, loading: () {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}));
|
||||
}
|
||||
|
||||
// Add stock popup
|
||||
Future<dynamic> addStockPopUp(
|
||||
BuildContext context, GlobalKey<FormState> _formKey, ThemeData theme, Product snapshot, int index) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
insetPadding: EdgeInsets.symmetric(horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).addStock,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: kTitleColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: kTitleColor, size: 16),
|
||||
iconSize: 16,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 30,
|
||||
minHeight: 30,
|
||||
),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(Color(0xffEEF3FF)),
|
||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(
|
||||
thickness: 0.3,
|
||||
color: kBorderColorTextField,
|
||||
height: 0,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
textAlign: TextAlign.center,
|
||||
controller: productStockController,
|
||||
validator: (value) {
|
||||
final int? enteredStock = int.tryParse(value ?? '');
|
||||
if (enteredStock == null || enteredStock < 1) {
|
||||
return lang.S.of(context).stockWarn;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: lang.S.of(context).enterStock,
|
||||
prefixIcon: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
height: 26,
|
||||
width: 26,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color(0xffE0E2E7),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
int quantity = int.tryParse(productStockController.text) ?? 1;
|
||||
if (quantity > 1) {
|
||||
quantity--;
|
||||
productStockController.text = quantity.toString();
|
||||
}
|
||||
},
|
||||
child: Icon(Icons.remove, color: Color(0xff4A4A52)),
|
||||
),
|
||||
),
|
||||
suffixIcon: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
height: 26,
|
||||
width: 26,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: kMainColor.withOpacity(0.15),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
onTap: () {
|
||||
int quantity = int.tryParse(productStockController.text) ?? 1;
|
||||
quantity++;
|
||||
productStockController.text = quantity.toString();
|
||||
},
|
||||
child: Icon(Icons.add, color: theme.colorScheme.primary),
|
||||
),
|
||||
),
|
||||
border: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xffE0E2E7))),
|
||||
enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xffE0E2E7))),
|
||||
focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Color(0xffE0E2E7))),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 0, vertical: 8),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
TextFormField(
|
||||
readOnly: true,
|
||||
controller: salePriceController,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: lang.S.of(context).salePrice,
|
||||
hintText: lang.S.of(context).enterAmount,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
side: BorderSide(color: Color(0xffF68A3D)),
|
||||
),
|
||||
child: Text(lang.S.of(context).cancel, style: TextStyle(color: Color(0xffF68A3D))),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
child: Text(lang.S.of(context).save),
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final int newStock = int.tryParse(productStockController.text) ?? 0;
|
||||
|
||||
try {
|
||||
EasyLoading.show(status: lang.S.of(context).updating);
|
||||
|
||||
final repo = ProductRepo();
|
||||
final String productId = snapshot.stocks?[index].id.toString() ?? '';
|
||||
|
||||
final bool success = await repo.addStock(
|
||||
id: productId,
|
||||
qty: newStock.toString(),
|
||||
);
|
||||
|
||||
EasyLoading.dismiss();
|
||||
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).updateSuccess)),
|
||||
);
|
||||
|
||||
ref.refresh(fetchProductDetails(widget.details.id.toString()));
|
||||
ref.refresh(productProvider);
|
||||
|
||||
productStockController.clear();
|
||||
salePriceController.clear();
|
||||
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(lang.S.of(context).updateFailed)),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
EasyLoading.dismiss();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: ${e.toString()}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// view modal sheet
|
||||
Future<dynamic> viewModal(BuildContext context, Product snapshot, int index) {
|
||||
final _lang = lang.S.of(context);
|
||||
return showModalBottomSheet(
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setNewState) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).view,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(Icons.close, size: 18),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(color: kBorderColor, height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (snapshot.stocks != null && snapshot.stocks!.isNotEmpty && index < snapshot.stocks!.length)
|
||||
...{
|
||||
_lang.batchNo: snapshot.stocks![index].batchNo ?? 'n/a',
|
||||
_lang.qty: snapshot.stocks![index].productStock?.toString() ?? '0',
|
||||
_lang.costExclusionTax: snapshot.vatType != 'exclusive'
|
||||
? (snapshot.stocks![index].productPurchasePrice != null && snapshot.vatAmount != null
|
||||
? '${snapshot.stocks![index].productPurchasePrice! - snapshot.vatAmount!}'
|
||||
: 'n/a')
|
||||
: (snapshot.stocks![index].productPurchasePrice?.toString() ?? 'n/a'),
|
||||
_lang.costInclusionTax: snapshot.vatType == 'exclusive'
|
||||
? (snapshot.stocks![index].productPurchasePrice != null && snapshot.vatAmount != null
|
||||
? '${snapshot.stocks![index].productPurchasePrice! + snapshot.vatAmount!}'
|
||||
: 'n/a')
|
||||
: (snapshot.stocks![index].productPurchasePrice?.toString() ?? 'n/a'),
|
||||
'${_lang.profitMargin} (%)': snapshot.stocks![index].profitPercent?.toString() ?? 'n/a',
|
||||
_lang.salePrice: snapshot.stocks![index].productSalePrice?.toString() ?? 'n/a',
|
||||
_lang.wholeSalePrice: snapshot.stocks![index].productWholeSalePrice?.toString() ?? 'n/a',
|
||||
_lang.dealerPrice: snapshot.stocks![index].productDealerPrice?.toString() ?? 'n/a',
|
||||
_lang.manufactureDate:
|
||||
(snapshot.stocks![index].mfgDate != null && snapshot.stocks![index].mfgDate!.isNotEmpty)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.tryParse(snapshot.stocks![index].mfgDate!) ?? DateTime(0))
|
||||
: 'n/a',
|
||||
_lang.expiredDate: (snapshot.stocks![index].expireDate != null &&
|
||||
snapshot.stocks![index].expireDate!.isNotEmpty)
|
||||
? DateFormat('d MMMM yyyy')
|
||||
.format(DateTime.tryParse(snapshot.stocks![index].expireDate!) ?? DateTime(0))
|
||||
: 'n/a',
|
||||
}.entries.map(
|
||||
(entry) => KeyValueRow(
|
||||
title: entry.key,
|
||||
titleFlex: 6,
|
||||
description: entry.value.toString(),
|
||||
descriptionFlex: 8,
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(_lang.noStockAvailable),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showEditDeletePopUp(
|
||||
{required BuildContext context, Stock? data, required WidgetRef ref, required String productId}) async {
|
||||
final _theme = Theme.of(context);
|
||||
return await showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
lang.S.of(context).deleteBatchWarn,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 26),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Color(0xffF68A3D).withValues(alpha: 0.1),
|
||||
),
|
||||
padding: EdgeInsets.all(20),
|
||||
child: SvgPicture.asset(
|
||||
height: 146,
|
||||
width: 146,
|
||||
'images/trash.svg',
|
||||
),
|
||||
),
|
||||
SizedBox(height: 26),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(lang.S.of(context).cancel),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
await Future.delayed(Duration.zero);
|
||||
ProductRepo repo = ProductRepo();
|
||||
bool success;
|
||||
success = await repo.deleteStock(
|
||||
id: data?.id.toString() ?? '',
|
||||
);
|
||||
if (success) {
|
||||
ref.refresh(fetchProductDetails(productId));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(lang.S.of(context).deletedSuccessFully)));
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(lang.S.of(context).delete),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user