added comment
This commit is contained in:
parent
608222e8a3
commit
ce62c567c5
|
|
@ -8,4 +8,6 @@ class ApiConfig {
|
|||
static const String addReservation = "/reservation/add";
|
||||
static const String getReservations = "/reservation/get";
|
||||
static const String updateFcmToken = "/user/firebaseUpdate";
|
||||
}
|
||||
static const String addComment = "/comment/add";
|
||||
static const String getComments = "/comment/get/";
|
||||
}
|
||||
|
|
@ -23,13 +23,18 @@ class CommentModel extends Equatable {
|
|||
List<Object?> get props => [id, userName, rating, comment, publishedAt, uploadedImageUrls];
|
||||
|
||||
factory CommentModel.fromJson(Map<String, dynamic> json) {
|
||||
final List<String> images = (json['UserImages'] as List<dynamic>?)
|
||||
?.map((image) => image['Url'] as String)
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
return CommentModel(
|
||||
id: json['id'],
|
||||
userName: json['userName'],
|
||||
rating: (json['rating'] as num).toDouble(),
|
||||
comment: json['comment'],
|
||||
publishedAt: DateTime.parse(json['publishedAt']),
|
||||
uploadedImageUrls: List<String>.from(json['uploadedImageUrls'] ?? []),
|
||||
id: json['ID'] ?? '',
|
||||
userName: json['User']?['Name'] ?? 'کاربر ناشناس',
|
||||
rating: (json['Score'] as num?)?.toDouble() ?? 0.0,
|
||||
comment: json['Text'] ?? '',
|
||||
publishedAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
uploadedImageUrls: images,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
|
|||
import 'package:proxibuy/core/config/http_overrides.dart';
|
||||
import 'package:proxibuy/firebase_options.dart';
|
||||
import 'package:proxibuy/presentation/auth/bloc/auth_bloc.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_bloc.dart'; // این ایمپورت اضافه شد
|
||||
import 'package:proxibuy/presentation/notification_preferences/bloc/notification_preferences_bloc.dart';
|
||||
import 'package:proxibuy/presentation/offer/bloc/offer_bloc.dart';
|
||||
import 'package:proxibuy/presentation/reservation/cubit/reservation_cubit.dart';
|
||||
|
|
@ -26,16 +27,6 @@ void main() async {
|
|||
return token;
|
||||
}
|
||||
|
||||
// FirebaseMessaging.instance.onTokenRefresh
|
||||
// .listen((fcmToken) {
|
||||
// // TODO: If necessary send token to application server.
|
||||
// // Note: This callback is fired at each app startup and whenever a new
|
||||
// // token is generated.
|
||||
// })
|
||||
// .onError((err) {
|
||||
// // Error getting token.
|
||||
// });
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await initializeService();
|
||||
|
|
@ -62,6 +53,9 @@ class MyApp extends StatelessWidget {
|
|||
BlocProvider<NotificationPreferencesBloc>(
|
||||
create: (context) => NotificationPreferencesBloc(),
|
||||
),
|
||||
BlocProvider<CommentBloc>(
|
||||
create: (context) => CommentBloc(),
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Proxibuy',
|
||||
|
|
@ -140,4 +134,4 @@ class MyApp extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:proxibuy/core/config/api_config.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class CommentBloc extends Bloc<CommentEvent, CommentState> {
|
||||
late final Dio _dio;
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
CommentBloc() : super(CommentInitial()) {
|
||||
_dio = Dio();
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
logPrint: (obj) => debugPrint(obj.toString()),
|
||||
),
|
||||
);
|
||||
on<SubmitComment>(_onSubmitComment);
|
||||
}
|
||||
|
||||
Future<void> _onSubmitComment(SubmitComment event, Emitter<CommentState> emit) async {
|
||||
if (event.text.isEmpty && event.score == 0) {
|
||||
emit(const CommentSubmissionFailure("لطفا امتیاز یا نظری برای این تخفیف ثبت کنید."));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(CommentSubmitting());
|
||||
try {
|
||||
final token = await _storage.read(key: 'accessToken');
|
||||
if (token == null) {
|
||||
emit(const CommentSubmissionFailure("شما وارد نشدهاید."));
|
||||
return;
|
||||
}
|
||||
|
||||
final formData = FormData.fromMap({
|
||||
'Discount': event.discountId,
|
||||
'Text': event.text,
|
||||
'Score': event.score,
|
||||
});
|
||||
|
||||
for (File imageFile in event.images) {
|
||||
formData.files.add(MapEntry(
|
||||
'Images',
|
||||
await MultipartFile.fromFile(
|
||||
imageFile.path,
|
||||
filename: imageFile.path.split('/').last,
|
||||
contentType: MediaType('image', 'jpeg'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
final response = await _dio.post(
|
||||
ApiConfig.baseUrl + ApiConfig.addComment,
|
||||
data: formData,
|
||||
options: Options(
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
emit(CommentSubmissionSuccess());
|
||||
} else {
|
||||
emit(CommentSubmissionFailure(response.data['message'] ?? 'خطا در ارسال نظر'));
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
emit(CommentSubmissionFailure(e.response?.data['message'] ?? 'خطا در ارتباط با سرور'));
|
||||
} catch (e) {
|
||||
emit(CommentSubmissionFailure('خطایی ناشناخته رخ داد: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import 'dart:io';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class CommentEvent extends Equatable {
|
||||
const CommentEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class SubmitComment extends CommentEvent {
|
||||
final String discountId;
|
||||
final String text;
|
||||
final double score;
|
||||
final List<File> images;
|
||||
|
||||
const SubmitComment({
|
||||
required this.discountId,
|
||||
required this.text,
|
||||
required this.score,
|
||||
required this.images,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [discountId, text, score, images];
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class CommentState extends Equatable {
|
||||
const CommentState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CommentInitial extends CommentState {}
|
||||
|
||||
class CommentSubmitting extends CommentState {}
|
||||
|
||||
class CommentSubmissionSuccess extends CommentState {}
|
||||
|
||||
class CommentSubmissionFailure extends CommentState {
|
||||
final String error;
|
||||
|
||||
const CommentSubmissionFailure(this.error);
|
||||
|
||||
@override
|
||||
List<Object> get props => [error];
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ class AddPhotoScreen extends StatelessWidget {
|
|||
required this.offer,
|
||||
});
|
||||
|
||||
// متد ساخت توکن
|
||||
Future<String> _generateQrToken(BuildContext context) async {
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_bloc.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_event.dart';
|
||||
import 'package:proxibuy/presentation/comment/bloc/comment_state.dart';
|
||||
import 'package:proxibuy/presentation/pages/offers_page.dart';
|
||||
|
||||
class CommentPage extends StatefulWidget {
|
||||
final String discountId;
|
||||
|
||||
const CommentPage({super.key, required this.discountId});
|
||||
|
||||
@override
|
||||
State<CommentPage> createState() => _CommentPageState();
|
||||
}
|
||||
|
||||
class _CommentPageState extends State<CommentPage> {
|
||||
final _commentController = TextEditingController();
|
||||
double _rating = 0.0;
|
||||
final List<File> _images = [];
|
||||
final ImagePicker _picker = ImagePicker();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_commentController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _pickImage(ImageSource source) async {
|
||||
if (_images.length >= 2) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('شما فقط میتوانید ۲ عکس اضافه کنید.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final pickedFile = await _picker.pickImage(source: source, imageQuality: 80);
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
_images.add(File(pickedFile.path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showImageSourceActionSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||
),
|
||||
builder: (context) {
|
||||
return SafeArea(
|
||||
child: Wrap(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library, color: AppColors.primary),
|
||||
title: const Text('انتخاب از گالری'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickImage(ImageSource.gallery);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.camera_alt, color: AppColors.primary),
|
||||
title: const Text('گرفتن عکس با دوربین'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
_pickImage(ImageSource.camera);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CommentBloc(),
|
||||
child: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(Assets.images.userinfo.path, fit: BoxFit.cover),
|
||||
),
|
||||
DraggableScrollableSheet(
|
||||
initialChildSize: 0.65,
|
||||
minChildSize: 0.65,
|
||||
maxChildSize: 0.65,
|
||||
builder: (context, scrollController) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(32)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'خریدت با موفقیت انجام شد. منتظر دیدار دوبارهات هستیم. لطفا نظرت رو در مورد این تخفیف بهمون بگو.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: RatingBar.builder(
|
||||
initialRating: 0,
|
||||
minRating: 1,
|
||||
direction: Axis.horizontal,
|
||||
itemCount: 5,
|
||||
itemPadding: const EdgeInsets.symmetric(horizontal: 15.0),
|
||||
itemBuilder: (context, _) => Icon(
|
||||
Icons.star,
|
||||
color: Colors.amber.shade700,
|
||||
),
|
||||
onRatingUpdate: (rating) => setState(() => _rating = rating),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: _commentController,
|
||||
maxLines: 4,
|
||||
textAlign: TextAlign.right,
|
||||
decoration: InputDecoration(
|
||||
labelText: "گوشمون به شماست",
|
||||
hintText: "نظراتت رو بگو...",
|
||||
alignLabelWithHint: true,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: IconButton(
|
||||
icon: SvgPicture.asset(
|
||||
Assets.icons.galleryAdd.path,
|
||||
color: _images.length >= 2 ? Colors.grey : AppColors.primary,
|
||||
),
|
||||
onPressed: _images.length >= 2 ? null : _showImageSourceActionSheet,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_images.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _images.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Image.file(
|
||||
_images[index],
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return BlocConsumer<CommentBloc, CommentState>(
|
||||
listener: (context, state) {
|
||||
if (state is CommentSubmissionSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('نظر شما با موفقیت ثبت شد. ممنونیم!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const OffersPage()),
|
||||
(route) => false,
|
||||
);
|
||||
} else if (state is CommentSubmissionFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.error), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isLoading = state is CommentSubmitting;
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
context.read<CommentBloc>().add(
|
||||
SubmitComment(
|
||||
discountId: widget.discountId,
|
||||
text: _commentController.text,
|
||||
score: _rating,
|
||||
images: _images,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.confirm,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(50)),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
)
|
||||
: const Text('ارسال'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const OffersPage()),
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
child: const Text('رد شدن', style: TextStyle(color: Colors.black)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
|
@ -11,6 +12,7 @@ import 'package:maps_launcher/maps_launcher.dart';
|
|||
import 'package:proxibuy/core/config/api_config.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/data/models/comment_model.dart';
|
||||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
import 'package:proxibuy/presentation/pages/add_photo_screen.dart';
|
||||
import 'package:proxibuy/presentation/pages/reservation_details_screen.dart';
|
||||
|
|
@ -87,20 +89,9 @@ class ProductDetailPage extends StatelessWidget {
|
|||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
debugPrint("----------- REQUEST-----------");
|
||||
debugPrint("URL: POST $url");
|
||||
debugPrint("Headers: ${options.headers}");
|
||||
debugPrint("Body: $data");
|
||||
debugPrint("-----------------------------");
|
||||
|
||||
final response =
|
||||
await dio.post(url, data: data, options: options);
|
||||
|
||||
debugPrint("---------- RESPONSE-----------");
|
||||
debugPrint("StatusCode: ${response.statusCode}");
|
||||
debugPrint("Data: ${response.data}");
|
||||
debugPrint("-----------------------------");
|
||||
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
|
|
@ -131,11 +122,6 @@ class ProductDetailPage extends StatelessWidget {
|
|||
} on DioException catch (e) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
|
||||
debugPrint("---------- ERROR-----------");
|
||||
debugPrint("StatusCode: ${e.response?.statusCode}");
|
||||
debugPrint("Data: ${e.response?.data}");
|
||||
debugPrint("--------------------------");
|
||||
|
||||
final errorMessage = e.response?.data?['message'] ??
|
||||
'خطای سرور هنگام رزرو. لطفاً دوباره تلاش کنید.';
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
@ -145,10 +131,6 @@ class ProductDetailPage extends StatelessWidget {
|
|||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
debugPrint("---------- GENERAL ERROR -----------");
|
||||
debugPrint(e.toString());
|
||||
debugPrint("------------------------------------");
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(e.toString()),
|
||||
|
|
@ -205,14 +187,56 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
late List<String> imageList;
|
||||
late String selectedImage;
|
||||
final String _uploadKey = 'upload_image';
|
||||
late Future<List<CommentModel>> _commentsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
imageList = List.from(widget.offer.imageUrls)..add(_uploadKey);
|
||||
selectedImage = imageList.first;
|
||||
selectedImage = imageList.isNotEmpty ? imageList.first : 'https://via.placeholder.com/400x200.png?text=No+Image';
|
||||
_commentsFuture = _fetchComments();
|
||||
}
|
||||
|
||||
Future<List<CommentModel>> _fetchComments() async {
|
||||
// 1. توکن را از حافظه امن بخوان
|
||||
const storage = FlutterSecureStorage();
|
||||
final token = await storage.read(key: 'accessToken');
|
||||
|
||||
// 2. اگر توکن وجود نداشت، خطا برگردان
|
||||
// هرچند کاربر لاگین نکرده معمولا به این صفحه دسترسی ندارد
|
||||
if (token == null) {
|
||||
throw Exception('Authentication token not found!');
|
||||
}
|
||||
|
||||
try {
|
||||
final dio = Dio();
|
||||
// 3. هدر Authorization را به درخواست اضافه کن
|
||||
final response = await dio.get(
|
||||
ApiConfig.baseUrl + ApiConfig.getComments + widget.offer.id,
|
||||
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> commentsJson = response.data['data']['comments'];
|
||||
return commentsJson.map((json) => CommentModel.fromJson(json)).toList();
|
||||
} else {
|
||||
// خطاهای دیگر سرور
|
||||
throw Exception('Failed to load comments with status code: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// چاپ خطای کامل Dio برای دیباگ بهتر
|
||||
debugPrint("DioException fetching comments: $e");
|
||||
if (e.response != null) {
|
||||
debugPrint("Response data: ${e.response?.data}");
|
||||
}
|
||||
throw Exception('Failed to load comments: ${e.message}');
|
||||
} catch (e) {
|
||||
debugPrint("Error fetching comments: $e");
|
||||
throw Exception('An unknown error occurred: $e');
|
||||
}
|
||||
}
|
||||
// ############ END: FIX SECTION ############
|
||||
|
||||
void _launchMaps(double lat, double lon, String title) {
|
||||
MapsLauncher.launchCoordinates(lat, lon, title);
|
||||
}
|
||||
|
|
@ -405,8 +429,7 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
? widget.offer.expiryTime.difference(DateTime.now())
|
||||
: Duration.zero;
|
||||
|
||||
final formatCurrency =
|
||||
NumberFormat.decimalPattern('fa_IR'); // Or 'en_US'
|
||||
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0)
|
||||
|
|
@ -687,7 +710,26 @@ class _ProductDetailViewState extends State<ProductDetailView> {
|
|||
const SizedBox(height: 24),
|
||||
_buildDiscountTypeSection(),
|
||||
const SizedBox(height: 24),
|
||||
CommentsSection(comments: widget.offer.comments),
|
||||
FutureBuilder<List<CommentModel>>(
|
||||
future: _commentsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Center(child: Text('خطا در بارگذاری نظرات. لطفاً صفحه را رفرش کنید.'));
|
||||
}
|
||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||
child: Text('هنوز نظری برای این تخفیف ثبت نشده است.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return CommentsSection(comments: snapshot.data!);
|
||||
},
|
||||
),
|
||||
].animate(interval: 80.ms).slideX(begin: -0.05).fadeIn(
|
||||
duration: 400.ms,
|
||||
curve: Curves.easeOut,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
import 'package:proxibuy/presentation/pages/comment_page.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
|
|
@ -30,17 +33,17 @@ class _ReservationConfirmationPageState
|
|||
Timer? _timer;
|
||||
Duration _remaining = Duration.zero;
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
StreamSubscription? _mqttSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_playSound();
|
||||
|
||||
_calculateRemainingTime();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_calculateRemainingTime();
|
||||
});
|
||||
_listenToMqtt();
|
||||
}
|
||||
|
||||
void _playSound() async {
|
||||
|
|
@ -69,14 +72,46 @@ class _ReservationConfirmationPageState
|
|||
}
|
||||
}
|
||||
|
||||
void _listenToMqtt() async {
|
||||
final mqttService = context.read<MqttService>();
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
final discountId = widget.offer.id;
|
||||
|
||||
if (userID == null) {
|
||||
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
final topic = 'user-order/$userID/$discountId';
|
||||
mqttService.subscribe(topic);
|
||||
debugPrint("✅ Subscribed to MQTT topic: $topic");
|
||||
|
||||
_mqttSubscription = mqttService.messages.listen((message) {
|
||||
debugPrint("✅ MQTT Message received on details page: $message");
|
||||
final receivedDiscountId = message['Discount'];
|
||||
if (receivedDiscountId == discountId) {
|
||||
if (mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CommentPage(discountId: discountId),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_audioPlayer.dispose();
|
||||
_mqttSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Directionality(
|
||||
|
|
@ -94,13 +129,13 @@ class _ReservationConfirmationPageState
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'تخفیف ${widget.offer.discountType} رزرو شد!',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
)
|
||||
'تخفیف ${widget.offer.discountType} رزرو شد!',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(delay: 300.ms, duration: 500.ms)
|
||||
.slideY(begin: -0.2, end: 0),
|
||||
|
|
@ -227,7 +262,7 @@ class _ReservationConfirmationPageState
|
|||
SvgPicture.asset(Assets.icons.ticketDiscount.path),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'(${(100-widget.offer.finalPrice/widget.offer.originalPrice*100).toInt()}%)',
|
||||
'(${(100 - widget.offer.finalPrice / widget.offer.originalPrice * 100).toInt()}%)',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.singleOfferType,
|
||||
|
|
@ -401,4 +436,4 @@ class _ReservationConfirmationPageState
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:proxibuy/core/gen/assets.gen.dart';
|
||||
import 'package:proxibuy/presentation/pages/comment_page.dart';
|
||||
import 'package:proxibuy/services/mqtt_service.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:proxibuy/core/config/app_colors.dart';
|
||||
import 'package:proxibuy/data/models/offer_model.dart';
|
||||
|
|
@ -25,6 +29,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
Timer? _timer;
|
||||
Duration _remaining = Duration.zero;
|
||||
Future<String>? _qrTokenFuture;
|
||||
StreamSubscription? _mqttSubscription; // برای مدیریت لیسنر MQTT
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -57,10 +62,52 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
}
|
||||
|
||||
void _toggleExpansion() {
|
||||
final isExpired = _remaining <= Duration.zero;
|
||||
if (isExpired) return;
|
||||
|
||||
setState(() {
|
||||
_isExpanded = !_isExpanded;
|
||||
if (_isExpanded && _qrTokenFuture == null) {
|
||||
_qrTokenFuture = _generateQrToken();
|
||||
if (_isExpanded) {
|
||||
if (_qrTokenFuture == null) {
|
||||
_qrTokenFuture = _generateQrToken();
|
||||
}
|
||||
_listenToMqtt();
|
||||
} else {
|
||||
_mqttSubscription?.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _listenToMqtt() async {
|
||||
final mqttService = context.read<MqttService>();
|
||||
const storage = FlutterSecureStorage();
|
||||
final userID = await storage.read(key: 'userID');
|
||||
final discountId = widget.offer.id;
|
||||
|
||||
if (userID == null) {
|
||||
debugPrint("MQTT Listener: UserID not found, cannot subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
final topic = 'user-order/$userID/$discountId';
|
||||
mqttService.subscribe(topic);
|
||||
debugPrint("✅ Subscribed to MQTT topic: $topic");
|
||||
|
||||
|
||||
_mqttSubscription = mqttService.messages.listen((message) {
|
||||
debugPrint("✅ MQTT Message received on reserved card: $message");
|
||||
final receivedDiscountId = message['Discount'];
|
||||
|
||||
if (receivedDiscountId == discountId) {
|
||||
if (mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CommentPage(discountId: discountId),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -84,6 +131,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_mqttSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -104,24 +152,47 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
margin: EdgeInsets.zero,
|
||||
child: _buildOfferPrimaryDetails(),
|
||||
),
|
||||
_buildActionsRow(),
|
||||
_buildExpansionPanel(),
|
||||
_buildActionsRow(isExpired),
|
||||
if (!isExpired) _buildExpansionPanel(),
|
||||
],
|
||||
);
|
||||
|
||||
if (isExpired) {
|
||||
return ColorFiltered(
|
||||
colorFilter: const ColorFilter.matrix(<double>[
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]),
|
||||
child: cardContent,
|
||||
);
|
||||
}
|
||||
|
||||
return cardContent;
|
||||
return Stack(
|
||||
children: [
|
||||
if (isExpired)
|
||||
ColorFiltered(
|
||||
colorFilter: const ColorFilter.matrix(<double>[
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0.2126, 0.7152, 0.0722, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]),
|
||||
child: cardContent,
|
||||
)
|
||||
else
|
||||
cardContent,
|
||||
if (isExpired)
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: -35,
|
||||
child: Transform.rotate(
|
||||
angle: -45 * (3.1415926535 / 180),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 4),
|
||||
color: Colors.red,
|
||||
child: const Text(
|
||||
'منقضی شده',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOfferPrimaryDetails() {
|
||||
|
|
@ -195,13 +266,13 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildActionsRow() {
|
||||
Widget _buildActionsRow(bool isExpired) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (_remaining > Duration.zero)
|
||||
if (!isExpired)
|
||||
Column(
|
||||
children: [
|
||||
Localizations.override(
|
||||
|
|
@ -220,7 +291,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
fontSize: 20,
|
||||
color: AppColors.countdown,
|
||||
),
|
||||
decoration: const BoxDecoration(color: Colors.white),
|
||||
decoration: const BoxDecoration(color: Colors.transparent),
|
||||
shouldShowDays: (d) => d.inDays > 0,
|
||||
shouldShowHours: (d) => d.inHours > 0,
|
||||
shouldShowMinutes: (d) => d.inSeconds > 0,
|
||||
|
|
@ -231,36 +302,30 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
],
|
||||
)
|
||||
else
|
||||
const Text(
|
||||
'منقضی شده',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
const SizedBox(height: 0),
|
||||
SizedBox(width: 10),
|
||||
if (!isExpired)
|
||||
TextButton(
|
||||
onPressed: _toggleExpansion,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_isExpanded ? 'بستن' : 'اطلاعات بیشتر',
|
||||
style: TextStyle(color: AppColors.active),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SvgPicture.asset(
|
||||
Assets.icons.arrowDown.path,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
TextButton(
|
||||
onPressed: _toggleExpansion,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_isExpanded ? 'بستن' : 'اطلاعات بیشتر',
|
||||
style: TextStyle(color: AppColors.active),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
AnimatedRotation(
|
||||
turns: _isExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: SvgPicture.asset(
|
||||
Assets.icons.arrowDown.path,
|
||||
height: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -324,6 +389,7 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
|
||||
Widget _buildExpansionPanel() {
|
||||
final formatCurrency = NumberFormat.decimalPattern('fa_IR');
|
||||
final isExpired = _remaining <= Duration.zero;
|
||||
|
||||
return AnimatedCrossFade(
|
||||
firstChild: Container(),
|
||||
|
|
@ -395,39 +461,41 @@ class _ReservedListItemCardState extends State<ReservedListItemCard> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, 246, 246, 246),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
if (!isExpired) ...[
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, 246, 246, 246),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: FutureBuilder<String>(
|
||||
future: _qrTokenFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: Text("خطا در ساخت کد QR")),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return QrImageView(
|
||||
data: snapshot.data!,
|
||||
version: QrVersions.auto,
|
||||
size: 280.0,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
child: FutureBuilder<String>(
|
||||
future: _qrTokenFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return const SizedBox(
|
||||
height: 280.0,
|
||||
child: Center(child: Text("خطا در ساخت کد QR")),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasData) {
|
||||
return QrImageView(
|
||||
data: snapshot.data!,
|
||||
version: QrVersions.auto,
|
||||
size: 280.0,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class CustomStarRating extends StatelessWidget {
|
|||
stars.add(_buildStar(Assets.icons.star2.path));
|
||||
remaining = 0;
|
||||
} else {
|
||||
stars.add(_buildStar(Assets.icons.starHalf.path,));
|
||||
stars.add(_buildStar(Assets.icons.star2.path,));
|
||||
}
|
||||
}
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: stars);
|
||||
|
|
|
|||
|
|
@ -739,6 +739,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.28"
|
||||
flutter_rating_bar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_rating_bar
|
||||
sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ dependencies:
|
|||
workmanager: ^0.7.0
|
||||
firebase_messaging: ^15.2.10
|
||||
firebase_crashlytics: ^4.3.10
|
||||
flutter_rating_bar: ^4.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in New Issue