Compare commits

...

2 Commits

Author SHA1 Message Date
mohamadmahdi jebeli 205a58359c fixed reserve list 2025-08-10 16:52:13 +03:30
mohamadmahdi jebeli 585cb91df5 add scanner 2025-07-31 10:25:16 +03:30
27 changed files with 798 additions and 149 deletions

View File

@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:label="business_panel" android:label="business_panel"

View File

@ -1,5 +1,3 @@
// lib/core/config/api_config.dart
class ApiConfig { class ApiConfig {
// Private constructor to prevent instantiation // Private constructor to prevent instantiation
ApiConfig._(); ApiConfig._();
@ -50,4 +48,17 @@ class ApiConfig {
/// Headers: {'Authorization': 'Bearer <token>'} /// Headers: {'Authorization': 'Bearer <token>'}
static String editDiscount(String id) => '$baseUrl/discount/edit/$id'; static String editDiscount(String id) => '$baseUrl/discount/edit/$id';
static String deleteDiscount(String id) => '$baseUrl/discount/delete/$id'; static String deleteDiscount(String id) => '$baseUrl/discount/delete/$id';
// ========== Order Endpoints ==========
/// Endpoint to add a new order.
/// Method: POST
/// Body: {'Discount': discountId, 'User': userId}
/// Headers: {'Authorization': 'Bearer <token>'}
static const String addOrder = '$baseUrl/order/add';
// ========== Reservation Endpoints ==========
/// Endpoint to get reservations.
/// Method: GET
/// Headers: {'Authorization': 'Bearer <token>'}
static const String getReservations = '$baseUrl/reservation/get';
} }

View File

@ -0,0 +1,76 @@
class ReservationEntity {
final String id;
final String shop;
final ReservationDiscount discount;
final String user;
final bool status;
ReservationEntity({
required this.id,
required this.shop,
required this.discount,
required this.user,
required this.status,
});
factory ReservationEntity.fromJson(Map<String, dynamic> json) {
return ReservationEntity(
id: json['ID'] ?? '',
shop: json['Shop'] ?? '',
discount: ReservationDiscount.fromJson(json['Discount'] ?? {}),
user: json['User'] ?? '',
status: json['Status'] ?? false,
);
}
}
class ReservationDiscount {
final String id;
final String name;
final String typeName;
final DateTime? endDate;
final List<ReservationImage> images;
ReservationDiscount({
required this.id,
required this.name,
this.endDate,
required this.images,
required this.typeName,
});
factory ReservationDiscount.fromJson(Map<String, dynamic> json) {
List<ReservationImage> _parseImages(dynamic imageList) {
if (imageList is List) {
return imageList.map((img) => ReservationImage.fromJson(img)).toList();
}
return [];
}
return ReservationDiscount(
id: json['ID'] ?? '',
name: json['Name'] ?? 'بدون نام',
endDate: json['EndDate'] != null ? DateTime.tryParse(json['EndDate']) : null,
images: _parseImages(json['Images']),
typeName: json['Type']?['Name'] ?? 'نامشخص',
);
}
}
class ReservationImage {
final String id;
final String url;
ReservationImage({
required this.id,
required this.url,
});
factory ReservationImage.fromJson(Map<String, dynamic> json) {
return ReservationImage(
id: json['_id'] ?? '',
url: json['Url'] ?? '',
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/core/config/app_colors.dart';
import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart'; import 'package:business_panel/presentation/auth/bloc/auth_bloc.dart';
import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; import 'package:business_panel/presentation/home/bloc/home_bloc.dart';
import 'package:business_panel/presentation/order/bloc/order_bloc.dart';
import 'package:business_panel/presentation/pages/splash_page.dart'; import 'package:business_panel/presentation/pages/splash_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -31,6 +32,7 @@ class MyApp extends StatelessWidget {
BlocProvider( BlocProvider(
create: (context) => HomeBloc()..add(FetchDiscounts()), create: (context) => HomeBloc()..add(FetchDiscounts()),
), ),
BlocProvider(create: (context) => OrderBloc()),
], ],
child: MaterialApp( child: MaterialApp(
title: 'Proxibuy', title: 'Proxibuy',

View File

@ -1,4 +1,3 @@
// lib/presentation/discount/bloc/discount_bloc.dart
import 'dart:developer'; import 'dart:developer';
import 'package:business_panel/core/config/api_config.dart'; import 'package:business_panel/core/config/api_config.dart';
@ -6,10 +5,8 @@ import 'package:business_panel/core/services/token_storage_service.dart';
import 'package:business_panel/core/utils/logging_interceptor.dart'; import 'package:business_panel/core/utils/logging_interceptor.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'discount_event.dart'; import 'discount_event.dart';
import 'discount_state.dart'; import 'discount_state.dart';
import 'dart:convert'; // Import for jsonEncode
class DiscountBloc extends Bloc<DiscountEvent, DiscountState> { class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
final Dio _dio = Dio(); final Dio _dio = Dio();
@ -156,6 +153,10 @@ class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
discountId: event.discountId, discountId: event.discountId,
); );
}); });
on<ClearErrorMessage>((event, emit) {
emit(state.copyWith(clearErrorMessage: true));
});
} }
Future<void> _submitDiscountForm( Future<void> _submitDiscountForm(
@ -224,7 +225,7 @@ class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
); );
emit(state.copyWith(isSubmitting: false, isSuccess: true)); emit(state.copyWith(isSubmitting: false, isSuccess: true));
} on DioException catch (e) { }on DioException catch (e) {
emit( emit(
state.copyWith( state.copyWith(
isSubmitting: false, isSubmitting: false,

View File

@ -58,8 +58,9 @@ class NotificationRadiusChanged extends DiscountEvent {
class SubmitDiscount extends DiscountEvent {} class SubmitDiscount extends DiscountEvent {}
// Event for updating an existing discount
class UpdateDiscount extends DiscountEvent { class UpdateDiscount extends DiscountEvent {
final String discountId; final String discountId;
UpdateDiscount(this.discountId); UpdateDiscount(this.discountId);
} }
class ClearErrorMessage extends DiscountEvent {} // <-- این رویداد جدید را اضافه کنید

View File

@ -1,10 +1,7 @@
// lib/presentation/discount/bloc/discount_state.dart
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class DiscountState extends Equatable { class DiscountState extends Equatable {
final String? discountId; final String? discountId;
// *** CHANGE IS HERE: Storing image ID and URL together ***
final List<Map<String, String?>> productImages; final List<Map<String, String?>> productImages;
final String productName; final String productName;
final String? discountTypeId; final String? discountTypeId;
@ -57,6 +54,7 @@ class DiscountState extends Equatable {
bool? isLoadingDetails, bool? isLoadingDetails,
bool? isSuccess, bool? isSuccess,
String? errorMessage, String? errorMessage,
bool? clearErrorMessage,
}) { }) {
return DiscountState( return DiscountState(
discountId: discountId ?? this.discountId, discountId: discountId ?? this.discountId,
@ -74,7 +72,7 @@ class DiscountState extends Equatable {
isSubmitting: isSubmitting ?? this.isSubmitting, isSubmitting: isSubmitting ?? this.isSubmitting,
isLoadingDetails: isLoadingDetails ?? this.isLoadingDetails, isLoadingDetails: isLoadingDetails ?? this.isLoadingDetails,
isSuccess: isSuccess ?? this.isSuccess, isSuccess: isSuccess ?? this.isSuccess,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: (clearErrorMessage == true) ? null : errorMessage ?? this.errorMessage,
); );
} }

View File

@ -0,0 +1,46 @@
import 'package:bloc/bloc.dart';
import 'package:business_panel/core/config/api_config.dart';
import 'package:business_panel/core/services/token_storage_service.dart';
import 'package:dio/dio.dart';
part 'order_event.dart';
part 'order_state.dart';
class OrderBloc extends Bloc<OrderEvent, OrderState> {
final Dio _dio = Dio();
final TokenStorageService _tokenStorage = TokenStorageService();
OrderBloc() : super(OrderInitial()) {
on<SubmitOrder>((event, emit) async {
emit(OrderSubmissionInProgress());
try {
final token = await _tokenStorage.getAccessToken();
if (token == null) {
emit(OrderSubmissionFailure("خطای احراز هویت."));
return;
}
final response = await _dio.post(
ApiConfig.addOrder,
data: {
'Discount': event.discountId,
'User': event.userId,
},
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
if (response.statusCode == 200) {
emit(OrderSubmissionSuccess(response.data['message'] ?? "سفارش با موفقیت ثبت شد."));
} else {
emit(OrderSubmissionFailure(response.data['message'] ?? 'خطا در ثبت سفارش.'));
}
} on DioException catch (e) {
emit(OrderSubmissionFailure(e.response?.data['message'] ?? 'خطای شبکه.'));
} catch (e) {
emit(OrderSubmissionFailure('خطای پیش‌بینی نشده: ${e.toString()}'));
}
});
}
}

View File

@ -0,0 +1,10 @@
part of 'order_bloc.dart';
abstract class OrderEvent {}
class SubmitOrder extends OrderEvent {
final String discountId;
final String userId;
SubmitOrder({required this.discountId, required this.userId});
}

View File

@ -0,0 +1,17 @@
part of 'order_bloc.dart';
abstract class OrderState {}
class OrderInitial extends OrderState {}
class OrderSubmissionInProgress extends OrderState {}
class OrderSubmissionSuccess extends OrderState {
final String message;
OrderSubmissionSuccess(this.message);
}
class OrderSubmissionFailure extends OrderState {
final String error;
OrderSubmissionFailure(this.error);
}

View File

@ -10,11 +10,39 @@ import 'package:business_panel/presentation/pages/home_page.dart';
import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar.dart';
import 'package:business_panel/presentation/widgets/info_popup.dart'; import 'package:business_panel/presentation/widgets/info_popup.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:persian_datetime_picker/persian_datetime_picker.dart'; import 'package:persian_datetime_picker/persian_datetime_picker.dart';
class ThousandsSeparatorInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) {
if (newValue.text.isEmpty) {
return newValue.copyWith(text: '');
}
final String newText = newValue.text.replaceAll(',', '');
final number = int.tryParse(newText);
if (number == null) {
return oldValue;
}
final formatter = NumberFormat('#,##0');
final String newString = formatter.format(number);
return TextEditingValue(
text: newString,
selection: TextSelection.collapsed(offset: newString.length),
);
}
}
class AddDiscountPage extends StatelessWidget { class AddDiscountPage extends StatelessWidget {
final String? discountId; final String? discountId;
@ -49,9 +77,18 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
final _descController = TextEditingController(); final _descController = TextEditingController();
final _priceController = TextEditingController(); final _priceController = TextEditingController();
final _discountPriceController = TextEditingController(); final _discountPriceController = TextEditingController();
final _priceFormatter = NumberFormat('#,##0');
bool get _isEditMode => widget.discountId != null; bool get _isEditMode => widget.discountId != null;
@override
void initState() {
super.initState();
_priceController.addListener(() => setState(() {}));
_discountPriceController.addListener(() => setState(() {}));
}
@override @override
void dispose() { void dispose() {
_nameController.dispose(); _nameController.dispose();
@ -126,7 +163,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
"تخفیف با موفقیت ${_isEditMode ? 'ویرایش' : 'ثبت'} شد!"), "تخفیف با موفقیت ${_isEditMode ? 'ویرایش' : 'ثبت'} شد!"),
backgroundColor: Colors.green), backgroundColor: Colors.green),
); );
// HIGHLIGHT: مسیریابی به صفحهی اصلی و حذف تمام صفحات قبلی
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => BlocProvider( builder: (_) => BlocProvider(
@ -142,12 +178,15 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
SnackBar( SnackBar(
content: Text(state.errorMessage!), backgroundColor: Colors.red), content: Text(state.errorMessage!), backgroundColor: Colors.red),
); );
context.read<DiscountBloc>().add(ClearErrorMessage());
} }
if (state.productName.isNotEmpty && _nameController.text.isEmpty) { if (state.productName.isNotEmpty && _nameController.text.isEmpty) {
_nameController.text = state.productName; _nameController.text = state.productName;
_descController.text = state.description; _descController.text = state.description;
_priceController.text = state.price; final price = int.tryParse(state.price.replaceAll(',', '')) ?? 0;
_discountPriceController.text = state.discountedPrice; _priceController.text = _priceFormatter.format(price);
final discountedPrice = int.tryParse(state.discountedPrice.replaceAll(',', '')) ?? 0;
_discountPriceController.text = _priceFormatter.format(discountedPrice);
} }
}, },
builder: (context, state) { builder: (context, state) {
@ -185,7 +224,7 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
context.read<DiscountBloc>().add(ProductNameChanged(value)), context.read<DiscountBloc>().add(ProductNameChanged(value)),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildDiscountTypeDropdown(state), // Pass state here _buildDiscountTypeDropdown(state),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildTextField( _buildTextField(
controller: _descController, controller: _descController,
@ -202,25 +241,19 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
const SizedBox(height: 30), const SizedBox(height: 30),
_buildTimeRangePicker(context), _buildTimeRangePicker(context),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildTextField(
_buildPriceField(
controller: _priceController, controller: _priceController,
label: "قیمت بدون تخفیف", label: "قیمت بدون تخفیف (تومان)",
isRequired: true, hint: "مثلاً 240,000",
hint: "مثلاً 240000 تومان", onChanged: (value) => context.read<DiscountBloc>().add(PriceChanged(value)),
keyboardType: TextInputType.number,
onChanged: (value) =>
context.read<DiscountBloc>().add(PriceChanged(value)),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildTextField( _buildPriceField(
controller: _discountPriceController, controller: _discountPriceController,
label: "قیمت با تخفیف", label: "قیمت با تخفیف (تومان)",
hint: "مثلاً 200000 تومان", hint: "مثلاً 200,000",
isRequired: true, onChanged: (value) => context.read<DiscountBloc>().add(DiscountedPriceChanged(value)),
keyboardType: TextInputType.number,
onChanged: (value) => context
.read<DiscountBloc>()
.add(DiscountedPriceChanged(value)),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
_buildNotificationRadiusSlider(), _buildNotificationRadiusSlider(),
@ -253,6 +286,30 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
); );
} }
Widget _buildPriceField({
required TextEditingController controller,
required String label,
required String hint,
required ValueChanged<String> onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: controller,
onChanged: (value) => onChanged(value.replaceAll(',', '')),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
ThousandsSeparatorInputFormatter(),
],
decoration: _inputDecoration(label, hint: hint, isRequired: true),
),
],
);
}
Widget _buildSectionTitle({ Widget _buildSectionTitle({
required String title, required String title,
String? popupTitle, String? popupTitle,
@ -287,7 +344,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
return BlocBuilder<DiscountBloc, DiscountState>( return BlocBuilder<DiscountBloc, DiscountState>(
buildWhen: (p, c) => p.productImages != c.productImages, buildWhen: (p, c) => p.productImages != c.productImages,
builder: (context, state) { builder: (context, state) {
// We ensure the list has at least 2 elements for the UI, filling with null
final displayImages = List<Map<String, String?>?>.from(state.productImages); final displayImages = List<Map<String, String?>?>.from(state.productImages);
while (displayImages.length < 2) { while (displayImages.length < 2) {
displayImages.add(null); displayImages.add(null);
@ -296,7 +352,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(2, (index) { children: List.generate(2, (index) {
// *** CHANGE IS HERE: Read from the map structure ***
final imageMap = displayImages[index]; final imageMap = displayImages[index];
final imageUrl = imageMap?['url']; final imageUrl = imageMap?['url'];
final isUrl = imageUrl?.startsWith('http') ?? false; final isUrl = imageUrl?.startsWith('http') ?? false;
@ -345,16 +400,14 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
} }
Widget _buildDiscountTypeDropdown(DiscountState state) { Widget _buildDiscountTypeDropdown(DiscountState state) {
// Create a set of available IDs for quick lookup.
final availableTypeIds = discountTypes.map((type) => type.id).toSet(); final availableTypeIds = discountTypes.map((type) => type.id).toSet();
// Check if the current discount's type ID is in our list. If not, use null.
final String? selectedValue = availableTypeIds.contains(state.discountTypeId) final String? selectedValue = availableTypeIds.contains(state.discountTypeId)
? state.discountTypeId ? state.discountTypeId
: null; : null;
return DropdownButtonFormField<String>( return DropdownButtonFormField<String>(
value: selectedValue, // Use the safe value here. value: selectedValue,
icon: SvgPicture.asset( icon: SvgPicture.asset(
Assets.icons.arrowDown, Assets.icons.arrowDown,
width: 24, width: 24,
@ -411,7 +464,6 @@ class _AddDiscountViewState extends State<_AddDiscountView> {
Expanded( Expanded(
child: Text( child: Text(
displayText, displayText,
textDirection: TextDirection.rtl,
style: const TextStyle(fontSize: 15), style: const TextStyle(fontSize: 15),
), ),
), ),

View File

@ -0,0 +1,123 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:business_panel/presentation/order/bloc/order_bloc.dart';
import 'package:business_panel/presentation/widgets/success_popup.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:vibration/vibration.dart';
class BarcodeScannerPage extends StatefulWidget {
final String discountId;
const BarcodeScannerPage({super.key, required this.discountId});
@override
State<BarcodeScannerPage> createState() => _BarcodeScannerPageState();
}
class _BarcodeScannerPageState extends State<BarcodeScannerPage> {
final MobileScannerController _scannerController = MobileScannerController();
final AudioPlayer _audioPlayer = AudioPlayer();
bool _isProcessing = false;
void _handleBarcode(BarcodeCapture capture) {
if (_isProcessing) return;
setState(() => _isProcessing = true);
final String? rawValue = capture.barcodes.first.rawValue;
if (rawValue == null) {
_showError("بارکد نامعتبر است.");
return;
}
try {
final Map<String, dynamic> decodedToken = JwtDecoder.decode(rawValue);
final String? userId = decodedToken['userID'];
final String? discountIdFromToken = decodedToken['discountID'];
if (userId == null || discountIdFromToken == null) {
_showError("اطلاعات لازم در بارکد یافت نشد.");
return;
}
if (discountIdFromToken != widget.discountId) {
_showError("این بارکد برای این تخفیف معتبر نیست.");
return;
}
context.read<OrderBloc>().add(
SubmitOrder(discountId: discountIdFromToken, userId: userId),
);
} catch (e) {
_showError("فرمت بارکد صحیح نیست.");
}
}
void _showError(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
setState(() => _isProcessing = false);
}
@override
void dispose() {
_scannerController.dispose();
_audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("اسکن بارکد مشتری")),
body: BlocListener<OrderBloc, OrderState>(
listener: (context, state) async {
if (state is OrderSubmissionSuccess) {
if (await Vibration.hasVibrator() ?? false) {
Vibration.vibrate(duration: 200);
}
await _audioPlayer.play(AssetSource('sounds/short-success-sound-glockenspiel-treasure-video-game-6346.mp3'));
await showSuccessDialog(context, message: state.message);
Navigator.of(context).pop();
} else if (state is OrderSubmissionFailure) {
_showError(state.error);
}
},
child: Stack(
children: [
MobileScanner(
controller: _scannerController,
onDetect: _handleBarcode,
),
Center(
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
border: Border.all(color: Colors.green, width: 4),
borderRadius: BorderRadius.circular(12),
),
),
),
BlocBuilder<OrderBloc, OrderState>(
builder: (context, state) {
if (state is OrderSubmissionInProgress) {
return Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: CircularProgressIndicator(color: Colors.white),
),
);
}
return const SizedBox.shrink();
},
),
],
),
),
);
}
}

View File

@ -1,12 +1,31 @@
import 'dart:async'; // اضافه کردن کتابخانه برای استفاده از Timer import 'dart:async';
import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; import 'package:business_panel/presentation/reservation/bloc/reservation_bloc.dart';
import 'package:business_panel/presentation/widgets/active_discount_card.dart'; import 'package:business_panel/presentation/widgets/active_discount_card.dart';
import 'package:business_panel/presentation/widgets/custom_app_bar_single.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar_single.dart';
import 'package:business_panel/domain/entities/discount_entity.dart';
import 'package:business_panel/domain/entities/reservation_entity.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/gen/assets.gen.dart';
extension ReservationToDiscount on ReservationEntity {
DiscountEntity toDiscountEntity() {
return DiscountEntity(
id: id,
name: discount.name,
shopName: 'فروشگاه',
images: discount.images.map((img) => img.url).toList(),
type: discount.typeName,
description: 'رزرو شده',
price: 0.0,
nPrice: 0.0,
startDate: null,
endDate: discount.endDate,
);
}
}
class ReserveManegmment extends StatefulWidget { class ReserveManegmment extends StatefulWidget {
const ReserveManegmment({super.key}); const ReserveManegmment({super.key});
@ -15,23 +34,35 @@ class ReserveManegmment extends StatefulWidget {
} }
class _ReserveManegmmentState extends State<ReserveManegmment> { class _ReserveManegmmentState extends State<ReserveManegmment> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ReservationBloc()..add(FetchReservations()),
child: const _ReserveManegmmentView(),
);
}
}
class _ReserveManegmmentView extends StatefulWidget {
const _ReserveManegmmentView();
@override
State<_ReserveManegmmentView> createState() => _ReserveManegmmentViewState();
}
class _ReserveManegmmentViewState extends State<_ReserveManegmmentView> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
Timer? _debounce; Timer? _debounce;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.read<HomeBloc>().add(FetchDiscounts());
}
});
} }
@override @override
void dispose() { void dispose() {
_searchController.dispose(); _searchController.dispose();
_debounce?.cancel(); // کنسل کردن تایمر برای جلوگیری از نشت حافظه _debounce?.cancel();
super.dispose(); super.dispose();
} }
@ -39,7 +70,7 @@ class _ReserveManegmmentState extends State<ReserveManegmment> {
if (_debounce?.isActive ?? false) _debounce!.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () { _debounce = Timer(const Duration(milliseconds: 500), () {
if (mounted) { if (mounted) {
context.read<HomeBloc>().add(SearchDiscounts(query: query)); context.read<ReservationBloc>().add(SearchReservations(query: query));
} }
}); });
} }
@ -73,30 +104,31 @@ class _ReserveManegmmentState extends State<ReserveManegmment> {
), ),
contentPadding: const EdgeInsets.symmetric(vertical: 0), contentPadding: const EdgeInsets.symmetric(vertical: 0),
), ),
onChanged: _onSearchChanged, // استفاده از متد جدید onChanged: _onSearchChanged,
), ),
), ),
Expanded( Expanded(
child: BlocBuilder<HomeBloc, HomeState>( child: BlocBuilder<ReservationBloc, ReservationState>(
builder: (context, state) { builder: (context, state) {
if (state is HomeError) { if (state is ReservationError) {
return Center(child: Text('خطا: ${state.message}')); return Center(child: Text('خطا: ${state.message}'));
} }
if (state is HomeLoaded) { if (state is ReservationLoaded) {
if (state.discounts.isEmpty) { if (state.reservations.isEmpty) {
return const Center(child: Text("هیچ تخفیفی با این مشخصات یافت نشد.")); return const Center(child: Text("هیچ رزروی با این مشخصات یافت نشد."));
} }
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
context.read<HomeBloc>().add(FetchDiscounts()); context.read<ReservationBloc>().add(FetchReservations());
_searchController.clear(); _searchController.clear();
}, },
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: state.discounts.length, itemCount: state.reservations.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final discount = state.discounts[index]; final reservation = state.reservations[index];
final discount = reservation.toDiscountEntity();
return ActiveDiscountCard(discount: discount); return ActiveDiscountCard(discount: discount);
}, },
), ),

View File

@ -8,7 +8,7 @@ import 'package:business_panel/presentation/store_info/bloc/store_info_bloc.dart
import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart'; import 'package:business_panel/presentation/store_info/bloc/store_info_state.dart';
import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart'; // **ADD THIS IMPORT**
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -127,6 +127,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red), SnackBar(content: Text(state.errorMessage!), backgroundColor: Colors.red),
); );
context.read<StoreInfoBloc>().add(ClearStoreInfoError());
} }
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
@ -181,6 +182,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
if(state.logoPath == null) SvgPicture.asset(Assets.icons.addImg),
Align( Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: CircleAvatar( child: CircleAvatar(
@ -248,6 +250,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
_buildTextField( _buildTextField(
controller: _addressController, controller: _addressController,
label: "جزئیات آدرس", label: "جزئیات آدرس",
isRequired: true,
maxLines: 3, maxLines: 3,
hint: "خیابان، محله، ساختمان و ....", hint: "خیابان، محله، ساختمان و ....",
onChanged: onChanged:
@ -263,6 +266,8 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
child: _buildTextField( child: _buildTextField(
controller: _plaqueController, controller: _plaqueController,
label: "پلاک", label: "پلاک",
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: onChanged:
(value) => context.read<StoreInfoBloc>().add( (value) => context.read<StoreInfoBloc>().add(
PlaqueChanged(value), PlaqueChanged(value),
@ -274,6 +279,8 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
child: _buildTextField( child: _buildTextField(
controller: _postalCodeController, controller: _postalCodeController,
label: "کد پستی", label: "کد پستی",
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) => context onChanged: (value) => context
.read<StoreInfoBloc>() .read<StoreInfoBloc>()
.add(PostalCodeChanged(value)), .add(PostalCodeChanged(value)),
@ -305,7 +312,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
height: 23, height: 23,
), ),
label: const Text( label: const Text(
"انتخاب آدرس فروشگاه روی نقشه", "انتخاب آدرس فروشگاه روی نقشه *",
style: TextStyle(color: AppColors.button), style: TextStyle(color: AppColors.button),
), ),
), ),
@ -333,7 +340,9 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
_buildTextField( _buildTextField(
controller: _phoneController, controller: _phoneController,
label: "تلفن تماس", label: "تلفن تماس",
isRequired: true,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
hint: "شماره تماس ثابت یا موبایل فروشگاه", hint: "شماره تماس ثابت یا موبایل فروشگاه",
onChanged: (value) => onChanged: (value) =>
context.read<StoreInfoBloc>().add(ContactPhoneChanged(value)), context.read<StoreInfoBloc>().add(ContactPhoneChanged(value)),
@ -346,6 +355,8 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
_buildTextField( _buildTextField(
controller: _licenseController, controller: _licenseController,
label: "شماره جواز کسب", label: "شماره جواز کسب",
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
hint: "شناسه صنفی 12 رقمی یکتا", hint: "شناسه صنفی 12 رقمی یکتا",
onChanged: (value) => onChanged: (value) =>
context.read<StoreInfoBloc>().add(LicenseNumberChanged(value)), context.read<StoreInfoBloc>().add(LicenseNumberChanged(value)),
@ -353,18 +364,32 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
const SizedBox(height: 44), const SizedBox(height: 44),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: BlocBuilder<StoreInfoBloc, StoreInfoState>(
onPressed: () { builder: (context, state) {
Navigator.of(context).push( return ElevatedButton(
MaterialPageRoute( onPressed: state.isFormValid ? () {
builder: (_) => BlocProvider.value( Navigator.of(context).push(
value: BlocProvider.of<StoreInfoBloc>(context), MaterialPageRoute(
child: const StoreInfoDisplayPage(), builder: (_) => BlocProvider.value(
), value: BlocProvider.of<StoreInfoBloc>(context),
child: const StoreInfoDisplayPage(),
),
),
);
} : () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("لطفاً تمام فیلدهای ستاره‌دار را پر کنید."),
backgroundColor: Colors.red,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: state.isFormValid ? AppColors.button : Colors.grey,
), ),
child: const Text("تایید و ادامه"),
); );
}, },
child: const Text("تایید و ادامه"),
), ),
), ),
const SizedBox(height: 34), const SizedBox(height: 34),
@ -391,6 +416,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
TextInputType? keyboardType, TextInputType? keyboardType,
TextEditingController? controller, TextEditingController? controller,
ValueChanged<String>? onChanged, ValueChanged<String>? onChanged,
List<TextInputFormatter>? inputFormatters,
}) { }) {
return TextFormField( return TextFormField(
controller: controller, controller: controller,
@ -398,6 +424,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
maxLines: maxLines, maxLines: maxLines,
maxLength: maxLength, maxLength: maxLength,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters,
decoration: InputDecoration( decoration: InputDecoration(
counterText: "", counterText: "",
hintText: hint, hintText: hint,
@ -577,7 +604,7 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: context.watch<StoreInfoBloc>().state.activityTypeId, value: context.watch<StoreInfoBloc>().state.activityTypeId,
icon: SvgPicture.asset( icon: SvgPicture.asset(
Assets.icons.arrowDown, Assets.icons.arrowDown,
width: 24, width: 24,
color: Colors.black, color: Colors.black,
), ),
@ -616,56 +643,4 @@ class _StoreInfoPageState extends State<StoreInfoPage> {
), ),
); );
} }
} }
// Future<void> _pickWorkingHours(BuildContext context) async {
// // ۱. انتخاب تاریخ شروع
// Jalali? startDate = await showPersianDatePicker(
// context: context,
// initialDate: Jalali.now(),
// firstDate: Jalali(1400),
// lastDate: Jalali(1405),
// );
// if (startDate == null || !context.mounted) return;
// // ۲. انتخاب ساعت شروع
// TimeOfDay? startTime = await showTimePicker(
// context: context,
// initialTime: TimeOfDay.now(),
// );
// if (startTime == null || !context.mounted) return;
// // ۳. انتخاب تاریخ پایان
// Jalali? endDate = await showPersianDatePicker(
// context: context,
// initialDate: startDate, // شروع از تاریخ انتخابی قبلی
// firstDate: startDate, // تاریخ پایان نمیتواند قبل از شروع باشد
// lastDate: Jalali(1405),
// );
// if (endDate == null || !context.mounted) return;
// // ۴. انتخاب ساعت پایان
// TimeOfDay? endTime = await showTimePicker(
// context: context,
// initialTime: startTime,
// );
// if (endTime == null || !context.mounted) return;
// // ۵. تبدیل به آبجکت DateTime و ارسال به BLoC
// final DateTime startDateTime = startDate.toDateTime().add(
// Duration(hours: startTime.hour, minutes: startTime.minute),
// );
// final DateTime endDateTime = endDate.toDateTime().add(
// Duration(hours: endTime.hour, minutes: endTime.minute),
// );
// context.read<StoreInfoBloc>().add(
// WorkingHoursChanged(
// startDateTime: startDateTime,
// endDateTime: endDateTime,
// ),
// );
// }

View File

@ -0,0 +1,75 @@
import 'package:bloc/bloc.dart';
import 'package:business_panel/core/config/api_config.dart';
import 'package:business_panel/core/services/token_storage_service.dart';
import 'package:business_panel/core/utils/logging_interceptor.dart';
import 'package:business_panel/domain/entities/reservation_entity.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
part 'reservation_event.dart';
part 'reservation_state.dart';
class ReservationBloc extends Bloc<ReservationEvent, ReservationState> {
final Dio _dio = Dio();
final TokenStorageService _tokenStorage = TokenStorageService();
ReservationBloc() : super(ReservationInitial()) {
_dio.interceptors.add(LoggingInterceptor());
on<FetchReservations>((event, emit) async {
await _fetchReservations(emit);
});
on<SearchReservations>((event, emit) async {
await _fetchReservations(emit, searchQuery: event.query);
});
}
Future<void> _fetchReservations(Emitter<ReservationState> emit, {String? searchQuery}) async {
emit(ReservationLoading());
try {
final token = await _tokenStorage.getAccessToken();
if (token == null || token.isEmpty) {
emit(ReservationError("خطای احراز هویت. لطفا دوباره وارد شوید."));
return;
}
String url = ApiConfig.getReservations;
if (searchQuery != null && searchQuery.isNotEmpty) {
url = '$url?search=$searchQuery';
}
final response = await _dio.get(
url,
options: Options(
headers: {'Authorization': 'Bearer $token'},
),
);
if (response.statusCode == 200 && response.data['data'] != null) {
final List<dynamic> data = response.data['data']['items'] ?? [];
final reservations = data
.map((json) => ReservationEntity.fromJson(json))
.where((reservation) {
if (reservation.discount.endDate == null) return true;
return reservation.discount.endDate!.isAfter(DateTime.now());
})
.toList();
emit(ReservationLoaded(reservations));
} else {
emit(ReservationError(response.data['message'] ?? 'خطا در دریافت اطلاعات'));
}
} on DioException catch (e) {
if (kDebugMode) {
print('DioException in ReservationBloc: ${e.response?.data}');
}
emit(ReservationError(e.response?.data['message'] ?? 'خطای شبکه'));
} catch (e, stackTrace) {
if (kDebugMode) {
print('Error in ReservationBloc: $e');
print(stackTrace);
}
emit(ReservationError('خطای پیش‌بینی نشده رخ داد: ${e.toString()}'));
}
}
}

View File

@ -0,0 +1,11 @@
part of 'reservation_bloc.dart';
abstract class ReservationEvent {}
class FetchReservations extends ReservationEvent {}
class SearchReservations extends ReservationEvent {
final String query;
SearchReservations({required this.query});
}

View File

@ -0,0 +1,19 @@
part of 'reservation_bloc.dart';
abstract class ReservationState {}
class ReservationInitial extends ReservationState {}
class ReservationLoading extends ReservationState {}
class ReservationLoaded extends ReservationState {
final List<ReservationEntity> reservations;
ReservationLoaded(this.reservations);
}
class ReservationError extends ReservationState {
final String message;
ReservationError(this.message);
}

View File

@ -13,6 +13,10 @@ class StoreInfoBloc extends Bloc<StoreInfoEvent, StoreInfoState> {
final TokenStorageService _tokenStorage = TokenStorageService(); final TokenStorageService _tokenStorage = TokenStorageService();
StoreInfoBloc() : super(StoreInfoState()) { StoreInfoBloc() : super(StoreInfoState()) {
on<ClearStoreInfoError>((event, emit) {
emit(state.copyWith(clearErrorMessage: true));
});
on<StoreLogoChanged>((event, emit) { on<StoreLogoChanged>((event, emit) {
emit(state.copyWith(logoPath: event.imagePath)); emit(state.copyWith(logoPath: event.imagePath));
}); });

View File

@ -1,7 +1,9 @@
part of 'store_info_bloc.dart'; part of 'store_info_bloc.dart';
abstract class StoreInfoEvent {} abstract class StoreInfoEvent {}
class ClearStoreInfoError extends StoreInfoEvent {}
class StoreLogoChanged extends StoreInfoEvent { class StoreLogoChanged extends StoreInfoEvent {
final String imagePath; final String imagePath;
StoreLogoChanged(this.imagePath); StoreLogoChanged(this.imagePath);

View File

@ -43,6 +43,21 @@ class StoreInfoState {
this.activityTypeId, this.activityTypeId,
}); });
bool get isFormValid =>
storeName.isNotEmpty &&
activityTypeId != null &&
activityTypeId!.isNotEmpty &&
address.isNotEmpty &&
contactPhone != null &&
contactPhone!.isNotEmpty &&
workingDays.isNotEmpty &&
startTime != null &&
startTime!.isNotEmpty &&
endTime != null &&
endTime!.isNotEmpty &&
latitude != null &&
longitude != null;
StoreInfoState copyWith({ StoreInfoState copyWith({
String? logoPath, String? logoPath,
String? storeName, String? storeName,
@ -64,6 +79,7 @@ class StoreInfoState {
String? endTime, String? endTime,
List<String>? features, List<String>? features,
String? activityTypeId, String? activityTypeId,
bool? clearErrorMessage,
}) { }) {
return StoreInfoState( return StoreInfoState(
logoPath: logoPath ?? this.logoPath, logoPath: logoPath ?? this.logoPath,
@ -76,7 +92,7 @@ class StoreInfoState {
postalCode: postalCode ?? this.postalCode, postalCode: postalCode ?? this.postalCode,
isSubmitting: isSubmitting ?? this.isSubmitting, isSubmitting: isSubmitting ?? this.isSubmitting,
isSuccess: isSuccess ?? this.isSuccess, isSuccess: isSuccess ?? this.isSuccess,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: (clearErrorMessage == true) ? null : errorMessage ?? this.errorMessage,
latitude: latitude ?? this.latitude, latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude, longitude: longitude ?? this.longitude,
contactPhone: contactPhone ?? this.contactPhone, contactPhone: contactPhone ?? this.contactPhone,

View File

@ -1,8 +1,8 @@
import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/core/config/app_colors.dart';
import 'package:business_panel/domain/entities/discount_entity.dart'; import 'package:business_panel/domain/entities/discount_entity.dart';
import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/gen/assets.gen.dart';
import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; import 'package:business_panel/presentation/order/bloc/order_bloc.dart';
import 'package:business_panel/presentation/pages/add_discount_page.dart'; import 'package:business_panel/presentation/pages/barcode_scanner_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
@ -15,8 +15,6 @@ class ActiveDiscountCard extends StatelessWidget {
const ActiveDiscountCard({super.key, required this.discount}); const ActiveDiscountCard({super.key, required this.discount});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// ... (تمام کد مربوط به _buildDiscountCard از home_page.dart به اینجا منتقل شد)
// ... (متدهای کمکی مثل _buildCountdownTimer و _buildTimerLabels هم به اینجا منتقل شدند)
final remaining = final remaining =
discount.endDate != null ? discount.endDate!.difference(DateTime.now()) : const Duration(seconds: -1); discount.endDate != null ? discount.endDate!.difference(DateTime.now()) : const Duration(seconds: -1);
@ -150,30 +148,42 @@ class ActiveDiscountCard extends StatelessWidget {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( InkWell(
children: [ onTap: () {
SvgPicture.asset( Navigator.of(context).push(
Assets.icons.scanBarcode, MaterialPageRoute(
width: 18, builder: (_) => BlocProvider.value(
color: Colors.grey.shade700, value: context.read<OrderBloc>(),
), child: BarcodeScannerPage(discountId: discount.id),
const SizedBox(width: 10),
Expanded(
child: Text(
"اسکن بارکد مشتری",
style: TextStyle(
fontSize: 15,
color: AppColors.active,
), ),
overflow: TextOverflow.ellipsis,
), ),
), );
], },
child: Row(
children: [
SvgPicture.asset(
Assets.icons.scanBarcode,
width: 18,
color: Colors.grey.shade700,
),
const SizedBox(width: 10),
Expanded(
child: Text(
"اسکن بارکد مشتری",
style: TextStyle(
fontSize: 15,
color: AppColors.active,
fontWeight: FontWeight.normal,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
), ),
], ],
), ),
), ),
], ],
), ),
), ),

View File

@ -1,5 +1,3 @@
// lib/presentation/widgets/analytics_discount_card.dart
import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/core/config/app_colors.dart';
import 'package:business_panel/domain/entities/discount_entity.dart'; import 'package:business_panel/domain/entities/discount_entity.dart';
import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/gen/assets.gen.dart';
@ -99,7 +97,7 @@ class AnalyticsDiscountCard extends StatelessWidget {
textColor: Colors.grey.shade600), textColor: Colors.grey.shade600),
const SizedBox(height: 5), const SizedBox(height: 5),
if (discount.endDate == null) if (discount.endDate == null)
_buildStatusText('تاریخ نامعتبر', Colors.orange) _buildStatusText('تاریخ نامعتبر', Colors.orange)
else if (remaining.isNegative) else if (remaining.isNegative)
_buildExpiredDateRange() _buildExpiredDateRange()
else else
@ -228,7 +226,7 @@ class AnalyticsDiscountCard extends StatelessWidget {
if (value == true) { if (value == true) {
final bloc = context.read<DiscountManagementBloc>(); final bloc = context.read<DiscountManagementBloc>();
bloc.add( bloc.add(
FetchManagedDiscounts(status: 1)); // Defaulting to active FetchManagedDiscounts(status: 1));
} }
}); });
}, },

View File

@ -214,7 +214,7 @@ class DiscountCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
NumberFormat('#,##0').format(discount.nPrice), "${NumberFormat('#,##0').format(discount.nPrice)} تومان",
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,

View File

@ -0,0 +1,43 @@
import 'package:business_panel/core/config/app_colors.dart';
import 'package:business_panel/gen/assets.gen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
Future<void> showSuccessDialog(
BuildContext context, {
required String message,
}) async {
await showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(Assets.icons.tickCircle, height: 80, color: AppColors.confirm),
const SizedBox(height: 24),
Text(
"موفقیت‌آمیز!",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16, color: Colors.black54),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("فهمیدم"),
),
],
),
),
);
},
);
}

View File

@ -49,6 +49,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: e653f162ddfcec1da2040ba2d8553fff1662b5c2a5c636f4c21a3b11bee497de
url: "https://pub.dev"
source: hosted
version: "6.5.0"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
url: "https://pub.dev"
source: hosted
version: "5.2.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
url: "https://pub.dev"
source: hosted
version: "4.2.1"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
bloc: bloc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -273,6 +329,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.11"
device_info_plus:
dependency: transitive
description:
name: device_info_plus
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
url: "https://pub.dev"
source: hosted
version: "11.5.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.3"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -797,6 +869,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.9.0"
jwt_decoder:
dependency: "direct main"
description:
name: jwt_decoder
sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
latlong2: latlong2:
dependency: "direct main" dependency: "direct main"
description: description:
@ -901,6 +981,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -1234,6 +1322,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -1330,6 +1426,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
vibration:
dependency: "direct main"
description:
name: vibration
sha256: "804ee8f9628f31ee71fbe6137a2bc6206a64e101ec22cd9dd6d3a7dc0272591b"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
vibration_platform_interface:
dependency: transitive
description:
name: vibration_platform_interface
sha256: "03e9deaa4df48a1a6212e281bfee5f610d62e9247929dd2f26f4efd4fa5e225c"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -1378,6 +1490,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.13.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:
@ -1412,4 +1532,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.7.2 <4.0.0" dart: ">=3.7.2 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.29.0"

View File

@ -53,6 +53,10 @@ dependencies:
firebase_auth: ^5.7.0 firebase_auth: ^5.7.0
cloud_firestore: ^5.6.12 cloud_firestore: ^5.6.12
firebase_storage: ^12.4.10 firebase_storage: ^12.4.10
mobile_scanner: ^7.0.1
vibration: ^3.1.3
audioplayers: ^6.5.0
jwt_decoder: ^2.0.1
dev_dependencies: dev_dependencies:
@ -83,6 +87,7 @@ flutter:
assets: assets:
- assets/images/ - assets/images/
- assets/icons/ - assets/icons/
- assets/sounds/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images