finished base

This commit is contained in:
mohamadmahdi jebeli 2025-07-21 11:28:31 +03:30
parent 9e40be9045
commit 3dc25e64a8
22 changed files with 1103 additions and 170 deletions

12
assets/icons/CLUNDER.svg Normal file
View File

@ -0,0 +1,12 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 1.5V3.75" stroke="#A1A0A0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 1.5V3.75" stroke="#A1A0A0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.625 6.81738H15.375" stroke="#A1A0A0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.75 6.375V12.75C15.75 15 14.625 16.5 12 16.5H6C3.375 16.5 2.25 15 2.25 12.75V6.375C2.25 4.125 3.375 2.625 6 2.625H12C14.625 2.625 15.75 4.125 15.75 6.375Z" stroke="#A1A0A0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.771 10.2754H11.7778" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.771 12.5254H11.7778" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.99661 10.2754H9.00335" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.99661 12.5254H9.00335" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.22073 10.2754H6.22747" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.22073 12.5254H6.22747" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

6
assets/icons/chart.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 17.4766H16.5" stroke="#A1A0A0" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.3125 3.97656V17.4766H10.6875V3.97656C10.6875 3.15156 10.35 2.47656 9.3375 2.47656H8.6625C7.65 2.47656 7.3125 3.15156 7.3125 3.97656Z" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.25 8.47656V17.4766H5.25V8.47656C5.25 7.65156 4.95 6.97656 4.05 6.97656H3.45C2.55 6.97656 2.25 7.65156 2.25 8.47656Z" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.75 12.2266V17.4766H15.75V12.2266C15.75 11.4016 15.45 10.7266 14.55 10.7266H13.95C13.05 10.7266 12.75 11.4016 12.75 12.2266Z" stroke="#A1A0A0" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

7
assets/icons/trash.svg Normal file
View File

@ -0,0 +1,7 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 3.98665C12.28 3.76665 10.0467 3.65332 7.82 3.65332C6.5 3.65332 5.18 3.71999 3.86 3.85332L2.5 3.98665" stroke="#B71C1C" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.16666 3.31301L6.31332 2.43967C6.41999 1.80634 6.49999 1.33301 7.62666 1.33301H9.37332C10.5 1.33301 10.5867 1.83301 10.6867 2.44634L10.8333 3.31301" stroke="#B71C1C" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.0667 6.09375L12.6333 12.8071C12.56 13.8537 12.5 14.6671 10.64 14.6671H6.35999C4.49999 14.6671 4.43999 13.8537 4.36665 12.8071L3.93332 6.09375" stroke="#B71C1C" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.38666 11H9.60666" stroke="#B71C1C" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.83334 8.33301H10.1667" stroke="#B71C1C" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 931 B

6
assets/icons/trash2.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="53" height="53" viewBox="0 0 53 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.4615 12.1409C42.9732 11.7942 39.4849 11.5342 35.9749 11.3392V11.3176L35.4982 8.50091C35.1732 6.50758 34.6965 3.51758 29.6265 3.51758H23.9499C18.9015 3.51758 18.4249 6.37758 18.0782 8.47924L17.6232 11.2526C15.6082 11.3826 13.5932 11.5126 11.5782 11.7076L7.1582 12.1409C6.2482 12.2276 5.5982 13.0292 5.68487 13.9176C5.77154 14.8059 6.55154 15.4559 7.46154 15.3692L11.8815 14.9359C23.2349 13.8092 34.6749 14.2426 46.1582 15.3909C46.2232 15.3909 46.2665 15.3909 46.3315 15.3909C47.1549 15.3909 47.8699 14.7626 47.9565 13.9176C48.0215 13.0292 47.3715 12.2276 46.4615 12.1409Z" fill="#B71C1C"/>
<path opacity="0.3991" d="M42.4748 18.4466C41.9548 17.9049 41.2398 17.6016 40.5032 17.6016H13.1165C12.3798 17.6016 11.6432 17.9049 11.1448 18.4466C10.6465 18.9882 10.3648 19.7249 10.4082 20.4832L11.7515 42.7132C11.9898 46.0066 12.2932 50.1232 19.8548 50.1232H33.7648C41.3265 50.1232 41.6298 46.0282 41.8682 42.7132L43.2115 20.5049C43.2548 19.7249 42.9732 18.9882 42.4748 18.4466Z" fill="#B71C1C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.5664 37.6426C21.5664 36.7451 22.2939 36.0176 23.1914 36.0176H30.4064C31.3039 36.0176 32.0314 36.7451 32.0314 37.6426C32.0314 38.54 31.3039 39.2676 30.4064 39.2676H23.1914C22.2939 39.2676 21.5664 38.54 21.5664 37.6426Z" fill="#B71C1C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7682 28.9766C19.7682 28.0791 20.4957 27.3516 21.3932 27.3516H32.2265C33.124 27.3516 33.8515 28.0791 33.8515 28.9766C33.8515 29.874 33.124 30.6016 32.2265 30.6016H21.3932C20.4957 30.6016 19.7682 29.874 19.7682 28.9766Z" fill="#B71C1C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -49,4 +49,5 @@ class ApiConfig {
/// Body: FormData
/// Headers: {'Authorization': 'Bearer <token>'}
static String editDiscount(String id) => '$baseUrl/discount/edit/$id';
static String deleteDiscount(String id) => '$baseUrl/discount/delete/$id';
}

