import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:dio/dio.dart'; import 'package:share_plus/share_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../domain/project_task.dart'; import '../../providers/project_provider.dart'; import '../../../transactions/presentation/widgets/attachment_gallery_screen.dart'; class TaskDetailsSheet extends ConsumerStatefulWidget { final ProjectTask task; const TaskDetailsSheet({Key? key, required this.task}) : super(key: key); @override ConsumerState createState() => _TaskDetailsSheetState(); } class _TaskDetailsSheetState extends ConsumerState { final TextEditingController _commentController = TextEditingController(); final ImagePicker _picker = ImagePicker(); final List> _attachments = []; bool _isSubmitting = false; String? _jwtToken; final Set _downloadingIds = {}; @override void initState() { super.initState(); _loadToken(); } Future _loadToken() async { const storage = FlutterSecureStorage(); _jwtToken = await storage.read(key: 'jwt_token'); if (mounted) setState(() {}); } Future _pickImage() async { final XFile? image = await _picker.pickImage(source: ImageSource.gallery); if (image != null) { final bytes = await image.readAsBytes(); setState(() { _attachments.add({ 'fileName': image.name, 'contentType': 'image/jpeg', 'base64Content': base64Encode(bytes), }); }); } } Future _pickFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles(); if (result != null && result.files.single.path != null) { final file = File(result.files.single.path!); final bytes = await file.readAsBytes(); final extension = result.files.single.extension?.toLowerCase() ?? ''; String contentType = 'application/octet-stream'; if (['pdf'].contains(extension)) contentType = 'application/pdf'; else if (['doc', 'docx'].contains(extension)) contentType = 'application/msword'; else if (['png', 'jpg', 'jpeg'].contains(extension)) contentType = 'image/jpeg'; setState(() { _attachments.add({ 'fileName': result.files.single.name, 'contentType': contentType, 'base64Content': base64Encode(bytes), }); }); } } Future _submitComment() async { if (_commentController.text.trim().isEmpty && _attachments.isEmpty) return; setState(() => _isSubmitting = true); try { final repo = ref.read(projectRepositoryProvider); await repo.addComment( taskId: widget.task.id, content: _commentController.text.trim(), attachments: _attachments, ); _commentController.clear(); setState(() { _attachments.clear(); }); ref.invalidate(projectTaskCommentsProvider(widget.task.id)); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error adding comment: $e'))); } } finally { if (mounted) setState(() => _isSubmitting = false); } } Future _downloadAttachment(Map attMap, int commentId, int attIndex) async { final filePath = attMap['filePath']; final fileName = attMap['fileName'] ?? 'attachment'; final contentType = attMap['contentType'] ?? 'application/octet-stream'; if (filePath == null) return; final uniqueId = commentId * 1000 + attIndex; setState(() => _downloadingIds.add(uniqueId)); try { final dir = await getTemporaryDirectory(); final savePath = '${dir.path}/$fileName'; final dio = Dio(); if (_jwtToken != null) { dio.options.headers['Authorization'] = 'Bearer $_jwtToken'; } final url = 'https://app.technobeesolutions.in/api/kifi/projects/tasks/attachments/download?filePath=$filePath&contentType=$contentType'; await dio.download(url, savePath); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Downloaded $fileName'))); await Share.shareXFiles([XFile(savePath)]); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error downloading: $e'))); } } finally { if (mounted) { setState(() => _downloadingIds.remove(uniqueId)); } } } @override Widget build(BuildContext context) { final commentsAsync = ref.watch(projectTaskCommentsProvider(widget.task.id)); return Container( height: MediaQuery.of(context).size.height * 0.9, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( children: [ _buildHeader(context), Expanded( child: commentsAsync.when( data: (comments) { if (comments.isEmpty) return const Center(child: Text('No comments yet.')); return ListView.builder( padding: const EdgeInsets.all(16), itemCount: comments.length, itemBuilder: (context, index) { final c = comments[index]; return Card( margin: const EdgeInsets.only(bottom: 12), color: Colors.grey[100], elevation: 0, child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(c.content, style: const TextStyle(fontSize: 14)), if (c.images.isNotEmpty) ...[ const SizedBox(height: 8), Wrap( spacing: 8, children: c.images.asMap().entries.map((entry) { final imgIndex = entry.key; final imgData = entry.value; bool isOldBase64 = !imgData.startsWith('{'); Map? parsedData; if (!isOldBase64) { try { parsedData = jsonDecode(imgData); } catch (_) { isOldBase64 = true; } } final isImage = isOldBase64 || (parsedData?['contentType']?.startsWith('image/') ?? false); if (isImage) { ImageProvider provider; if (isOldBase64 || parsedData?['base64Content'] != null) { final b64 = isOldBase64 ? imgData : parsedData!['base64Content']; provider = MemoryImage(base64Decode(b64)); } else { final url = 'https://app.technobeesolutions.in/api/kifi/projects/tasks/attachments/download?filePath=${parsedData!['filePath']}&contentType=${parsedData['contentType']}'; provider = _jwtToken != null ? NetworkImage(url, headers: {'Authorization': 'Bearer $_jwtToken'}) : const AssetImage('assets/images/placeholder.png') as ImageProvider; } return GestureDetector( onTap: () { // Collect all images for the gallery final allImages = c.images.where((i) { if (!i.startsWith('{')) return true; try { final map = jsonDecode(i); return map['contentType']?.startsWith('image/') ?? false; } catch (_) { return true; } }).map((i) { if (!i.startsWith('{')) return MemoryImage(base64Decode(i)) as ImageProvider; final map = jsonDecode(i); if (map['base64Content'] != null) return MemoryImage(base64Decode(map['base64Content'])) as ImageProvider; final url = 'https://app.technobeesolutions.in/api/kifi/projects/tasks/attachments/download?filePath=${map['filePath']}&contentType=${map['contentType']}'; return NetworkImage(url, headers: {'Authorization': 'Bearer $_jwtToken'}) as ImageProvider; }).toList(); // Find index of clicked image among images int realIndex = 0; for (int i=0; i<=imgIndex; i++) { final ii = c.images[i]; bool img = !ii.startsWith('{'); if (!img) { try { final m = jsonDecode(ii); img = m['contentType']?.startsWith('image/') ?? false; } catch(_) { img = true; } } if (img && i < imgIndex) realIndex++; } Navigator.push( context, MaterialPageRoute( builder: (context) => AttachmentGalleryScreen( images: allImages, initialIndex: realIndex, ), ), ); }, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Image( image: provider, width: 80, height: 80, fit: BoxFit.cover, ), ), ); } else { // Non-image file final fileName = parsedData?['fileName'] ?? 'Document'; final uniqueId = c.id * 1000 + imgIndex; final isDownloading = _downloadingIds.contains(uniqueId); return GestureDetector( onTap: () { if (!isDownloading && parsedData != null) { _downloadAttachment(parsedData, c.id, imgIndex); } }, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ isDownloading ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(LucideIcons.fileText, color: Colors.blue), const SizedBox(height: 4), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( fileName, style: const TextStyle(fontSize: 10), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } }).toList(), ) ] ], ), ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Center(child: Text('Error: $err')), ), ), _buildCommentInput(), ], ), ); } Widget _buildHeader(BuildContext context) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(border: Border(bottom: BorderSide(color: Colors.grey.shade300))), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.task.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), if (widget.task.description != null && widget.task.description!.isNotEmpty) Text(widget.task.description!, style: const TextStyle(color: Colors.grey)), ], ), ), IconButton(icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(context)), ], ), ); } Widget _buildCommentInput() { return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom + 16, left: 16, right: 16, top: 16, ), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: Colors.grey.shade300)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_attachments.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Wrap( spacing: 8, children: _attachments.asMap().entries.map((entry) { final att = entry.value; final isImg = att['contentType']?.startsWith('image/') ?? false; return Stack( children: [ Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), ), child: isImg ? ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.memory( base64Decode(att['base64Content']!), fit: BoxFit.cover, ), ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(LucideIcons.fileText, size: 24, color: Colors.blue), Text(att['fileName'] ?? '', style: const TextStyle(fontSize: 8), maxLines: 1, overflow: TextOverflow.ellipsis), ], ), ), Positioned( right: 0, top: 0, child: GestureDetector( onTap: () { setState(() { _attachments.removeAt(entry.key); }); }, child: Container( color: Colors.black54, child: const Icon(LucideIcons.x, size: 16, color: Colors.white), ), ), ), ], ); }).toList(), ), ), Row( children: [ IconButton( icon: const Icon(LucideIcons.image), onPressed: _pickImage, ), IconButton( icon: const Icon(LucideIcons.paperclip), onPressed: _pickFile, ), Expanded( child: TextField( controller: _commentController, decoration: const InputDecoration( hintText: 'Add a comment...', border: InputBorder.none, ), ), ), _isSubmitting ? const Padding(padding: EdgeInsets.all(12.0), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))) : IconButton( icon: const Icon(LucideIcons.send, color: Colors.blue), onPressed: _submitComment, ), ], ), ], ), ); } }