taxgilde/lib/view/screens/history/detail_screen.dart

1974 lines
101 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/app_style.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/consts/details_webscokect.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/consts/validation_popup.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/controller/api_repository.dart';
import 'package:taxglide/model/detail_model.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart';
import 'package:taxglide/view/screens/history/completed_live_chat_screen.dart';
import 'package:taxglide/services/razorpay_service.dart';
import 'package:razorpay_flutter/razorpay_flutter.dart';
class DetailScreen extends ConsumerStatefulWidget {
final int id;
final int? sourceTabIndex;
const DetailScreen({super.key, required this.id, this.sourceTabIndex});
@override
ConsumerState<DetailScreen> createState() => _DetailScreenState();
}
class _DetailScreenState extends ConsumerState<DetailScreen> {
Map<String, bool> _downloadingFiles = {}; // Track each file separately
bool _isDownloadingInvoice = false;
String? _downloadedInvoicePath;
bool _isWebSocketInitialized = false; // Add this flag
late RazorpayService _razorpayService;
@override
void initState() {
super.initState();
_razorpayService = RazorpayService(
onSuccess: _handlePaymentSuccess,
onFailure: _handlePaymentFailure,
);
_initializeWebSocket(); // Initialize WebSocket
}
void _handlePaymentSuccess(PaymentSuccessResponse response) {
ValidationPopup().showSuccessMessage(
context,
"Payment successful! ID: ${response.paymentId}",
);
ref.invalidate(serviceDetailProvider(widget.id));
}
void _handlePaymentFailure(PaymentFailureResponse response) {
ValidationPopup().showErrorMessage(
context,
"Payment failed: ${response.message}",
);
}
// Add this method
Future<void> _initializeWebSocket() async {
if (_isWebSocketInitialized) return;
try {
String? userId = await LocalStore().getUserId();
if (userId != null && userId.isNotEmpty) {
final wsService = DetailsWebscokect();
// Setup message handler to refresh count
wsService.onMessageReceived = (message) {
debugPrint("📨 WebSocket message received: $message");
// Refresh count when message arrives
_refreshCount();
};
// Connect WebSocket
await wsService.connect(userId: userId);
_isWebSocketInitialized = true;
debugPrint("✅ WebSocket initialized for detail screen");
}
} catch (e) {
debugPrint("❌ Failed to initialize WebSocket: $e");
}
}
// Add this method to refresh count
void _refreshCount() {
// Get the chatId from the detail model
final detailAsync = ref.read(serviceDetailProvider(widget.id));
detailAsync.whenData((detailModel) {
final chatId = detailModel.data?.chatId;
if (chatId != null && chatId.isNotEmpty) {
// Invalidate the count provider to trigger refresh
ref.invalidate(countProvider(chatId));
debugPrint("🔄 Count refreshed for chatId: $chatId");
}
});
}
@override
void dispose() {
_razorpayService.dispose();
// Optionally disconnect WebSocket when leaving screen
// DetailsWebscokect().disconnect();
super.dispose();
}
Future<void> _handleDownload(String url) async {
if (_downloadingFiles[url] == true) return;
setState(() {
_downloadingFiles[url] = true;
});
try {
// Check if file exists, if yes open it, if no download it
final result = await DownloadHelper.downloadFile(url);
// No snackbar for downloads - only notification will show
// For existing files (view mode), no messages shown at all
// If there's an error, still show it
if (mounted && result['success'] == false) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${result['error']}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) {
setState(() {
_downloadingFiles[url] = false;
});
}
}
}
Future<void> _handleInvoiceDownload(String invoiceUrl) async {
if (_isDownloadingInvoice) return;
// Check if already downloaded
final existingPath = await DownloadHelper.checkFileExists(invoiceUrl);
if (existingPath != null) {
setState(() {
_downloadedInvoicePath = existingPath;
});
// Open the existing file
await DownloadHelper.openDownloadedFile(existingPath);
return;
}
setState(() {
_isDownloadingInvoice = true;
});
try {
final result = await DownloadHelper.downloadFile(invoiceUrl);
if (mounted && result['success'] == true) {
setState(() {
_downloadedInvoicePath = result['filePath'];
});
} else if (mounted && result['success'] == false) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Download failed: ${result['error']}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) {
setState(() {
_isDownloadingInvoice = false;
});
}
}
}
Widget _buildInvoiceDownloadButton(String? invoiceUrl, double width) {
if (invoiceUrl == null || invoiceUrl.isEmpty) {
return SizedBox(
width: double.infinity,
height: 57,
child: ElevatedButton.icon(
onPressed: null,
icon: const Icon(Icons.error_outline, color: Colors.white, size: 22),
label: Text(
"Invoice Not Available",
textAlign: TextAlign.center,
style: AppTextStyles.regular.copyWith(
fontSize: width * 0.045,
height: 1.3,
letterSpacing: 0.04 * 17.64,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
disabledBackgroundColor: Colors.grey,
),
),
);
}
return FutureBuilder<String?>(
future: DownloadHelper.checkFileExists(invoiceUrl),
builder: (context, snapshot) {
final isAlreadyDownloaded =
snapshot.data != null || _downloadedInvoicePath != null;
return SizedBox(
width: double.infinity,
height: 57,
child: ElevatedButton.icon(
onPressed: _isDownloadingInvoice
? null
: () => _handleInvoiceDownload(invoiceUrl),
icon: _isDownloadingInvoice
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Icon(
isAlreadyDownloaded
? Icons.visibility
: Icons.download_rounded,
color: Colors.white,
size: 22,
),
label: Text(
_isDownloadingInvoice
? "Downloading..."
: isAlreadyDownloaded
? "View Invoice"
: "Download Invoice",
textAlign: TextAlign.center,
style: AppTextStyles.regular.copyWith(
fontSize: width * 0.045,
height: 1.3,
letterSpacing: 0.04 * 17.64,
color: Colors.white,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: isAlreadyDownloaded
? const Color(0xFF0C5A78)
: const Color(0xFF1E780C),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
elevation: 7.8,
shadowColor: const Color(0x66000000),
disabledBackgroundColor: Colors.grey,
),
),
);
},
);
}
Widget _buildProformaDownloadButton(String? proforma) {
final bool isDisabled = proforma == null || proforma.isEmpty;
return FutureBuilder<String?>(
future: isDisabled ? null : DownloadHelper.checkFileExists(proforma),
builder: (context, snapshot) {
final bool isDownloaded = snapshot.data != null;
return GestureDetector(
onTap: isDisabled
? null
: () async {
if (isDownloaded) {
/// 👀 View Proforma (OPEN LOCAL FILE)
await DownloadHelper.openDownloadedFile(snapshot.data!);
} else {
/// ⬇️ Download Proforma
await _handleInvoiceDownload(proforma);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
/// 🔵 Icon
Container(
width: 38.45,
height: 38.45,
decoration: const BoxDecoration(
color: Color(0xFF4000FF),
shape: BoxShape.circle,
),
child: Icon(
isDownloaded ? Icons.visibility : Icons.download_rounded,
color: Colors.white,
size: 22,
),
),
const SizedBox(width: 5),
/// 🔤 Text
Text(
isDisabled
? "Proforma Not Available"
: isDownloaded
? "View Proforma"
: "Download Proforma",
style: AppTextStyles.semiBold.copyWith(
fontSize: 12,
height: 1.3,
letterSpacing: 0.04 * 14.47,
color: isDisabled ? Colors.grey : const Color(0xFF4000FF),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
final validationPopup = ValidationPopup();
final detailAsync = ref.watch(serviceDetailProvider(widget.id));
// 🔄 Silent refresh when notification trigger changes
ref.listen(notificationTriggerProvider, (previous, next) {
if (previous != null && next != previous) {
debugPrint("🔔 Silent refresh triggered for DetailScreen");
ref.refresh(serviceDetailProvider(widget.id));
// Also refresh unread count if available
detailAsync.whenData((model) {
final chatId = model.data?.chatId;
if (chatId != null && chatId.isNotEmpty) {
ref.refresh(countProvider(chatId));
}
});
}
});
return Scaffold(
body: Container(
height: height,
width: width,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.backgroundimages),
fit: BoxFit.cover,
),
),
child: SafeArea(
child: detailAsync.when(
skipLoadingOnRefresh: true,
loading: () => const Center(
child: CircularProgressIndicator(color: Colors.deepPurple),
),
error: (e, _) => Center(
child: Text(
"Error: $e",
style: AppTextStyles.regular.copyWith(
color: Colors.red,
fontSize: 16,
),
),
),
data: (DetailModel detailModel) {
final data = detailModel.data;
if (data == null) {
return const Center(child: Text("No details found"));
}
final countAsync = data.chatId != null && data.chatId!.isNotEmpty
? ref.watch(countProvider(data.chatId!))
: null;
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// 🔹 Custom App Bar
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 21,
),
child: SizedBox(
height: 50,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
// ✅ Center Title
Text(
"Service Details",
textAlign: TextAlign.center,
style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.055,
color: const Color(0xFF111827),
),
),
// ✅ Left Back Icon
Positioned(
left: 0,
child: GestureDetector(
onTap: () {
final targetTab = widget.sourceTabIndex ?? 0;
Get.offAll(
() =>
MainController(initialIndex: targetTab),
);
},
child: const Icon(
Icons.arrow_back_ios_rounded,
color: Color(0xFF111827),
),
),
),
if (data.serviceStatus == 'Completed')
Positioned(
right: 0,
child: GestureDetector(
onTap: () {
Get.to(
() => CompletedLiveChatScreen(
fileid: data.id.toString(),
chatid:
int.tryParse(
data.completedChatId ?? '0',
) ??
0,
),
)?.then((_) {
ref
.read(
notificationTriggerProvider
.notifier,
)
.state++;
ref.invalidate(chatMessagesProvider);
});
},
child: Container(
width: 39.0,
height: 39.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(
color: const Color(0xFF055976),
width: 0.8,
),
),
child: const Icon(
Icons.message_rounded,
size: 18,
),
),
),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Detailed Information",
style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.047,
height: 1.4,
letterSpacing: 0.03 * 20,
),
textAlign: TextAlign.center,
),
if (data.serviceStatus == 'Waiting for Admin' ||
data.serviceStatus == 'Payment Pending' ||
data.serviceStatus == 'Cancelled')
Container(
width: width * 0.3,
height: height * 0.04,
decoration: BoxDecoration(
color: const Color(0xFFFFE8E8),
borderRadius: BorderRadius.circular(5.52),
border: Border.all(
color: const Color(0xFFFFD7D7),
width: 1.84,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.17,
offset: const Offset(0, 3.68),
),
],
),
child: Center(
child: Text(
data.serviceStatus.toString(),
style: AppTextStyles.regular.copyWith(
fontSize: width * 0.029,
color: const Color(0xFFFF0F0F),
),
),
),
),
// 🔹 In Progress Badge
if (data.serviceStatus == 'In Progress')
Container(
width: width * 0.25,
height: height * 0.042,
decoration: BoxDecoration(
color: const Color(0xFFEAFAE6),
borderRadius: BorderRadius.circular(6.21),
border: Border.all(
color: const Color(0xFFDDFFDD),
width: 2.07,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 8.08,
offset: const Offset(0, 4.14),
),
],
),
child: Center(
child: Text(
data.serviceStatus.toString(),
style: AppTextStyles.regular.copyWith(
fontSize: width * 0.032,
letterSpacing: 0.03,
color: const Color(0xFF12800C),
),
),
),
),
// 🔹 Completed Badge
if (data.serviceStatus == 'Completed')
Container(
width: width * 0.23,
height: height * 0.038,
decoration: BoxDecoration(
color: const Color(0xFFFAF7E6),
borderRadius: BorderRadius.circular(5.52),
border: Border.all(
color: const Color(0xFFFFE9DD),
width: 1.84,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.17,
offset: const Offset(0, 3.68),
),
],
),
child: Center(
child: Text(
data.serviceStatus.toString(),
style: AppTextStyles.regular.copyWith(
fontSize: width * 0.029,
letterSpacing: 0.03,
color: const Color(0xFFFF630F),
),
),
),
),
],
),
),
if (data.serviceStatus == "Completed") ...[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 20,
horizontal: 20,
),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFE1E1E1),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x66757575),
blurRadius: 15,
offset: Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"This is your final task document",
style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.042,
height: 1.4,
letterSpacing: 0.03 * 16,
color: const Color(0xFF111827),
),
textAlign: TextAlign.start,
),
const SizedBox(height: 15),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: 'Date: ',
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text: data.createdDate ?? "-",
style: AppTextStyles.regular
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
const SizedBox(width: 10),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
text: TextSpan(
children: [
TextSpan(
text: 'Payment : ',
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text: data.paymentStatus ?? "-",
style: AppTextStyles.regular
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
],
),
const SizedBox(height: 15),
if (data.userHandoverDocuments.isNotEmpty)
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount:
data.userHandoverDocuments.length,
itemBuilder: (context, index) {
final doc =
data.userHandoverDocuments[index];
final filePath = doc.filePath ?? '';
final isImage =
filePath.toLowerCase().endsWith(
'.jpg',
) ||
filePath.toLowerCase().endsWith(
'.jpeg',
) ||
filePath.toLowerCase().endsWith(
'.png',
) ||
filePath.toLowerCase().endsWith(
'.gif',
) ||
filePath.toLowerCase().endsWith(
'.webp',
);
final isPdf = filePath
.toLowerCase()
.endsWith('.pdf');
final isDownloadingThis =
_downloadingFiles[filePath] == true;
return Padding(
padding: const EdgeInsets.only(
right: 10,
),
child: GestureDetector(
onTap: () =>
_handleDownload(filePath),
child: Container(
height: 90,
width: 90,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8),
color: const Color(0xFFF5F5F5),
boxShadow: [
BoxShadow(
color: Colors.black
.withOpacity(0.05),
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(
8,
),
child: isImage
? Image.network(
filePath,
width: 90,
height: 90,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stackTrace,
) {
return const Center(
child: Icon(
Icons
.broken_image,
color: Colors
.grey,
),
);
},
)
: isPdf
? const Center(
child: Icon(
Icons
.picture_as_pdf,
color: Colors.red,
size: 50,
),
)
: const Center(
child: Icon(
Icons
.insert_drive_file,
color:
Colors.grey,
size: 50,
),
),
),
Center(
child: Container(
width: 56.12,
height: 56.12,
decoration: BoxDecoration(
color: Colors.white
.withOpacity(0.37),
borderRadius:
BorderRadius.circular(
8,
),
),
child: Center(
child: Container(
width: 36.66,
height: 36.66,
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(
6,
),
boxShadow: const [
BoxShadow(
color: Colors
.black12,
blurRadius: 4,
offset: Offset(
0,
2,
),
),
],
),
child:
isDownloadingThis
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth:
2,
color: Colors
.deepPurple,
),
)
: FutureBuilder<
String?
>(
future:
DownloadHelper.checkFileExists(
filePath,
),
builder:
(
context,
snapshot,
) {
final fileExists =
snapshot.data !=
null;
return Icon(
fileExists
? Icons.visibility
: Icons.download,
color:
Colors.black87,
size:
20,
);
},
),
),
),
),
),
],
),
),
),
);
},
),
)
else
const SizedBox.shrink(),
],
),
),
),
),
],
Column(
children: [
if (data.paymentStatus == "Un Paid" &&
!(data.serviceStatus ?? "").toLowerCase().contains(
"cancelled",
)) ...[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 31,
horizontal: 20,
),
child: Column(
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFFFF6ED),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: const Color(0xFFD8CEC5),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x66716E6E),
blurRadius: 15.2,
offset: Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Payment Advice",
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.042,
height: 1.4,
letterSpacing: 0.03 * 16,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: 'Date: ',
style: AppTextStyles
.semiBold
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text:
data.createdDate ??
"-",
style: AppTextStyles
.regular
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
const SizedBox(width: 10),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
text: TextSpan(
children: [
TextSpan(
text: 'Payment : ',
style: AppTextStyles
.semiBold
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text:
data.paymentStatus ??
"-",
style: AppTextStyles
.regular
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
],
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Total Amount",
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.04,
color: const Color(
0xFF111827,
),
),
),
Text(
"${data.paymentAmount ?? '0'}",
style: AppTextStyles.semiBold
.copyWith(
fontFamily: 'Roboto',
fontSize: width * 0.045,
color: const Color(
0xFF111827,
),
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
CommanButton(
text: 'Pay Now',
onPressed: () async {
try {
final response = await ApiRepository()
.payNow(
id: int.parse(data.id.toString()),
);
if (response['status'] == 'success') {
_razorpayService.openCheckout(
key: response['razorpay_key'],
amount: response['amount'],
orderId: response['order_id'],
description:
"Payment for Service ID: ${data.id}",
);
} else {
validationPopup.showErrorMessage(
context,
"Failed to initiate payment",
);
}
} catch (e) {
validationPopup.showErrorMessage(
context,
e.toString(),
);
}
},
),
],
),
),
] else if (data.paymentStatus == "Paid") ...[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 31,
horizontal: 20,
),
child: Column(
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFFFF6ED),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: const Color(0xFFD8CEC5),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x66716E6E),
blurRadius: 15.2,
offset: Offset(0, 1),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Payment Advice",
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.042,
height: 1.4,
letterSpacing: 0.03 * 16,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: 'Date: ',
style: AppTextStyles
.semiBold
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text:
data.createdDate ??
"-",
style: AppTextStyles
.regular
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
const SizedBox(width: 10),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
text: TextSpan(
children: [
TextSpan(
text: 'Payment: ',
style: AppTextStyles
.semiBold
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text:
data.paymentStatus ??
"-",
style: AppTextStyles
.regular
.copyWith(
fontSize:
width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
],
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
"Total Amount",
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.04,
color: const Color(
0xFF111827,
),
),
),
Text(
"${data.paymentAmount ?? '0'}",
style: AppTextStyles.semiBold
.copyWith(
fontFamily: 'Roboto',
fontSize: width * 0.045,
color: const Color(
0xFF111827,
),
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
_buildInvoiceDownloadButton(
data.invoice,
width,
),
],
),
),
] else ...[
const SizedBox.shrink(),
],
if (data.proforma != null &&
data.proforma!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 31,
horizontal: 20,
),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white, // #FFFFFF
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFFE1E1E1),
width: 1,
),
boxShadow: const [
BoxShadow(
color: Color(0x40757575), // #75757540
offset: Offset(0, 1),
blurRadius: 15,
spreadRadius: 0,
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Profoma Advice",
style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.042,
height: 1.4,
letterSpacing: 0.03 * 16,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
text: TextSpan(
children: [
TextSpan(
text: 'proforma Number : ',
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text:
data.proformaNumber ??
"-",
style: AppTextStyles.regular
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing:
0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
),
],
),
const SizedBox(height: 10),
RichText(
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
text: TextSpan(
children: [
TextSpan(
text: 'proforma Status : ',
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text: data.proformaStatus ?? "-",
style: AppTextStyles.regular
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
const SizedBox(height: 20),
RichText(
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
text: TextSpan(
children: [
TextSpan(
text: 'Subject : ',
style: AppTextStyles.semiBold
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
TextSpan(
text: data.subject ?? "-",
style: AppTextStyles.regular
.copyWith(
fontSize: width * 0.035,
height: 1.3,
letterSpacing: 0.04 * 13.97,
color: const Color(
0xFF111827,
),
),
),
],
),
),
const SizedBox(height: 20),
if (data.proformaStatus != "Accepted") ...[
Text(
"Note : Once you accept this proforma only you can pay the amount for the service",
textAlign: TextAlign.start,
style: AppTextStyles.semiBold.copyWith(
fontSize: 13,
height: 1.78, // 178% line-height
letterSpacing: 0.04 * 13, // 4%
color: Colors.red,
),
),
const SizedBox(height: 20),
],
Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
/// 📄 Proforma (More width)
Expanded(
flex: 3,
child: _buildProformaDownloadButton(
data.proforma,
),
),
const SizedBox(width: 8),
if (data.proformaStatus != "Accepted")
/// ✅ Accept (Less width)
Expanded(
flex: 2,
child: SizedBox(
height:
42, // match proforma height
child: CommanButton(
text: 'Accept',
onPressed: () async {
try {
await ApiRepository()
.proformaaccept(
proformaId: int.parse(
data.proformaId
.toString(),
),
);
validationPopup
.showSuccessMessage(
context,
"Proforma accepted successfully",
);
ref.invalidate(
serviceDetailProvider,
);
ref.invalidate(
serviceHistoryNotifierProvider,
);
} catch (error) {
validationPopup.showErrorMessage(
context,
error is Map &&
error['error'] !=
null
? error['error']
: "Failed to accept proforma",
);
}
},
),
),
),
],
),
const SizedBox(height: 20),
],
),
),
),
),
],
],
),
/// 🔹 Main Detail Box
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow("File ID", "${data.id}", width),
const SizedBox(height: 16),
_buildInfoRow(
"Request Type",
"${data.service}",
width,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildInfoRow(
"Date",
"${data.createdDate}",
width,
),
),
const SizedBox(width: 10),
Expanded(
child: _buildInfoRow(
"Time",
"${data.createdTime}",
width,
),
),
],
),
const SizedBox(height: 16),
_buildInfoRow(
"Created by",
"${data.createdBy}",
width,
),
const SizedBox(height: 16),
_buildInfoRow("Message", "${data.message}", width),
const SizedBox(height: 16),
/// 🔹 File List Section
Text(
"File Attachments:",
style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.04,
color: const Color(0xFF111827),
),
),
const SizedBox(height: 10),
if (data.userUploadedDocuments.isNotEmpty)
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: data.userUploadedDocuments.length,
itemBuilder: (context, index) {
final doc =
data.userUploadedDocuments[index];
final filePath = doc.filePath ?? '';
final isImage =
filePath.toLowerCase().endsWith(
'.jpg',
) ||
filePath.toLowerCase().endsWith(
'.jpeg',
) ||
filePath.toLowerCase().endsWith(
'.png',
) ||
filePath.toLowerCase().endsWith(
'.gif',
) ||
filePath.toLowerCase().endsWith(
'.webp',
);
final isPdf = filePath
.toLowerCase()
.endsWith('.pdf');
final isDownloadingThis =
_downloadingFiles[filePath] == true;
return Padding(
padding: const EdgeInsets.only(right: 10),
child: GestureDetector(
onTap: () => _handleDownload(filePath),
child: Container(
height: 90,
width: 90,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
8,
),
color: const Color(0xFFF5F5F5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(
0.05,
),
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: isImage
? Image.network(
filePath,
width: 90,
height: 90,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stackTrace,
) {
return const Center(
child: Icon(
Icons
.broken_image,
color: Colors
.grey,
),
);
},
)
: isPdf
? const Center(
child: Icon(
Icons.picture_as_pdf,
color: Colors.red,
size: 50,
),
)
: const Center(
child: Icon(
Icons
.insert_drive_file,
color: Colors.grey,
size: 50,
),
),
),
Center(
child: Container(
width: 56.12,
height: 56.12,
decoration: BoxDecoration(
color: Colors.white
.withOpacity(0.37),
borderRadius:
BorderRadius.circular(
8,
),
),
child: Center(
child: Container(
width: 36.66,
height: 36.66,
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
BorderRadius.circular(
6,
),
boxShadow: const [
BoxShadow(
color:
Colors.black12,
blurRadius: 4,
offset: Offset(
0,
2,
),
),
],
),
child: isDownloadingThis
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors
.deepPurple,
),
)
: FutureBuilder<
String?
>(
future:
DownloadHelper.checkFileExists(
filePath,
),
builder: (context, snapshot) {
final fileExists =
snapshot
.data !=
null;
return Icon(
fileExists
? Icons
.visibility
: Icons
.download,
color: Colors
.black87,
size: 20,
);
},
),
),
),
),
),
],
),
),
),
);
},
),
)
else
Text(
"No documents uploaded",
style: AppTextStyles.regular.copyWith(
color: const Color(0xFF6B7280),
),
),
],
),
),
),
if (data.chatId != null &&
data.chatId!.isNotEmpty &&
!(data.serviceStatus ?? "").toLowerCase().contains(
"cancelled",
))
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 20,
),
child:
countAsync?.when(
data: (count) => CommanButton(
text: "Chat",
backgroundImage: AppAssets.backgroundimages,
prefixIcon: messageIconWithBadge(count.count),
onPressed: () =>
Get.to(
() => LiveChatScreen(
fileid: data.id.toString(),
chatid: int.parse(
data.chatId.toString(),
),
),
)?.then((_) {
ref
.read(
notificationTriggerProvider
.notifier,
)
.state++;
ref.invalidate(chatMessagesProvider);
}),
),
loading: () => CommanButton(
text: "Chat",
backgroundImage: AppAssets.backgroundimages,
prefixIcon: messageIconWithBadge(0),
onPressed: () =>
Get.to(
() => LiveChatScreen(
fileid: data.id.toString(),
chatid: int.parse(
data.chatId.toString(),
),
),
)?.then((_) {
ref
.read(
notificationTriggerProvider
.notifier,
)
.state++;
ref.invalidate(chatMessagesProvider);
}),
),
error: (_, __) => CommanButton(
text: "Chat",
backgroundImage: AppAssets.backgroundimages,
prefixIcon: messageIconWithBadge(0),
onPressed: () =>
Get.to(
() => LiveChatScreen(
fileid: data.id.toString(),
chatid: int.parse(
data.chatId.toString(),
),
),
)?.then((_) {
ref
.read(
notificationTriggerProvider
.notifier,
)
.state++;
ref.invalidate(chatMessagesProvider);
}),
),
) ??
CommanButton(
text: "Chat",
backgroundImage: AppAssets.backgroundimages,
prefixIcon: messageIconWithBadge(0),
onPressed: () =>
Get.to(
() => LiveChatScreen(
fileid: data.id.toString(),
chatid: int.parse(data.chatId.toString()),
),
)?.then((_) {
ref
.read(
notificationTriggerProvider.notifier,
)
.state++;
ref.invalidate(chatMessagesProvider);
}),
),
),
SizedBox(
height: 80.0 + MediaQuery.of(context).padding.bottom,
),
],
),
);
},
),
),
),
);
}
/// 🔹 Helper Widget for Each Info Row
Widget _buildInfoRow(String label, String value, double width) {
return RichText(
text: TextSpan(
children: [
TextSpan(
text: '$label: ',
style: AppTextStyles.semiBold.copyWith(
fontSize: width * 0.04,
color: const Color(0xFF111827),
),
),
TextSpan(
text: value.isEmpty ? '' : value,
style: AppTextStyles.medium.copyWith(
fontSize: width * 0.04,
height: 1.5,
color: const Color(0xFF374151),
),
),
],
),
);
}
Widget messageIconWithBadge(int count) {
return Stack(
clipBehavior: Clip.none,
children: [
const Icon(Icons.message_outlined, color: Colors.white, size: 26),
if (count > 0)
Positioned(
right: -2,
top: -2,
child: Container(
padding: const EdgeInsets.all(3),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
count.toString(),
style: AppTextStyles.bold.copyWith(
color: Colors.white,
fontSize: 10,
),
),
),
),
],
);
}
}