View File

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

View File

@ -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<String, dynamic> json) {
// A helper function to safely extract lists
List<String> _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,
);

View File

@ -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<String> 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,
];

View File

@ -45,7 +45,7 @@ class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
images = (data['Images'] as List)
.map<Map<String, String?>>(
(img) => {
'id': img['_id'] as String?,
'id': img['ID'] as String?,
'url': img['Url'] as String?,
},
)
@ -208,10 +208,9 @@ class DiscountBloc extends Bloc<DiscountEvent, DiscountState> {
}
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);

View File

@ -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<DiscountManagementEvent, DiscountManagementState> {
final Dio _dio = Dio();
final TokenStorageService _tokenStorage = TokenStorageService();
DiscountManagementBloc() : super(DiscountManagementInitial()) {
_dio.interceptors.add(LoggingInterceptor());
on<FetchManagedDiscounts>((event, emit) async {
await _fetchDiscounts(emit, status: event.status);
});
on<SearchManagedDiscounts>((event, emit) async {
await _fetchDiscounts(emit, searchQuery: event.query, status: event.status);
});
// **NEW EVENT HANDLER**
on<DeleteDiscount>((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<void> _fetchDiscounts(Emitter<DiscountManagementState> 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<String, dynamic> 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<dynamic> 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()}'));
}
}
}

View File

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

View File

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

View File

@ -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<HomeEvent, HomeState> {
String url = ApiConfig.getActiveDiscounts;
if (searchQuery != null && searchQuery.isNotEmpty) {
url = '$url&search=$searchQuery';
url = '$url?search=$searchQuery';
}
final response = await _dio.get(

View File

@ -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<DiscountBloc, DiscountState>(
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),
),
],
),
],
),
),
),
),
);
}
}

View File

@ -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<DiscountManegment> createState() => _DiscountManegmentState();
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DiscountManagementBloc(),
child: const _DiscountManegmentView(),
);
}
}
class _DiscountManegmentState extends State<DiscountManegment> {
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<HomeBloc>().add(FetchDiscounts());
context
.read<DiscountManagementBloc>()
.add(FetchManagedDiscounts(status: _selectedStatus));
}
});
}
@ -33,7 +49,7 @@ class _DiscountManegmentState extends State<DiscountManegment> {
@override
void dispose() {
_searchController.dispose();
_debounce?.cancel(); // کنسل کردن تایمر برای جلوگیری از نشت حافظه
_debounce?.cancel();
super.dispose();
}
@ -41,7 +57,8 @@ class _DiscountManegmentState extends State<DiscountManegment> {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
if (mounted) {
context.read<HomeBloc>().add(SearchDiscounts(query: query));
context.read<DiscountManagementBloc>().add(
SearchManagedDiscounts(query: query, status: _selectedStatus));
}
});
}
@ -50,67 +67,207 @@ class _DiscountManegmentState extends State<DiscountManegment> {
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBarSingle(
page: "رزرو ها",
page: "تخفیف ها",
),
body: Column(
body: BlocListener<DiscountManagementBloc, DiscountManagementState>(
listener: (context, state) {
if (state is DiscountDeleteSuccess) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: Colors.green),
);
context
.read<DiscountManagementBloc>()
.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<HomeBloc, HomeState>(
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<HomeBloc>().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<DiscountManagementBloc>()
.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<DiscountManagementBloc, DiscountManagementState>(
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<String, List<DiscountEntity>> 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<DiscountManagementBloc>()
.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<DiscountManagementBloc>(),
child: AnalyticsDiscountCard(discount: discount),
);
}).toList(),
],
);
},
),
);
}
return const Center(child: CircularProgressIndicator());
},
),
);
}
}

View File

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

View File

@ -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<ReserveManegmment> createState() => _ReserveManegmmentState();
}
class _ReserveManegmmentState extends State<ReserveManegmment> {
final TextEditingController _searchController = TextEditingController();
Timer? _debounce;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.read<HomeBloc>().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<HomeBloc>().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<HomeBloc, HomeState>(
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<HomeBloc>().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());
},
),
),
],
),
);
}
}

View File

@ -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<DiscountManagementBloc>();
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<DiscountManagementBloc>()
.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<Widget> 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(),
);
}
}

View File

@ -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<HomeBloc, HomeState>(
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);
}
}

View File

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

View File

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