first commit

This commit is contained in:
2026-02-07 15:57:09 +07:00
commit 157096f164
1153 changed files with 415766 additions and 0 deletions

View File

@@ -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 {}
}
}

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

View 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,
// ),
// ),
// ),
// ],
// )
],
),
),
),
);
}
}

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

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

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

View File

@@ -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;
}
}

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

View 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,
//'Dont 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(),
),
);
}),
],
),
),
),
),
);
},
),
);
}
}

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

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

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

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

View File

@@ -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 !',
),
],
),
),
),
),
);
},
);
}

View File

@@ -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',
),
],
),
),
),
),
),
);
}
}

View File

@@ -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;
}
}

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

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

View File

@@ -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;
}
}

View 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',
),
],
),
),
),
),
),
);
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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)),
// ],
// ),
// ),
// );
// }),
// ),
// );
// });
// }
// }

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

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

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

View File

@@ -0,0 +1,7 @@
class ChartData {
ChartData(this.x, this.y, this.y1);
final String x;
final double y;
final double y1;
}

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

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

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

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

View File

@@ -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;
}
}

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

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

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

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

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

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

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

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

View File

@@ -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());

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

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

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

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

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

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

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

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

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

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

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

View 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}';
}
}

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

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

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

View File

@@ -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());

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

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

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

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

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

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

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

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

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

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

View 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
View 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,
// );
// },
// ),
// ],
// ),
// ),
],
),
);
}
}

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

View File

@@ -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);
}
}
}

View File

@@ -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')),
);
}
}
}

View 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