import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:provider/provider.dart'; import 'package:file_picker/file_picker.dart'; import 'package:open_filex/open_filex.dart'; import 'dart:io'; import '../models/project_model.dart'; import '../models/branch_model.dart'; import '../models/project_doc_model.dart'; import '../models/task_log_model.dart'; import '../services/project_service.dart'; import '../services/task_log_service.dart'; import 'project_form_screen.dart'; class ProjectDetailScreen extends StatefulWidget { final ProjectModel project; const ProjectDetailScreen({super.key, required this.project}); @override State createState() => _ProjectDetailScreenState(); } class _ProjectDetailScreenState extends State { bool _isUploading = false; bool _isEditMode = false; late List _currentFlavors; @override void initState() { super.initState(); _currentFlavors = List.from(widget.project.flavors); } void _launchURL(String? url) async { if (url == null || url.isEmpty) return; final uri = Uri.parse(url); try { if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { throw 'Could not launch $url'; } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); } } } void _addBranch() { final controller = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("New Branch"), content: TextField( controller: controller, decoration: const InputDecoration(hintText: "e.g. main, develop, feature/auth"), autofocus: true, ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("CANCEL")), TextButton( onPressed: () { if (controller.text.isNotEmpty) { context.read().addBranch(widget.project.id, controller.text.trim()); } Navigator.pop(ctx); }, child: const Text("ADD"), ), ], ), ); } void _addFlavor() { final controller = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("Add Project Flavor"), content: TextField( controller: controller, decoration: const InputDecoration(hintText: "e.g. dev, prod, staging"), autofocus: true, ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("CANCEL")), TextButton( onPressed: () async { String f = controller.text.trim(); if (f.isNotEmpty && !_currentFlavors.contains(f)) { setState(() => _currentFlavors.add(f)); final updatedProject = ProjectModel( id: widget.project.id, name: widget.project.name, gitUrl: widget.project.gitUrl, apiDocUrl: widget.project.apiDocUrl, description: widget.project.description, flavors: _currentFlavors, createdAt: widget.project.createdAt, ); await context.read().updateProject(updatedProject); } if (mounted) Navigator.pop(ctx); }, child: const Text("ADD"), ), ], ), ); } void _deleteFlavor(String flavor) async { setState(() => _currentFlavors.remove(flavor)); final updatedProject = ProjectModel( id: widget.project.id, name: widget.project.name, gitUrl: widget.project.gitUrl, apiDocUrl: widget.project.apiDocUrl, description: widget.project.description, flavors: _currentFlavors, createdAt: widget.project.createdAt, ); await context.read().updateProject(updatedProject); } void _addDocDialog() { showModalBottomSheet( context: context, builder: (ctx) => Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.link, color: Colors.blue), title: const Text("Add Link"), onTap: () { Navigator.pop(ctx); _addLinkDoc(); }, ), ListTile( leading: const Icon(Icons.picture_as_pdf, color: Colors.red), title: const Text("Upload PDF"), onTap: () { Navigator.pop(ctx); _uploadPDFDoc(); }, ), const SizedBox(height: 20), ], ), ); } void _addLinkDoc() { final titleCtrl = TextEditingController(); final urlCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("Add Link Document"), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField(controller: titleCtrl, decoration: const InputDecoration(labelText: "Label")), TextField(controller: urlCtrl, decoration: const InputDecoration(labelText: "URL")), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("CANCEL")), TextButton( onPressed: () { if (titleCtrl.text.isNotEmpty && urlCtrl.text.isNotEmpty) { context.read().addDoc( widget.project.id, titleCtrl.text.trim(), urlCtrl.text.trim(), 'link' ); } Navigator.pop(ctx); }, child: const Text("SAVE"), ), ], ), ); } void _uploadPDFDoc() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf'], ); if (result != null) { setState(() => _isUploading = true); try { File file = File(result.files.single.path!); String label = result.files.single.name; String url = await context.read().uploadDoc(widget.project.id, file); await context.read().addDoc(widget.project.id, label, url, 'pdf'); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Upload failed: $e"))); } finally { if (mounted) setState(() => _isUploading = false); } } } void _confirmDeleteProject() { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("Delete Project"), content: Text("Are you sure? All data related to '${widget.project.name}' will be lost."), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("CANCEL")), TextButton( onPressed: () async { await context.read().deleteProject(widget.project.id); if (mounted) { Navigator.pop(ctx); Navigator.pop(context); } }, child: const Text("DELETE PROJECT", style: TextStyle(color: Colors.red)), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8FAFC), appBar: AppBar( title: Text(widget.project.name), actions: [ if (_isEditMode) ...[ IconButton( onPressed: () => Navigator.push( context, MaterialPageRoute(builder: (_) => ProjectFormScreen(project: widget.project)) ), icon: const Icon(Icons.settings_outlined), tooltip: "Project Settings", ), IconButton( onPressed: _confirmDeleteProject, icon: const Icon(Icons.delete_forever, color: Colors.red), tooltip: "Delete Project", ), ], IconButton( onPressed: () => setState(() => _isEditMode = !_isEditMode), icon: Icon(_isEditMode ? Icons.check_circle_rounded : Icons.edit_rounded, color: _isEditMode ? Colors.green : null), tooltip: _isEditMode ? "Finish Editing" : "Edit Mode", ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLatestActivity(), const SizedBox(height: 32), _buildSectionHeader("Quick Links"), Row( children: [ _linkTile("GIT REPO", Icons.code_rounded, widget.project.gitUrl), if (widget.project.apiDocUrl != null) ...[ const SizedBox(width: 12), _linkTile("API DOCS", Icons.api_rounded, widget.project.apiDocUrl!), ], ], ), const SizedBox(height: 32), _buildSectionHeader("Description"), Text( widget.project.description, style: const TextStyle(height: 1.6, color: Color(0xFF475569)), ), const SizedBox(height: 32), _isEditMode ? _buildSectionHeaderWithAction("Branches", Icons.add_rounded, _addBranch) : _buildSectionHeader("Branches"), _buildBranchList(), const SizedBox(height: 32), _isEditMode ? _buildSectionHeaderWithAction("Project Flavors", Icons.add_rounded, _addFlavor) : _buildSectionHeader("Project Flavors"), _buildFlavorList(), const SizedBox(height: 32), _isEditMode ? _buildSectionHeaderWithAction("Documents", Icons.add_rounded, _addDocDialog) : _buildSectionHeader("Documents"), if (_isUploading) const Padding( padding: EdgeInsets.only(bottom: 12), child: LinearProgressIndicator(), ), _buildDocList(), ], ), ), ); } Widget _buildLatestActivity() { return FutureBuilder( future: context.read().getLatestLogForProject(widget.project.id), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Container( height: 80, alignment: Alignment.center, decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(24)), child: const CircularProgressIndicator(strokeWidth: 2), ); } if (snapshot.hasError) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(16)), child: Column( children: [ const Icon(Icons.error_outline, color: Colors.red), const SizedBox(height: 8), Text( "Query Error: ${snapshot.error}. Check if a Firestore Index is needed.", style: const TextStyle(color: Colors.red, fontSize: 11, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ], ), ); } final log = snapshot.data; if (log == null) { return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), border: Border.all(color: Colors.grey.shade100), ), child: const Row( children: [ Icon(Icons.info_outline_rounded, color: Colors.indigo), SizedBox(width: 16), Text( "No activity tracked for this project yet.", style: TextStyle(color: Color(0xFF64748B), fontSize: 13, fontWeight: FontWeight.w500), ), ], ), ); } return Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF1E293B), Color(0xFF0F172A)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(32), boxShadow: [ BoxShadow( color: const Color(0xFF0F172A).withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "LATEST ACTIVITY", style: TextStyle(color: Colors.white54, fontSize: 10, fontWeight: FontWeight.bold, letterSpacing: 1.5), ), if (log.flavor != null) Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF6366F1), borderRadius: BorderRadius.circular(8), ), child: Text( log.flavor!.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold), ), ), ], ), const SizedBox(height: 20), Row( children: [ CircleAvatar( radius: 20, backgroundColor: Colors.white.withOpacity(0.1), child: const Icon(Icons.person_rounded, color: Colors.white70), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( log.userName, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18), ), Text( "Worked on ${log.moduleName}", style: const TextStyle(color: Colors.white60, fontSize: 12), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( DateFormat('dd MMM').format(log.date), style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14), ), Text( DateFormat('yyyy').format(log.date), style: const TextStyle(color: Colors.white38, fontSize: 10), ), ], ), ], ), ], ), ); }, ); } Widget _buildSectionHeader(String title) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: Text( title.toUpperCase(), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 12, color: Color(0xFF6366F1), letterSpacing: 1.2 ), ), ); } Widget _buildSectionHeaderWithAction(String title, IconData icon, VoidCallback onTap) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildSectionHeader(title), TextButton.icon( onPressed: onTap, icon: Icon(icon, size: 16), label: const Text("ADD", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11)), style: TextButton.styleFrom(foregroundColor: const Color(0xFF6366F1)), ), ], ); } Widget _linkTile(String label, IconData icon, String url) { return Expanded( child: Material( color: Colors.white, borderRadius: BorderRadius.circular(20), child: InkWell( onTap: () => _launchURL(url), borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade100), borderRadius: BorderRadius.circular(20), ), child: Column( children: [ Icon(icon, color: const Color(0xFF6366F1), size: 28), const SizedBox(height: 12), Text( label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: Color(0xFF1E293B), letterSpacing: 0.5), ), ], ), ), ), ), ); } Widget _buildBranchList() { return StreamBuilder>( stream: context.read().getBranches(widget.project.id), builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(); final branches = snapshot.data!; if (branches.isEmpty) return const Text("No active branches recorded.", style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12, fontStyle: FontStyle.italic)); return Wrap( spacing: 10, runSpacing: 10, children: branches.map((b) => Chip( label: Text(b.name, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)), backgroundColor: const Color(0xFFF1F5F9), side: BorderSide.none, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onDeleted: _isEditMode ? () { context.read().deleteBranch(widget.project.id, b.id); } : null, deleteIconColor: const Color(0xFFF43F5E), deleteIcon: const Icon(Icons.close_rounded, size: 14), )).toList(), ); }, ); } Widget _buildFlavorList() { if (_currentFlavors.isEmpty) { return const Text("No flavors defined yet.", style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12, fontStyle: FontStyle.italic)); } return Wrap( spacing: 10, runSpacing: 10, children: _currentFlavors.map((flavor) => Chip( label: Text(flavor, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), backgroundColor: const Color(0xFFEEF2FF), labelStyle: const TextStyle(color: Color(0xFF6366F1)), side: BorderSide.none, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onDeleted: _isEditMode ? () => _deleteFlavor(flavor) : null, deleteIconColor: const Color(0xFFF43F5E), deleteIcon: const Icon(Icons.close_rounded, size: 14), )).toList(), ); } Widget _buildDocList() { return StreamBuilder>( stream: context.read().getDocs(widget.project.id), builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(); final docs = snapshot.data!; if (docs.isEmpty) return const Text("No documents or technical specs.", style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12, fontStyle: FontStyle.italic)); return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: docs.length, itemBuilder: (context, index) { final doc = docs[index]; return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade100), ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: (doc.type == 'pdf' ? const Color(0xFFF43F5E) : Colors.blue).withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon( doc.type == 'pdf' ? Icons.picture_as_pdf_rounded : Icons.link_rounded, color: doc.type == 'pdf' ? const Color(0xFFF43F5E) : Colors.blue, size: 20, ), ), title: Text(doc.label, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Color(0xFF1E293B))), trailing: _isEditMode ? IconButton( onPressed: () => context.read().deleteDoc(widget.project.id, doc.id), icon: const Icon(Icons.delete_outline_rounded, size: 18, color: Color(0xFFF43F5E)), ) : const Icon(Icons.arrow_forward_ios_rounded, size: 14, color: Color(0xFF94A3B8)), onTap: () { if (!_isEditMode) { if (doc.type == 'pdf') { OpenFilex.open(doc.url); } else { _launchURL(doc.url); } } }, ), ); }, ); }, ); } }