From 3dc25e64a81d7414cb5411f02e2abf4707fdbd8a Mon Sep 17 00:00:00 2001 From: mohamadmahdi jebeli Date: Mon, 21 Jul 2025 11:28:31 +0330 Subject: [PATCH] finished base --- assets/icons/CLUNDER.svg | 12 + assets/icons/chart.svg | 6 + assets/icons/trash.svg | 7 + assets/icons/trash2.svg | 6 + lib/core/config/api_config.dart | 1 + lib/core/config/app_colors.dart | 2 + lib/domain/entities/discount_entity.dart | 7 +- lib/gen/assets.gen.dart | 16 + .../discount/bloc/discount_bloc.dart | 5 +- .../bloc/discount_management_bloc.dart | 112 ++++++ .../bloc/discount_management_event.dart | 23 ++ .../bloc/discount_management_state.dart | 31 ++ lib/presentation/home/bloc/home_bloc.dart | 3 +- lib/presentation/pages/add_discount_page.dart | 47 +-- .../pages/discount_manegment_page.dart | 295 ++++++++++++---- lib/presentation/pages/home_page.dart | 3 +- .../pages/reserve_manegment_page.dart | 114 ++++++ .../widgets/analytics_discount_card.dart | 324 ++++++++++++++++++ lib/presentation/widgets/custom_app_bar.dart | 153 ++++++--- .../widgets/custom_app_bar_single.dart | 2 +- .../widgets/delete_confirmation_dialog.dart | 104 ++++++ ...ount_card.dart.dart => discount_card.dart} | 0 22 files changed, 1103 insertions(+), 170 deletions(-) create mode 100644 assets/icons/CLUNDER.svg create mode 100644 assets/icons/chart.svg create mode 100644 assets/icons/trash.svg create mode 100644 assets/icons/trash2.svg create mode 100644 lib/presentation/discount_management/bloc/discount_management_bloc.dart create mode 100644 lib/presentation/discount_management/bloc/discount_management_event.dart create mode 100644 lib/presentation/discount_management/bloc/discount_management_state.dart create mode 100644 lib/presentation/pages/reserve_manegment_page.dart create mode 100644 lib/presentation/widgets/analytics_discount_card.dart create mode 100644 lib/presentation/widgets/delete_confirmation_dialog.dart rename lib/presentation/widgets/{discount_card.dart.dart => discount_card.dart} (100%) diff --git a/assets/icons/CLUNDER.svg b/assets/icons/CLUNDER.svg new file mode 100644 index 0000000..e3671a2 --- /dev/null +++ b/assets/icons/CLUNDER.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/chart.svg b/assets/icons/chart.svg new file mode 100644 index 0000000..76cf57c --- /dev/null +++ b/assets/icons/chart.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/trash.svg b/assets/icons/trash.svg new file mode 100644 index 0000000..a039c0c --- /dev/null +++ b/assets/icons/trash.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/trash2.svg b/assets/icons/trash2.svg new file mode 100644 index 0000000..f9c5bc9 --- /dev/null +++ b/assets/icons/trash2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/lib/core/config/api_config.dart b/lib/core/config/api_config.dart index 13b2fc7..05fe8c2 100644 --- a/lib/core/config/api_config.dart +++ b/lib/core/config/api_config.dart @@ -49,4 +49,5 @@ class ApiConfig { /// Body: FormData /// Headers: {'Authorization': 'Bearer '} static String editDiscount(String id) => '$baseUrl/discount/edit/$id'; + static String deleteDiscount(String id) => '$baseUrl/discount/delete/$id'; } \ No newline at end of file diff --git a/lib/core/config/app_colors.dart b/lib/core/config/app_colors.dart index 30f784e..d59507c 100644 --- a/lib/core/config/app_colors.dart +++ b/lib/core/config/app_colors.dart @@ -18,4 +18,6 @@ class AppColors { static const Color expiryReserve = Color.fromARGB(255, 183, 28, 28); static const Color uploadElevated = Color.fromARGB(255, 233, 245, 254); static const Color secTitle = Color.fromARGB(255, 95, 95, 95); + static const Color backDelete = Color.fromARGB(255, 254, 237,235); + static const Color backEdit = Color.fromARGB(255, 237, 247, 238); } \ No newline at end of file diff --git a/lib/domain/entities/discount_entity.dart b/lib/domain/entities/discount_entity.dart index 42ef9db..9427e57 100644 --- a/lib/domain/entities/discount_entity.dart +++ b/lib/domain/entities/discount_entity.dart @@ -9,6 +9,7 @@ class DiscountEntity { final String description; final double price; final double nPrice; + final DateTime? startDate; final DateTime? endDate; DiscountEntity({ @@ -20,11 +21,11 @@ class DiscountEntity { required this.description, required this.price, required this.nPrice, + this.startDate, required this.endDate, }); factory DiscountEntity.fromJson(Map json) { - // A helper function to safely extract lists List _parseImages(dynamic imageList) { if (imageList is List) { return imageList.map((img) => (img['Url'] ?? '') as String).toList(); @@ -36,7 +37,6 @@ class DiscountEntity { // --- FIX IS HERE: Reading "ID" instead of "_id" --- id: json['ID'] ?? '', // Changed from '_id' to 'ID' name: json['Name'] ?? 'بدون نام', - // Safely access nested properties shopName: (json['Shop'] != null ? json['Shop']['Name'] : 'فروشگاه') ?? 'فروشگاه', images: _parseImages(json['Images']), @@ -45,7 +45,8 @@ class DiscountEntity { description: json['Description'] ?? '', price: (json['Price'] as num? ?? 0).toDouble(), nPrice: (json['NPrice'] as num? ?? 0).toDouble(), - // Handle potential null or invalid date + startDate: + json['StartDate'] != null ? DateTime.tryParse(json['StartDate']) : null, endDate: json['EndDate'] != null ? DateTime.tryParse(json['EndDate']) : null, ); diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 0c3962e..c5b69b5 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -15,6 +15,9 @@ class $AssetsIconsGen { /// File path: assets/icons/Arrow - Right 2.svg String get arrowRight2 => 'assets/icons/Arrow - Right 2.svg'; + /// File path: assets/icons/CLUNDER.svg + String get clunder => 'assets/icons/CLUNDER.svg'; + /// File path: assets/icons/CatBack.svg String get catBack => 'assets/icons/CatBack.svg'; @@ -102,6 +105,9 @@ class $AssetsIconsGen { /// File path: assets/icons/card-pos.svg String get cardPos => 'assets/icons/card-pos.svg'; + /// File path: assets/icons/chart.svg + String get chart => 'assets/icons/chart.svg'; + /// File path: assets/icons/cinama.svg String get cinama => 'assets/icons/cinama.svg'; @@ -225,6 +231,12 @@ class $AssetsIconsGen { /// File path: assets/icons/timer-pause.svg String get timerPause => 'assets/icons/timer-pause.svg'; + /// File path: assets/icons/trash.svg + String get trash => 'assets/icons/trash.svg'; + + /// File path: assets/icons/trash2.svg + String get trash2 => 'assets/icons/trash2.svg'; + /// File path: assets/icons/volume-high.svg String get volumeHigh => 'assets/icons/volume-high.svg'; @@ -234,6 +246,7 @@ class $AssetsIconsGen { /// List of all assets List get values => [ arrowRight2, + clunder, catBack, cinema, coffee, @@ -263,6 +276,7 @@ class $AssetsIconsGen { callCalling, camera, cardPos, + chart, cinama, clock, clockProduct, @@ -304,6 +318,8 @@ class $AssetsIconsGen { tickPb, ticketDiscount, timerPause, + trash, + trash2, volumeHigh, warning2, ]; diff --git a/lib/presentation/discount/bloc/discount_bloc.dart b/lib/presentation/discount/bloc/discount_bloc.dart index 045464f..3134069 100644 --- a/lib/presentation/discount/bloc/discount_bloc.dart +++ b/lib/presentation/discount/bloc/discount_bloc.dart @@ -45,7 +45,7 @@ class DiscountBloc extends Bloc { images = (data['Images'] as List) .map>( (img) => { - 'id': img['_id'] as String?, + 'id': img['ID'] as String?, 'url': img['Url'] as String?, }, ) @@ -208,10 +208,9 @@ class DiscountBloc extends Bloc { } if (isEdit) { - data['ExistingImages'] = jsonEncode(existingImageIds); + data['Picture'] = existingImageIds; } - // *** CHANGE IS HERE: Logging the data before sending *** log('Submitting Discount Data: $data', name: 'DiscountBloc'); final formData = FormData.fromMap(data); diff --git a/lib/presentation/discount_management/bloc/discount_management_bloc.dart b/lib/presentation/discount_management/bloc/discount_management_bloc.dart new file mode 100644 index 0000000..be1a0ed --- /dev/null +++ b/lib/presentation/discount_management/bloc/discount_management_bloc.dart @@ -0,0 +1,112 @@ +// lib/presentation/discount_management/bloc/discount_management_bloc.dart +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/discount_entity.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +part 'discount_management_event.dart'; +part 'discount_management_state.dart'; + +class DiscountManagementBloc extends Bloc { + final Dio _dio = Dio(); + final TokenStorageService _tokenStorage = TokenStorageService(); + + DiscountManagementBloc() : super(DiscountManagementInitial()) { + _dio.interceptors.add(LoggingInterceptor()); + + on((event, emit) async { + await _fetchDiscounts(emit, status: event.status); + }); + + on((event, emit) async { + await _fetchDiscounts(emit, searchQuery: event.query, status: event.status); + }); + + // **NEW EVENT HANDLER** + on((event, emit) async { + emit(DiscountDeleteInProgress()); + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null || token.isEmpty) { + emit(DiscountDeleteFailure("خطای احراز هویت.")); + return; + } + + final response = await _dio.get( + ApiConfig.deleteDiscount(event.discountId), + options: Options( + headers: {'Authorization': 'Bearer $token'}, + ), + ); + + if (response.statusCode == 200) { + emit(DiscountDeleteSuccess("تخفیف با موفقیت حذف شد.")); + } else { + emit(DiscountDeleteFailure(response.data['message'] ?? 'خطا در حذف تخفیف')); + } + } on DioException catch (e) { + emit(DiscountDeleteFailure(e.response?.data['message'] ?? 'خطای شبکه')); + } catch (e) { + emit(DiscountDeleteFailure('خطای پیش‌بینی نشده: ${e.toString()}')); + } + }); + } + + Future _fetchDiscounts(Emitter emit, + {String? searchQuery, int? status}) async { + // To prevent showing old data, we start with loading + // but only if the current state is not about deletion status + if (state is! DiscountDeleteSuccess && state is! DiscountDeleteFailure) { + emit(DiscountManagementLoading()); + } + + try { + final token = await _tokenStorage.getAccessToken(); + if (token == null || token.isEmpty) { + emit(DiscountManagementError("خطای احراز هویت. لطفا دوباره وارد شوید.")); + return; + } + + String url = ApiConfig.getDiscounts; + Map queryParameters = {}; + + if (status != null) { + queryParameters['status'] = status; + } + if (searchQuery != null && searchQuery.isNotEmpty) { + queryParameters['search'] = searchQuery; + } + + final response = await _dio.get( + url, + queryParameters: queryParameters, + options: Options( + headers: {'Authorization': 'Bearer $token'}, + ), + ); + + if (response.statusCode == 200 && response.data['data'] != null) { + final List data = response.data['data']['discounts'] ?? []; + final discounts = + data.map((json) => DiscountEntity.fromJson(json)).toList(); + emit(DiscountManagementLoaded(discounts)); + } else { + emit(DiscountManagementError(response.data['message'] ?? 'خطا در دریافت اطلاعات')); + } + } on DioException catch (e) { + if (kDebugMode) { + print('DioException in DiscountManagementBloc: ${e.response?.data}'); + } + emit(DiscountManagementError(e.response?.data['message'] ?? 'خطای شبکه')); + } catch (e, stackTrace) { + if (kDebugMode) { + print('Error in DiscountManagementBloc: $e'); + print(stackTrace); + } + emit(DiscountManagementError('خطای پیش‌بینی نشده رخ داد: ${e.toString()}')); + } + } +} \ No newline at end of file diff --git a/lib/presentation/discount_management/bloc/discount_management_event.dart b/lib/presentation/discount_management/bloc/discount_management_event.dart new file mode 100644 index 0000000..bbc3d1c --- /dev/null +++ b/lib/presentation/discount_management/bloc/discount_management_event.dart @@ -0,0 +1,23 @@ +part of 'discount_management_bloc.dart'; + +abstract class DiscountManagementEvent {} + +class FetchManagedDiscounts extends DiscountManagementEvent { + final int? status; // 1 for active, 0 for inactive + + FetchManagedDiscounts({this.status}); +} + +class SearchManagedDiscounts extends DiscountManagementEvent { + final String query; + final int? status; + + SearchManagedDiscounts({required this.query, this.status}); +} + +// **NEW EVENT** +class DeleteDiscount extends DiscountManagementEvent { + final String discountId; + + DeleteDiscount(this.discountId); +} \ No newline at end of file diff --git a/lib/presentation/discount_management/bloc/discount_management_state.dart b/lib/presentation/discount_management/bloc/discount_management_state.dart new file mode 100644 index 0000000..0cf299b --- /dev/null +++ b/lib/presentation/discount_management/bloc/discount_management_state.dart @@ -0,0 +1,31 @@ +part of 'discount_management_bloc.dart'; + +abstract class DiscountManagementState {} + +class DiscountManagementInitial extends DiscountManagementState {} + +class DiscountManagementLoading extends DiscountManagementState {} + +class DiscountManagementLoaded extends DiscountManagementState { + final List discounts; + + DiscountManagementLoaded(this.discounts); +} + +class DiscountManagementError extends DiscountManagementState { + final String message; + + DiscountManagementError(this.message); +} + +class DiscountDeleteInProgress extends DiscountManagementState {} + +class DiscountDeleteSuccess extends DiscountManagementState { + final String message; + DiscountDeleteSuccess(this.message); +} + +class DiscountDeleteFailure extends DiscountManagementState { + final String error; + DiscountDeleteFailure(this.error); +} \ No newline at end of file diff --git a/lib/presentation/home/bloc/home_bloc.dart b/lib/presentation/home/bloc/home_bloc.dart index 57129f4..1cffd50 100644 --- a/lib/presentation/home/bloc/home_bloc.dart +++ b/lib/presentation/home/bloc/home_bloc.dart @@ -1,3 +1,4 @@ +// lib/presentation/home/bloc/home_bloc.dart import 'package:bloc/bloc.dart'; import 'package:business_panel/core/config/api_config.dart'; import 'package:business_panel/core/services/token_storage_service.dart'; @@ -36,7 +37,7 @@ class HomeBloc extends Bloc { String url = ApiConfig.getActiveDiscounts; if (searchQuery != null && searchQuery.isNotEmpty) { - url = '$url&search=$searchQuery'; + url = '$url?search=$searchQuery'; } final response = await _dio.get( diff --git a/lib/presentation/pages/add_discount_page.dart b/lib/presentation/pages/add_discount_page.dart index a67ba9a..1fedd03 100644 --- a/lib/presentation/pages/add_discount_page.dart +++ b/lib/presentation/pages/add_discount_page.dart @@ -5,6 +5,7 @@ import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/discount/bloc/discount_bloc.dart'; import 'package:business_panel/presentation/discount/bloc/discount_event.dart'; import 'package:business_panel/presentation/discount/bloc/discount_state.dart'; +import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; import 'package:business_panel/presentation/widgets/info_popup.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -113,14 +114,14 @@ class _AddDiscountViewState extends State<_AddDiscountView> { @override Widget build(BuildContext context) { return Scaffold( - appBar: _buildCustomAppBar(context), + appBar: CustomAppBar(), body: BlocConsumer( listener: (context, state) { if (state.isSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - "تخفیف با موفقیت ${this._isEditMode ? 'ویرایش' : 'ثبت'} شد!"), + "تخفیف با موفقیت ${_isEditMode ? 'ویرایش' : 'ثبت'} شد!"), backgroundColor: Colors.green), ); Navigator.of(context).pop(true); @@ -583,46 +584,4 @@ class _AddDiscountViewState extends State<_AddDiscountView> { ), ); } - - PreferredSizeWidget _buildCustomAppBar(BuildContext context) { - return PreferredSize( - preferredSize: const Size.fromHeight(70.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.vertical(bottom: Radius.circular(15)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SvgPicture.asset(Assets.icons.logoWithName), - Row( - children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.discountShape, color: Colors.black), - ), - IconButton( - onPressed: () {}, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } } \ No newline at end of file diff --git a/lib/presentation/pages/discount_manegment_page.dart b/lib/presentation/pages/discount_manegment_page.dart index 8d4bab3..c7af69b 100644 --- a/lib/presentation/pages/discount_manegment_page.dart +++ b/lib/presentation/pages/discount_manegment_page.dart @@ -1,31 +1,47 @@ -import 'dart:async'; // اضافه کردن کتابخانه برای استفاده از Timer -import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; -import 'package:business_panel/presentation/widgets/active_discount_card.dart'; +// lib/presentation/pages/discount_manegment_page.dart +import 'dart:async'; +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/domain/entities/discount_entity.dart'; +import 'package:business_panel/presentation/discount_management/bloc/discount_management_bloc.dart'; +import 'package:business_panel/presentation/widgets/analytics_discount_card.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar_single.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:business_panel/gen/assets.gen.dart'; -class DiscountManegment extends StatefulWidget { - const DiscountManegment({super.key}); +class DiscountManegmentPage extends StatelessWidget { + const DiscountManegmentPage({super.key}); @override - State createState() => _DiscountManegmentState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DiscountManagementBloc(), + child: const _DiscountManegmentView(), + ); + } } -class _DiscountManegmentState extends State { +class _DiscountManegmentView extends StatefulWidget { + const _DiscountManegmentView(); + + @override + State<_DiscountManegmentView> createState() => _DiscountManegmentPageState(); +} + +class _DiscountManegmentPageState extends State<_DiscountManegmentView> { final TextEditingController _searchController = TextEditingController(); Timer? _debounce; + int _selectedStatus = 1; // 1 for active, 0 for inactive @override void initState() { super.initState(); - // Fetch initial discounts when the page loads - // Note: Using addPostFrameCallback to ensure context is available WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { - context.read().add(FetchDiscounts()); + context + .read() + .add(FetchManagedDiscounts(status: _selectedStatus)); } }); } @@ -33,7 +49,7 @@ class _DiscountManegmentState extends State { @override void dispose() { _searchController.dispose(); - _debounce?.cancel(); // کنسل کردن تایمر برای جلوگیری از نشت حافظه + _debounce?.cancel(); super.dispose(); } @@ -41,7 +57,8 @@ class _DiscountManegmentState extends State { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { if (mounted) { - context.read().add(SearchDiscounts(query: query)); + context.read().add( + SearchManagedDiscounts(query: query, status: _selectedStatus)); } }); } @@ -50,67 +67,207 @@ class _DiscountManegmentState extends State { Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBarSingle( - page: "رزرو ها", + page: "تخفیف ها", ), - body: Column( + body: BlocListener( + listener: (context, state) { + if (state is DiscountDeleteSuccess) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: Colors.green), + ); + context + .read() + .add(FetchManagedDiscounts(status: _selectedStatus)); + } else if (state is DiscountDeleteFailure) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.error), backgroundColor: Colors.red), + ); + } + }, + child: Column( + children: [ + _buildSearchBar(), + _buildStatusFilters(), + _buildDiscountList(), + ], + ), + ), + ); + } + + Widget _buildSearchBar() { + return Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'دنبال چی می‌گردی؟', + hintStyle: + const TextStyle(color: Color.fromARGB(255, 157, 157, 157)), + prefixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset(Assets.icons.riSearch2Line), + ), + fillColor: const Color.fromARGB(255, 244, 244, 244), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(50), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + onChanged: _onSearchChanged, + ), + ); + } + + Widget _buildStatusFilters() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'دنبال چی می‌گردی؟', - hintStyle: TextStyle(color: Color.fromARGB(255, 157, 157, 157)), - prefixIcon: Padding( - padding: const EdgeInsets.all(12.0), - child: SvgPicture.asset( - Assets.icons.riSearch2Line, - ), - ), - fillColor: Color.fromARGB(255, 244, 244, 244), - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(50), - borderSide: BorderSide.none, - ), - contentPadding: const EdgeInsets.symmetric(vertical: 0), - ), - onChanged: _onSearchChanged, // استفاده از متد جدید - ), - ), - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state is HomeError) { - return Center(child: Text('خطا: ${state.message}')); - } - - if (state is HomeLoaded) { - if (state.discounts.isEmpty) { - return const Center(child: Text("هیچ تخفیفی با این مشخصات یافت نشد.")); - } - return RefreshIndicator( - onRefresh: () async { - context.read().add(FetchDiscounts()); - _searchController.clear(); - }, - child: ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16), - itemCount: state.discounts.length, - itemBuilder: (context, index) { - final discount = state.discounts[index]; - return ActiveDiscountCard(discount: discount); - }, - ), - ); - } - - return const Center(child: CircularProgressIndicator()); - }, - ), - ), + _buildStatusSelector(context, text: "تخفیف‌های فعال", status: 1), + const SizedBox(width: 16), + _buildStatusSelector(context, + text: "تخفیف‌های غیر فعال", status: 0), ], ), ); } + + Widget _buildStatusSelector(BuildContext context, + {required String text, required int status}) { + final bool isSelected = _selectedStatus == status; + return Expanded( + child: InkWell( + onTap: () { + setState(() => _selectedStatus = status); + context + .read() + .add(FetchManagedDiscounts(status: _selectedStatus)); + _searchController.clear(); + }, + borderRadius: BorderRadius.circular(50), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSelected ? Colors.white : Colors.transparent, + border: Border.all( + color: + isSelected ? AppColors.active : Colors.grey.shade400, + width: 2, + ), + ), + child: isSelected + ? Center( + child: Container( + width: 12, + height: 12, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.active, + ), + ), + ) + : null, + ), + const SizedBox(width: 10), + Text( + text, + style: const TextStyle( + color: AppColors.hint, + fontWeight: FontWeight.bold, + fontSize: 15), + ), + ], + ), + ), + ), + ); + } + + Widget _buildDiscountList() { + return Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is DiscountManagementLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is DiscountManagementError) { + return Center(child: Text('خطا: ${state.message}')); + } + + if (state is DiscountManagementLoaded) { + if (state.discounts.isEmpty) { + return const Center( + child: Text("هیچ تخفیفی با این مشخصات یافت نشد.")); + } + // Group discounts + final Map> groupedDiscounts = {}; + for (var discount in state.discounts) { + if (groupedDiscounts.containsKey(discount.type)) { + groupedDiscounts[discount.type]!.add(discount); + } else { + groupedDiscounts[discount.type] = [discount]; + } + } + final groupKeys = groupedDiscounts.keys.toList(); + + return RefreshIndicator( + onRefresh: () async { + context + .read() + .add(FetchManagedDiscounts(status: _selectedStatus)); + _searchController.clear(); + }, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: groupKeys.length, + itemBuilder: (context, index) { + final type = groupKeys[index]; + final discountsOfType = groupedDiscounts[type]!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 24, 0, 8), + child: Text( + "تخفیف $type", + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ...discountsOfType.map((discount) { + return BlocProvider.value( + value: context.read(), + child: AnalyticsDiscountCard(discount: discount), + ); + }).toList(), + ], + ); + }, + ), + ); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ); + } } \ No newline at end of file diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index f3935c8..94c5d67 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -3,7 +3,7 @@ import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; import 'package:business_panel/presentation/pages/add_discount_page.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar.dart'; -import 'package:business_panel/presentation/widgets/discount_card.dart.dart'; +import 'package:business_panel/presentation/widgets/discount_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -49,7 +49,6 @@ class HomePage extends StatelessWidget { ); } - // برای حالت‌های HomeInitial و HomeLoading return const Center(child: CircularProgressIndicator()); }, ), diff --git a/lib/presentation/pages/reserve_manegment_page.dart b/lib/presentation/pages/reserve_manegment_page.dart new file mode 100644 index 0000000..06f2fe3 --- /dev/null +++ b/lib/presentation/pages/reserve_manegment_page.dart @@ -0,0 +1,114 @@ +import 'dart:async'; // اضافه کردن کتابخانه برای استفاده از Timer +import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; +import 'package:business_panel/presentation/widgets/active_discount_card.dart'; +import 'package:business_panel/presentation/widgets/custom_app_bar_single.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:business_panel/gen/assets.gen.dart'; + +class ReserveManegmment extends StatefulWidget { + const ReserveManegmment({super.key}); + + @override + State createState() => _ReserveManegmmentState(); +} + +class _ReserveManegmmentState extends State { + final TextEditingController _searchController = TextEditingController(); + Timer? _debounce; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + context.read().add(FetchDiscounts()); + } + }); + } + + @override + void dispose() { + _searchController.dispose(); + _debounce?.cancel(); // کنسل کردن تایمر برای جلوگیری از نشت حافظه + super.dispose(); + } + + void _onSearchChanged(String query) { + if (_debounce?.isActive ?? false) _debounce!.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + if (mounted) { + context.read().add(SearchDiscounts(query: query)); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBarSingle( + page: "رزرو ها", + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'دنبال چی می‌گردی؟', + hintStyle: TextStyle(color: Color.fromARGB(255, 157, 157, 157)), + prefixIcon: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset( + Assets.icons.riSearch2Line, + ), + ), + fillColor: Color.fromARGB(255, 244, 244, 244), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(50), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + onChanged: _onSearchChanged, // استفاده از متد جدید + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is HomeError) { + return Center(child: Text('خطا: ${state.message}')); + } + + if (state is HomeLoaded) { + if (state.discounts.isEmpty) { + return const Center(child: Text("هیچ تخفیفی با این مشخصات یافت نشد.")); + } + return RefreshIndicator( + onRefresh: () async { + context.read().add(FetchDiscounts()); + _searchController.clear(); + }, + child: ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: state.discounts.length, + itemBuilder: (context, index) { + final discount = state.discounts[index]; + return ActiveDiscountCard(discount: discount); + }, + ), + ); + } + + return const Center(child: CircularProgressIndicator()); + }, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/analytics_discount_card.dart b/lib/presentation/widgets/analytics_discount_card.dart new file mode 100644 index 0000000..9212616 --- /dev/null +++ b/lib/presentation/widgets/analytics_discount_card.dart @@ -0,0 +1,324 @@ +// lib/presentation/widgets/analytics_discount_card.dart + +import 'package:business_panel/core/config/app_colors.dart'; +import 'package:business_panel/domain/entities/discount_entity.dart'; +import 'package:business_panel/gen/assets.gen.dart'; +import 'package:business_panel/presentation/discount_management/bloc/discount_management_bloc.dart'; +import 'package:business_panel/presentation/pages/add_discount_page.dart'; +import 'package:business_panel/presentation/widgets/delete_confirmation_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:persian_datetime_picker/persian_datetime_picker.dart'; +import 'package:slide_countdown/slide_countdown.dart'; + +class AnalyticsDiscountCard extends StatelessWidget { + final DiscountEntity discount; + + const AnalyticsDiscountCard({super.key, required this.discount}); + + @override + Widget build(BuildContext context) { + final remaining = discount.endDate != null + ? discount.endDate!.difference(DateTime.now()) + : const Duration(seconds: -1); + + final bool isExpired = remaining.isNegative; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isExpired) _buildActionButtons(context), + const SizedBox( + height: 5, + ), + Card( + color: Colors.white, + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + side: BorderSide(color: Colors.grey.shade300, width: 1), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProductImage(), + const SizedBox(width: 16), + _buildCardDetails(context, remaining), + ], + ), + ), + ), + const SizedBox(height: 15), + const Divider(height: 1), + const SizedBox(height: 20), + ], + ); + } + + Widget _buildProductImage() { + return ClipRRect( + borderRadius: BorderRadius.circular(15), + child: (discount.images.isNotEmpty && discount.images.first.isNotEmpty) + ? Image.network( + discount.images.first, + width: 100, + height: 100, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildImagePlaceholder(), + ) + : _buildImagePlaceholder(), + ); + } + + Widget _buildImagePlaceholder() { + return Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(15), + ), + child: const Icon(Icons.store, color: Colors.grey, size: 50), + ); + } + + Widget _buildCardDetails(BuildContext context, Duration remaining) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 5), + _buildInfoRow( + icon: Assets.icons.shoppingCart, + text: discount.name, + textColor: Colors.grey.shade600), + const SizedBox(height: 5), + if (discount.endDate == null) + _buildStatusText('تاریخ نامعتبر', Colors.orange) + else if (remaining.isNegative) + _buildExpiredDateRange() + else + _buildCountdownSection(remaining), + const SizedBox(height: 10), + _buildInfoRow( + icon: Assets.icons.chart, + text: "آنالیز فروش", + textColor: AppColors.active, + isBold: true, + ), + ], + ), + ); + } + + Widget _buildInfoRow( + {required String icon, + required String text, + Color? textColor, + bool isBold = false}) { + return Row( + children: [ + SvgPicture.asset(icon, width: 18, color: Colors.grey.shade700), + const SizedBox(width: 10), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 17, + color: textColor ?? Colors.black, + fontWeight: FontWeight.normal, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + Widget _buildStatusText(String text, Color color) { + return Text( + text, + style: TextStyle(color: color, fontWeight: FontWeight.bold), + ); + } + + Widget _buildExpiredDateRange() { + final jalaliStart = Jalali.fromDateTime(discount.startDate!); + final jalaliEnd = Jalali.fromDateTime(discount.endDate!); + final formattedDateRange = + '${jalaliStart.day} ${jalaliStart.formatter.mN} تا ${jalaliEnd.day} ${jalaliEnd.formatter.mN} ${jalaliEnd.year}'; + + return Row( + children: [ + SvgPicture.asset(Assets.icons.clunder, + width: 18, color: Colors.grey.shade700), + const SizedBox(width: 10), + Expanded( + child: Text( + formattedDateRange, + style: const TextStyle( + color: AppColors.secTitle, + fontWeight: FontWeight.normal, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + Widget _buildCountdownSection(Duration remaining) { + return Row( + children: [ + SvgPicture.asset(Assets.icons.timerPause, + width: 18, color: Colors.grey.shade700), + const SizedBox(width: 5), + Expanded(child: _buildCountdownTimer(remaining)), + ], + ); + } + + Widget _buildActionButtons(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + children: [ + InkWell( + onTap: () => _showDeleteConfirmation(context), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + color: AppColors.backDelete, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.expiryReserve)), + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.icons.trash, + width: 20, color: AppColors.expiryReserve), + const SizedBox(width: 5), + const Text("حذف", + style: TextStyle( + color: AppColors.expiryReserve, + fontWeight: FontWeight.bold)), + ], + ), + ), + ), + ), + const SizedBox(width: 15), + InkWell( + onTap: () { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (_) => AddDiscountPage(discountId: discount.id), + ), + ) + .then((value) { + if (value == true) { + final bloc = context.read(); + bloc.add( + FetchManagedDiscounts(status: 1)); // Defaulting to active + } + }); + }, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + color: AppColors.backEdit, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.selectedImg)), + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.icons.edit, + width: 20, color: AppColors.selectedImg), + const SizedBox(width: 5), + const Text("ویرایش", + style: TextStyle( + color: AppColors.selectedImg, + fontWeight: FontWeight.bold)), + ], + ), + ), + ), + ), + ], + ), + ); + } + + void _showDeleteConfirmation(BuildContext context) { + showDeleteConfirmationDialog( + context, + title: 'حذف تخفیف', + content: 'با حذف این تخفیف، رزروهای احتمالی هم غیرفعال می‌شن و دیگه برای مشتری‌ها نمایش داده نمی‌شه.', + onConfirm: () { + context + .read() + .add(DeleteDiscount(discount.id)); + }, + ); + } + + Widget _buildCountdownTimer(Duration remaining) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Directionality( + textDirection: TextDirection.ltr, + child: SlideCountdown( + duration: remaining, + slideDirection: SlideDirection.up, + separator: ':', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: AppColors.countdown), + separatorStyle: + const TextStyle(fontSize: 15, color: AppColors.countdown), + decoration: const BoxDecoration(color: Colors.transparent), + shouldShowDays: (d) => d.inDays > 0, + shouldShowHours: (d) => true, + shouldShowMinutes: (d) => true, + ), + ), + const SizedBox(height: 4), + _buildTimerLabels(remaining), + ], + ); + } + + Widget _buildTimerLabels(Duration duration) { + const labelStyle = TextStyle(fontSize: 9, color: AppColors.selectedImg); + List labels = []; + if (duration.inDays > 0) { + labels.add(const SizedBox(width: 30, child: Text("روز", style: labelStyle))); + } + if (duration.inHours > 0 || duration.inDays > 0) { + labels + .add(const SizedBox(width: 35, child: Text("ساعت", style: labelStyle))); + } + labels + .add(const SizedBox(width: 35, child: Text("دقیقه", style: labelStyle))); + labels + .add(const SizedBox(width: 35, child: Text(" ثانیه", style: labelStyle))); + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: labels.reversed.toList(), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/custom_app_bar.dart b/lib/presentation/widgets/custom_app_bar.dart index d0fa6e9..b1f5aa9 100644 --- a/lib/presentation/widgets/custom_app_bar.dart +++ b/lib/presentation/widgets/custom_app_bar.dart @@ -1,7 +1,9 @@ +// lib/presentation/widgets/custom_app_bar.dart import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; import 'package:business_panel/presentation/pages/discount_manegment_page.dart'; +import 'package:business_panel/presentation/pages/reserve_manegment_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -32,67 +34,124 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { SvgPicture.asset(Assets.icons.logoWithName), Row( children: [ - IconButton( - onPressed: () {}, - icon: SvgPicture.asset( - Assets.icons.discountShape, - color: Colors.black, - ), - ), BlocBuilder( builder: (context, state) { final count = state is HomeLoaded ? state.discounts.length : 0; - return Stack( - alignment: Alignment.center, + return Row( children: [ - IconButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const DiscountManegment(), - ), - ); - }, - icon: SvgPicture.asset(Assets.icons.scanBarcode), - ), - if (count > 0) - Positioned( - top: 2, - right: 6, - child: GestureDetector( - onTap: () { + Stack( + alignment: Alignment.center, + children: [ + IconButton( + onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => const DiscountManegment(), + builder: (_) => + const DiscountManegmentPage(), ), ); }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: AppColors.selectedImg, - shape: BoxShape.circle, - ), - constraints: const BoxConstraints( - minWidth: 16, - minHeight: 16, - ), - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Text( - '$count', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, + icon: SvgPicture.asset( + Assets.icons.discountShape, + color: Colors.black, + ), + ), + if (count > 0) + Positioned( + top: 2, + right: 6, + child: GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + const DiscountManegmentPage(), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), ), - textAlign: TextAlign.center, ), ), ), + ], + ), + Stack( + alignment: Alignment.center, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + const ReserveManegmment(), + ), + ); + }, + icon: + SvgPicture.asset(Assets.icons.scanBarcode), ), - ), + if (count > 0) + Positioned( + top: 2, + right: 6, + child: GestureDetector( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + const ReserveManegmment(), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: const BoxDecoration( + color: AppColors.selectedImg, + shape: BoxShape.circle, + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ), + ], + ), ], ); }, @@ -108,4 +167,4 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Size get preferredSize => const Size.fromHeight(70.0); -} +} \ No newline at end of file diff --git a/lib/presentation/widgets/custom_app_bar_single.dart b/lib/presentation/widgets/custom_app_bar_single.dart index b4d82f3..af03d94 100644 --- a/lib/presentation/widgets/custom_app_bar_single.dart +++ b/lib/presentation/widgets/custom_app_bar_single.dart @@ -1,7 +1,7 @@ import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/home/bloc/home_bloc.dart'; -import 'package:business_panel/presentation/pages/discount_manegment_page.dart'; +import 'package:business_panel/presentation/pages/reserve_manegment_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; diff --git a/lib/presentation/widgets/delete_confirmation_dialog.dart b/lib/presentation/widgets/delete_confirmation_dialog.dart new file mode 100644 index 0000000..f6c3987 --- /dev/null +++ b/lib/presentation/widgets/delete_confirmation_dialog.dart @@ -0,0 +1,104 @@ +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 showDeleteConfirmationDialog( + BuildContext context, { + required String title, + required String content, + required VoidCallback onConfirm, +}) async { + await showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + elevation: 10, + backgroundColor: Colors.white, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.topCenter, + children: [ + Padding( + padding: const EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Text( + content, + style: const TextStyle(color: AppColors.hint, fontSize: 16, height: 1.6), + textAlign: TextAlign.start, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text( + "لغو", + style: TextStyle(color: Colors.black), + ), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + side: BorderSide(color: AppColors.expiryReserve), + backgroundColor: AppColors.backDelete, + foregroundColor: AppColors.expiryReserve, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 10), + ), + onPressed: () { + onConfirm(); + Navigator.of(context).pop(); + }, + child: const Text("حذف تخفیف"), + ), + ], + ), + ], + ), + ), + Positioned( + top: -40, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: CircleAvatar( + backgroundColor: Colors.white, + radius: 40, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset( + Assets.icons.trash2, + height: 50, + width: 50, + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); +} \ No newline at end of file diff --git a/lib/presentation/widgets/discount_card.dart.dart b/lib/presentation/widgets/discount_card.dart similarity index 100% rename from lib/presentation/widgets/discount_card.dart.dart rename to lib/presentation/widgets/discount_card.dart