Compare commits
2 Commits
dfb93f2fc6
...
205a58359c
| Author | SHA1 | Date |
|---|---|---|
|
|
205a58359c | |
|
|
585cb91df5 |
|
|
@ -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"
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
@ -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'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {} // <-- این رویداد جدید را اضافه کنید
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()}'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
@ -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()}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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});
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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("فهمیدم"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
122
pubspec.lock
122
pubspec.lock
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue