2026-04-11

This commit is contained in:
Sarankumar committed 2026-04-11 10:21:31 +05:30
commit 27e27fb70b
270 files changed
+30296

No files matched your search

+373
View File
@@ -0,0 +1,373 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:photo_view/photo_view.dart';
class ChatProfileScreen extends ConsumerWidget {
final int chatid;
const ChatProfileScreen({super.key, required this.chatid});
// Helper function to check if document is valid
bool _isValidDocument(dynamic doc) {
// Check if fileName exists and is not empty
final hasFileName =
doc.fileName != null && doc.fileName.toString().trim().isNotEmpty;
// Check if filePath exists and is not empty
final hasFilePath =
doc.filePath != null && doc.filePath.toString().trim().isNotEmpty;
// Return true only if both fileName and filePath exist
return hasFileName && hasFilePath;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final result = ref.watch(chatDocumentProvider(chatid));
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(240),
child: AppBar(
automaticallyImplyLeading: true,
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
"Profile",
style: TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w400,
fontSize: 20,
height: 1.4,
letterSpacing: 0.6,
color: Colors.black,
),
),
flexibleSpace: result.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, st) => Center(child: Text(e.toString())),
data: (data) {
return Container(
width: double.infinity,
height: 282,
padding: const EdgeInsets.only(top: 100, bottom: 20),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: const Color(0xFFDEDEDE),
width: 1,
),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 45,
backgroundImage: NetworkImage(data.profile),
),
const SizedBox(height: 12),
Text(
data.name,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
data.role,
style: TextStyle(fontSize: 16, color: Colors.grey[700]),
),
],
),
);
},
),
),
),
body: result.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, st) => Center(child: Text(e.toString())),
data: (data) {
// Filter out invalid documents
final senderList = data.userUploadedDocuments
.where((doc) => _isValidDocument(doc))
.toList();
final receiverList = data.adminUploadedDocuments
.where((doc) => _isValidDocument(doc))
.toList();
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---------------- Shared by Taxglide ----------------
const Text(
"Shared By Taxglide",
style: TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w600,
fontSize: 20,
height: 1.4,
letterSpacing: 0.6,
color: Colors.black,
),
),
const SizedBox(height: 10),
receiverList.isEmpty
? const Text("No documents from Taxglide")
: SizedBox(
height: 110,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: receiverList.length,
separatorBuilder: (_, __) =>
const SizedBox(width: 12),
itemBuilder: (_, index) {
final doc = receiverList[index];
final isImage = DownloadHelper.isImageFile(
doc.filePath,
);
final isPdf =
DownloadHelper.getFileExtension(doc.filePath) ==
'pdf';
return GestureDetector(
onTap: () {
if (isImage) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ImagePreviewScreen(
imageUrl: doc.filePath,
),
),
);
} else if (isPdf) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PdfPreviewScreen(
pdfUrl: doc.filePath,
fileName: doc.fileName,
),
),
);
}
},
child: Container(
width: 100,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: Column(
children: [
Expanded(
child: isImage
? ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network(
doc.filePath,
width: 100,
fit: BoxFit.cover,
),
)
: isPdf
? const Icon(
Icons.picture_as_pdf,
size: 40,
color: Colors.red,
)
: const Icon(
Icons.insert_drive_file,
size: 40,
color: Colors.blue,
),
),
],
),
),
);
},
),
),
const SizedBox(height: 30),
// ---------------- Shared by Client ----------------
const Text(
"Shared By Client",
style: TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w600,
fontSize: 20,
height: 1.4,
letterSpacing: 0.6,
color: Colors.black,
),
),
const SizedBox(height: 10),
senderList.isEmpty
? const Text("No documents from Client")
: SizedBox(
height: 110,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: senderList.length,
separatorBuilder: (_, __) =>
const SizedBox(width: 12),
itemBuilder: (_, index) {
final doc = senderList[index];
final isImage = DownloadHelper.isImageFile(
doc.filePath,
);
final isPdf =
DownloadHelper.getFileExtension(doc.filePath) ==
'pdf';
return GestureDetector(
onTap: () async {
if (isImage) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ImagePreviewScreen(
imageUrl: doc.filePath,
),
),
);
} else {
await DownloadHelper.downloadFile(
doc.filePath,
);
}
},
child: Container(
width: 100,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade300,
),
),
child: Column(
children: [
Expanded(
child: isImage
? ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network(
doc.filePath,
width: 100,
fit: BoxFit.cover,
),
)
: isPdf
? const Icon(
Icons.picture_as_pdf,
size: 40,
color: Colors.red,
)
: const Icon(
Icons.insert_drive_file,
size: 40,
color: Colors.blue,
),
),
],
),
),
);
},
),
),
],
),
);
},
),
);
}
}
class ImagePreviewScreen extends StatelessWidget {
final String imageUrl;
const ImagePreviewScreen({super.key, required this.imageUrl});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
),
body: Center(
child: PhotoView(
imageProvider: NetworkImage(imageUrl),
backgroundDecoration: const BoxDecoration(color: Colors.black),
),
),
);
}
}
class PdfPreviewScreen extends StatelessWidget {
final String pdfUrl;
final String fileName;
const PdfPreviewScreen({
super.key,
required this.pdfUrl,
required this.fileName,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(fileName),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.picture_as_pdf, size: 80, color: Colors.red),
const SizedBox(height: 20),
Text(
fileName,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
const Text(
"PDF Preview not available",
style: TextStyle(color: Colors.grey),
),
],
),
),
);
}
}
+569
View File
@@ -0,0 +1,569 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/controller/api_repository.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
// ⭐⭐⭐ CHAT INPUT BOX WITH FILE TAG SUPPORT ⭐⭐⭐
class ChatInputBox extends ConsumerStatefulWidget {
final int chatId;
final VoidCallback? onMessageSent;
const ChatInputBox(this.chatId, {Key? key, this.onMessageSent})
: super(key: key);
@override
ConsumerState<ChatInputBox> createState() => _ChatInputBoxState();
}
class _ChatInputBoxState extends ConsumerState<ChatInputBox> {
final TextEditingController messageCtrl = TextEditingController();
final ImagePicker picker = ImagePicker();
List<File> selectedFiles = [];
bool _isSending = false;
@override
void initState() {
super.initState();
_requestPermissions();
}
// ⭐ Request storage permissions
Future<void> _requestPermissions() async {
await DownloadHelper.requestStoragePermission();
}
void showPickerMenu() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
),
builder: (context) {
return SafeArea(
bottom: true,
child: SizedBox(
height: 220,
child: Column(
children: [
const SizedBox(height: 10),
ListTile(
leading: const Icon(Icons.photo_camera, size: 28),
title: const Text("Camera", style: TextStyle(fontSize: 17)),
onTap: pickFromCamera,
),
ListTile(
leading: const Icon(Icons.photo_library, size: 28),
title: const Text("Gallery", style: TextStyle(fontSize: 17)),
onTap: pickFromGallery,
),
ListTile(
leading: const Icon(Icons.attach_file, size: 28),
title: const Text("Files", style: TextStyle(fontSize: 17)),
onTap: pickDocuments,
),
const SizedBox(height: 10),
],
),
),
);
},
);
}
Future pickFromCamera() async {
Navigator.pop(context);
try {
var status = await Permission.camera.status;
// Handle cases where we cannot show the system dialog again
// Skip strict blocks on iOS Simulator by just trying to launch picker
if (!Platform.isIOS || status.isGranted || status.isLimited) {
if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) {
status = await Permission.camera.request();
if (!status.isGranted && !status.isLimited) {
_showPermissionDialog("Camera");
return;
}
}
}
final XFile? image = await picker.pickImage(
source: ImageSource.camera,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (image != null) {
setState(() => selectedFiles.add(File(image.path)));
}
} catch (e) {
debugPrint("❌ Camera error: $e");
// If it's a simulator, it will throw an error since there is no camera
if (Platform.isIOS && e.toString().contains("camera not available")) {
_showErrorSnackbar("Camera not available on iOS Simulator");
} else {
_showErrorSnackbar("Failed to capture image");
}
}
}
Future pickFromGallery() async {
Navigator.pop(context);
try {
var status = await Permission.photos.status;
if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) {
status = await Permission.photos.request();
if (!status.isGranted && !status.isLimited && !Platform.isIOS) {
_showPermissionDialog("Photos");
return;
}
}
final List<XFile>? images = await picker.pickMultiImage(
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 85,
);
if (images != null && images.isNotEmpty) {
setState(() {
selectedFiles.addAll(images.map((e) => File(e.path)));
});
}
} catch (e) {
print("❌ Gallery error: $e");
_showErrorSnackbar("Failed to pick images");
}
}
Future pickDocuments() async {
Navigator.pop(context);
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.any,
);
if (result != null && result.files.isNotEmpty) {
setState(() {
selectedFiles.addAll(
result.paths.where((e) => e != null).map((e) => File(e!)),
);
});
}
} catch (e) {
print("❌ File picker error: $e");
_showErrorSnackbar("Failed to pick files");
}
}
void _showPermissionDialog(String permissionName) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("$permissionName Permission Required"),
content: Text(
"Allow Taxglide to access your $permissionName to capture and upload files easily. You can enable this in the app settings.",
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings();
},
child: const Text("Go to Settings"),
),
],
),
);
}
void _showErrorSnackbar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Text(message),
duration: const Duration(seconds: 2),
),
);
}
void removeFile(int index) {
setState(() {
selectedFiles.removeAt(index);
});
}
String getFileName(String path) {
return path.split('/').last;
}
IconData getFileIcon(String path) {
final ext = path.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) {
return Icons.image;
} else if (ext == 'pdf') {
return Icons.picture_as_pdf;
} else if (['doc', 'docx'].contains(ext)) {
return Icons.description;
} else if (['xls', 'xlsx'].contains(ext)) {
return Icons.table_chart;
} else {
return Icons.insert_drive_file;
}
}
Future<void> sendMessage() async {
if (_isSending) return; // Prevent double-tap
String text = messageCtrl.text.trim();
if (text.isEmpty && selectedFiles.isEmpty) return;
// ⭐ Get the tagged message
final taggedMessage = ref.read(taggedMessageProvider);
final tagId = taggedMessage?.id ?? 0;
setState(() => _isSending = true);
// Show loading dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => WillPopScope(
onWillPop: () async => false,
child: const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text("Sending message..."),
],
),
),
),
),
),
);
try {
List<File> filesToUpload = [];
// ⭐⭐⭐ Save files to Send folder
if (selectedFiles.isNotEmpty) {
print("📤 Saving ${selectedFiles.length} files to Send folder...");
for (File file in selectedFiles) {
try {
// Try to save to Send folder, fallback to original if fails
final savedPath = await DownloadHelper.saveToSendFolder(file);
filesToUpload.add(File(savedPath));
} catch (e) {
print("⚠️ Could not save file, using original: $e");
filesToUpload.add(file);
}
}
print("✅ Files prepared for upload: ${filesToUpload.length}");
}
// Send message with files
await ApiRepository().sendChatMessage(
chatId: widget.chatId,
messages: text,
tagId: tagId,
files: filesToUpload,
);
// Refresh chat messages
await ref.read(chatMessagesProvider(widget.chatId).notifier).refresh();
ref.invalidate(chatDocumentProvider);
print("✅ Message sent successfully with tagId: $tagId");
// Close loading dialog
if (mounted) Navigator.pop(context);
// Clear input
if (mounted) {
setState(() {
messageCtrl.clear();
selectedFiles.clear();
_isSending = false;
});
}
// Clear the tagged message
ref.read(taggedMessageProvider.notifier).state = null;
// Notify parent to scroll to bottom
if (widget.onMessageSent != null) {
widget.onMessageSent!();
}
} catch (e) {
print("❌ Error sending message: $e");
// Close loading dialog
if (mounted) Navigator.pop(context);
setState(() => _isSending = false);
// Show error message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.red,
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 8),
Expanded(
child: Text(
"Failed to send: ${e.toString()}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
duration: const Duration(seconds: 4),
action: SnackBarAction(
label: "Retry",
textColor: Colors.white,
onPressed: sendMessage,
),
),
);
}
}
}
@override
Widget build(BuildContext context) {
final taggedMessage = ref.watch(taggedMessageProvider);
return SafeArea(
bottom: true,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
color: Colors.white,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ⭐⭐⭐ SELECTED FILES PREVIEW (like WhatsApp)
if (selectedFiles.isNotEmpty)
Container(
height: 100,
margin: const EdgeInsets.only(bottom: 10),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: selectedFiles.length,
itemBuilder: (context, index) {
final file = selectedFiles[index];
final isImage = DownloadHelper.isImageFile(file.path);
return Container(
width: 80,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
// File preview
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: isImage
? Image.file(
file,
width: 80,
height: 100,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(Icons.broken_image),
);
},
)
: Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
getFileIcon(file.path),
size: 30,
color: Colors.grey[600],
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4,
),
child: Text(
getFileName(file.path),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 9),
),
),
],
),
),
),
// Remove button
Positioned(
top: 2,
right: 2,
child: GestureDetector(
onTap: () => removeFile(index),
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 16,
color: Colors.white,
),
),
),
),
],
),
);
},
),
),
// Input Row
Row(
children: [
Expanded(
child: Container(
constraints: const BoxConstraints(
minHeight: 62,
maxHeight: 150,
),
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7),
border: Border.all(color: const Color(0xFFCACACA)),
boxShadow: const [
BoxShadow(
color: Colors.black26,
offset: Offset(0, 4),
blurRadius: 7.8,
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: messageCtrl,
maxLines: null,
minLines: 1,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,
decoration: InputDecoration(
hintText: taggedMessage != null
? "Reply to ${taggedMessage.user?.name ?? 'message'}"
: "Send your message",
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
vertical: 12,
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: _isSending ? null : showPickerMenu,
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Icon(
Icons.attach_file,
size: 26,
color: _isSending ? Colors.grey : Colors.black,
),
),
),
],
),
),
),
const SizedBox(width: 10),
GestureDetector(
onTap: _isSending ? null : sendMessage,
child: Container(
height: 46,
width: 46,
decoration: BoxDecoration(
color: _isSending ? Colors.grey : const Color(0xFF08710C),
shape: BoxShape.circle,
),
child: Center(
child: _isSending
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(
Icons.send,
color: Colors.white,
size: 22,
),
),
),
),
],
),
],
),
),
);
}
@override
void dispose() {
messageCtrl.dispose();
super.dispose();
}
}
+323
View File
@@ -0,0 +1,323 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:taxglide/consts/download_helper.dart';
class DownloadsScreen extends StatefulWidget {
const DownloadsScreen({super.key});
@override
State<DownloadsScreen> createState() => _DownloadsScreenState();
}
class _DownloadsScreenState extends State<DownloadsScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<File> sentFiles = [];
List<File> receivedFiles = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_loadFiles();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadFiles() async {
setState(() {
isLoading = true;
});
try {
final sent = await DownloadHelper.getSentFiles();
final received = await DownloadHelper.getReceivedFiles();
setState(() {
sentFiles = sent;
receivedFiles = received;
isLoading = false;
});
} catch (e) {
print("Error loading files: $e");
setState(() {
isLoading = false;
});
}
}
Future<void> _deleteFile(String filePath, bool isSent) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text("Delete File"),
content: const Text("Are you sure you want to delete this file?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text("Cancel"),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text("Delete"),
),
],
),
);
if (confirmed == true) {
final success = await DownloadHelper.deleteFile(filePath);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("File deleted successfully"),
backgroundColor: Colors.green,
),
);
_loadFiles();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Failed to delete file"),
backgroundColor: Colors.red,
),
);
}
}
}
String _getFileName(String path) {
return path.split('/').last;
}
String _getFileSize(File file) {
final bytes = file.lengthSync();
if (bytes < 1024) return "$bytes B";
if (bytes < 1024 * 1024) return "${(bytes / 1024).toStringAsFixed(1)} KB";
return "${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB";
}
IconData _getFileIcon(String path) {
final ext = path.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) {
return Icons.image;
} else if (ext == 'pdf') {
return Icons.picture_as_pdf;
} else if (['doc', 'docx'].contains(ext)) {
return Icons.description;
} else if (['xls', 'xlsx'].contains(ext)) {
return Icons.table_chart;
} else {
return Icons.insert_drive_file;
}
}
Color _getFileIconColor(String path) {
final ext = path.split('.').last.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) {
return Colors.blue;
} else if (ext == 'pdf') {
return Colors.red;
} else if (['doc', 'docx'].contains(ext)) {
return Colors.indigo;
} else if (['xls', 'xlsx'].contains(ext)) {
return Colors.green;
} else {
return Colors.grey;
}
}
Widget _buildFileList(List<File> files, bool isSent) {
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (files.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isSent ? Icons.upload_file : Icons.download,
size: 80,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
isSent ? "No sent files" : "No downloaded files",
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
isSent
? "Files you upload will appear here"
: "Downloaded files will appear here",
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadFiles,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: files.length,
itemBuilder: (context, index) {
final file = files[index];
final fileName = _getFileName(file.path);
final fileSize = _getFileSize(file);
final isImage = DownloadHelper.isImageFile(file.path);
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: () => DownloadHelper.openDownloadedFile(file.path),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// File icon or image preview
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: _getFileIconColor(file.path).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: isImage
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(file, fit: BoxFit.cover),
)
: Icon(
_getFileIcon(file.path),
size: 30,
color: _getFileIconColor(file.path),
),
),
const SizedBox(width: 12),
// File info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileName,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
fileSize,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(width: 8),
// Delete button
IconButton(
icon: const Icon(Icons.delete_outline),
color: Colors.red,
onPressed: () => _deleteFile(file.path, isSent),
),
],
),
),
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
"Downloads",
style: TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.white,
elevation: 1,
foregroundColor: Colors.black,
bottom: TabBar(
controller: _tabController,
labelColor: const Color(0xFF630A73),
unselectedLabelColor: Colors.grey,
indicatorColor: const Color(0xFF630A73),
tabs: [
Tab(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.upload, size: 18),
const SizedBox(width: 6),
Text("Sent (${sentFiles.length})"),
],
),
),
Tab(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.download, size: 18),
const SizedBox(width: 6),
Text("Received (${receivedFiles.length})"),
],
),
),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildFileList(sentFiles, true),
_buildFileList(receivedFiles, false),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _loadFiles,
backgroundColor: const Color(0xFF630A73),
child: const Icon(Icons.refresh, color: Colors.white),
),
);
}
}
+690
View File
@@ -0,0 +1,690 @@
// ⭐⭐⭐ UPDATED SWIPEABLE MESSAGE BUBBLE WITH IMAGE GRID VIEWER ⭐⭐⭐
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/model/chat_model.dart';
class SwipeableMessageBubble extends StatefulWidget {
final MessageModel message;
final DateTime msgDate;
final String Function(DateTime) formatTime;
final Widget Function(int, int) buildTick;
final VoidCallback onSwipe;
final Function(int?)? onParentTagTap;
final Function(List<String> imageUrls, int initialIndex)?
onImageTap; // 🖼️ NEW
const SwipeableMessageBubble({
super.key,
required this.message,
required this.msgDate,
required this.formatTime,
required this.buildTick,
required this.onSwipe,
this.onParentTagTap,
this.onImageTap, // 🖼️ NEW
});
@override
State<SwipeableMessageBubble> createState() => SwipeableMessageBubbleState();
}
class SwipeableMessageBubbleState extends State<SwipeableMessageBubble>
with SingleTickerProviderStateMixin {
double _dragExtent = 0;
late AnimationController _controller;
late Animation<double> _animation;
final Map<String, bool> _downloadingFiles = {};
final Map<String, bool> _downloadedFiles = {};
bool _isHighlighted = false;
void highlight() {
if (!mounted) return;
// Reset first to ensure it re-triggers if already highlighted
setState(() => _isHighlighted = false);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() => _isHighlighted = true);
Future.delayed(const Duration(milliseconds: 1500), () {
if (mounted) setState(() => _isHighlighted = false);
});
});
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_animation = Tween<double>(begin: 0, end: 0).animate(_controller)
..addListener(() {
setState(() {
_dragExtent = _animation.value;
});
});
_checkDownloadedFiles();
}
Future<void> _checkDownloadedFiles() async {
for (var doc in widget.message.uploadedDocuments) {
final existingPath = await DownloadHelper.checkFileExistsInReceived(
doc.filePath,
);
if (existingPath != null && mounted) {
setState(() {
_downloadedFiles[doc.fileName] = true;
});
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (!mounted) return;
final isUserMessage = widget.message.chatBy == "user";
final delta = details.primaryDelta ?? 0;
if ((isUserMessage && delta < 0) || (!isUserMessage && delta > 0)) {
setState(() {
_dragExtent += delta;
_dragExtent = _dragExtent.clamp(
isUserMessage ? -80.0 : 0.0,
isUserMessage ? 0.0 : 80.0,
);
});
}
}
void _handleDragEnd(DragEndDetails details) {
if (!mounted) return;
final threshold = 60.0;
if (_dragExtent.abs() > threshold) {
widget.onSwipe();
}
_animation = Tween<double>(
begin: _dragExtent,
end: 0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller.reset();
_controller.forward();
}
// 🖼️ NEW: Handle image tap - open grid viewer
void _handleImageTap(int tappedIndex) {
if (widget.onImageTap == null) return;
// Get all image URLs from uploaded documents
final imageUrls = widget.message.uploadedDocuments
.where((doc) => DownloadHelper.isImageFile(doc.fileName))
.map((doc) => doc.filePath)
.toList();
if (imageUrls.isNotEmpty) {
widget.onImageTap!(imageUrls, tappedIndex);
}
}
Future<void> _handleFileView(String url, String fileName) async {
if (!mounted) return;
try {
print("👁️ Opening file: $fileName");
final existingPath = await DownloadHelper.checkFileExistsInReceived(url);
if (existingPath != null) {
await DownloadHelper.openDownloadedFile(existingPath);
} else {
final sendFolder = await DownloadHelper.getSendFolder();
final localPath = '${sendFolder.path}/$fileName';
final file = File(localPath);
if (await file.exists()) {
await DownloadHelper.openDownloadedFile(localPath);
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("File not found locally"),
backgroundColor: Colors.orange,
duration: Duration(seconds: 2),
),
);
}
}
}
} catch (e) {
print("❌ View error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Could not open file: $e"),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleFileDownload(String url, String fileName) async {
if (!mounted) return;
setState(() {
_downloadingFiles[fileName] = true;
});
try {
print("🔗 Downloading from: $url");
final result = await DownloadHelper.downloadFileToReceived(url);
if (!mounted) return;
if (result['success'] == true) {
setState(() {
_downloadedFiles[fileName] = true;
});
if (mounted) {
if (result['isNewDownload'] == false) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Opening $fileName"),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Downloaded $fileName"),
backgroundColor: Colors.green,
duration: const Duration(seconds: 1),
),
);
}
}
}
} catch (e) {
print("❌ Download error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Download failed: $e"),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_downloadingFiles[fileName] = false;
});
}
}
}
Future<void> _handleDownloadedFileView(String url, String fileName) async {
if (!mounted) return;
try {
final existingPath = await DownloadHelper.checkFileExistsInReceived(url);
if (existingPath != null) {
await DownloadHelper.openDownloadedFile(existingPath);
}
} catch (e) {
print("❌ View error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Could not open file: $e"),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
final isUserMessage = widget.message.chatBy == "user";
final hasDocuments = widget.message.uploadedDocuments.isNotEmpty;
print("💬 Building message bubble:");
print(" - ID: ${widget.message.id}");
print(" - chatBy: ${widget.message.chatBy}");
print(
" - uploadedDocuments count: ${widget.message.uploadedDocuments.length}",
);
return Stack(
children: [
if (_dragExtent.abs() > 10)
Positioned(
right: isUserMessage ? 20 : null,
left: isUserMessage ? null : 20,
top: 0,
bottom: 0,
child: Center(
child: Icon(
Icons.reply_rounded,
color: Colors.grey[600],
size: 24,
),
),
),
GestureDetector(
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
child: Transform.translate(
offset: Offset(_dragExtent, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.message.user != null)
Padding(
padding: const EdgeInsets.only(left: 5, right: 5),
child: Align(
alignment: isUserMessage
? Alignment.centerRight
: Alignment.centerLeft,
child: Text(
widget.message.user!.name,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
),
),
if (widget.message.parentTag != null)
GestureDetector(
onTap: () {
if (widget.onParentTagTap != null) {
widget.onParentTagTap!(
widget.message.parentTag?.id ?? widget.message.tagId,
);
}
},
child: Align(
alignment: isUserMessage
? Alignment.centerRight
: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(
bottom: 4,
left: 5,
right: 5,
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.60,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF630A73).withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 3,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF630A73),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.message.parentTag!.type == "file")
Row(
children: [
Icon(
Icons.insert_drive_file,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget
.message
.parentTag!
.fileName ??
"File",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
),
],
),
Text(
widget.message.parentTag!.message,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
),
],
),
),
),
),
Align(
alignment: isUserMessage
? Alignment.centerRight
: Alignment.centerLeft,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.70,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 14,
),
decoration: BoxDecoration(
color: _isHighlighted
? Colors.green.withOpacity(0.2)
: (isUserMessage
? const Color(0xFFFFF8E2)
: Colors.white),
border: Border.all(
color: _isHighlighted
? Colors.green.withOpacity(0.5)
: (isUserMessage
? const Color(0xFFF9D9C8)
: const Color(0xFFE2E2E2)),
),
borderRadius: BorderRadius.circular(10),
boxShadow: isUserMessage
? []
: const [
BoxShadow(
color: Color(0x40979797),
blurRadius: 3.6,
offset: Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ⭐⭐⭐ DISPLAY uploadedDocuments WITH IMAGE GRID SUPPORT
if (hasDocuments)
...widget.message.uploadedDocuments.asMap().entries.map((
entry,
) {
final index = entry.key;
final doc = entry.value;
final isImage = DownloadHelper.isImageFile(
doc.fileName,
);
final isDownloading =
_downloadingFiles[doc.fileName] ?? false;
final isDownloaded =
_downloadedFiles[doc.fileName] ?? false;
// 🖼️ Calculate image index (only count images before this one)
final imageIndex = widget
.message
.uploadedDocuments
.sublist(0, index)
.where(
(d) =>
DownloadHelper.isImageFile(d.fileName),
)
.length;
return GestureDetector(
onTap: isImage
? () =>
_handleImageTap(
imageIndex,
) // 🖼️ Open grid viewer
: (isUserMessage
? () => _handleFileView(
doc.filePath,
doc.fileName,
)
: null),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
child: isImage
? Stack(
children: [
ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network(
doc.filePath,
width: 150,
height: 100,
fit: BoxFit.cover,
errorBuilder:
(context, error, stack) {
return Container(
width: 200,
height: 150,
color: Colors.grey[300],
child: const Center(
child: Icon(
Icons.broken_image,
),
),
);
},
),
),
// ⭐ Download/View icon for receiver messages
if (!isUserMessage)
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: isDownloading
? null
: isDownloaded
? () =>
_handleDownloadedFileView(
doc.filePath,
doc.fileName,
)
: () =>
_handleFileDownload(
doc.filePath,
doc.fileName,
),
child: Container(
padding:
const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black
.withOpacity(0.6),
shape: BoxShape.circle,
),
child: isDownloading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<
Color
>(
Colors
.white,
),
),
)
: Icon(
isDownloaded
? Icons
.visibility
: Icons
.download,
color: Colors.white,
size: 20,
),
),
),
),
],
)
: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(
8,
),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Row(
children: [
Icon(
Icons.insert_drive_file,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
doc.fileName,
style: const TextStyle(
fontSize: 13,
fontWeight:
FontWeight.w600,
),
),
Text(
doc.fileType
.toUpperCase(),
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
),
),
],
),
),
GestureDetector(
onTap: isDownloading
? null
: () => isUserMessage
? _handleFileView(
doc.filePath,
doc.fileName,
)
: isDownloaded
? _handleDownloadedFileView(
doc.filePath,
doc.fileName,
)
: _handleFileDownload(
doc.filePath,
doc.fileName,
),
child: isDownloading
? const SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(
strokeWidth: 2,
),
)
: Icon(
isUserMessage
? Icons.open_in_new
: isDownloaded
? Icons.visibility
: Icons.download,
color: Colors.grey[600],
size: 20,
),
),
],
),
),
),
);
}).toList(),
if (widget.message.message.isNotEmpty)
Text(
widget.message.message,
style: const TextStyle(fontSize: 15),
),
const SizedBox(height: 3),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
widget.formatTime(widget.msgDate),
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
const SizedBox(width: 4),
widget.buildTick(
widget.message.isDelivered,
widget.message.isRead,
),
],
),
],
),
),
),
),
],
),
),
),
],
);
}
}
File diff suppressed because it is too large. Load diff
+296
View File
@@ -0,0 +1,296 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// WebSocket service for real-time chat communication
class ChatWebSocketService {
static final ChatWebSocketService _instance =
ChatWebSocketService._internal();
factory ChatWebSocketService() => _instance;
ChatWebSocketService._internal();
WebSocketChannel? _channel;
StreamSubscription? _subscription;
Timer? _pingTimer;
Timer? _reconnectTimer;
bool _isConnected = false;
bool _isIntentionalClose = false;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _reconnectDelay = Duration(seconds: 3);
// Callbacks
Function(Map<String, dynamic>)? onMessageReceived;
Function()? onConnected;
Function()? onDisconnected;
Function(dynamic error)? onError;
String? _currentChatId;
String? _wsUrl;
/// Check if WebSocket is currently connected
bool get isConnected => _isConnected;
/// Get current chat ID
String? get currentChatId => _currentChatId;
/// Connect to WebSocket for a specific chat
Future<void> connect({
required String chatId,
String baseUrl = "wss://taxglide.amrithaa.net:443/reverb",
String appKey = "2yj0lyzc9ylw2h03ts6i",
}) async {
try {
// Close existing connection if any
await disconnect();
_currentChatId = chatId;
_wsUrl = "$baseUrl/app/$appKey?protocol=7&client=js&version=1.0";
_isIntentionalClose = false;
_reconnectAttempts = 0;
await _establishConnection();
} catch (e) {
debugPrint("❌ Connection error: $e");
onError?.call(e);
_scheduleReconnect();
}
}
/// Establish WebSocket connection
Future<void> _establishConnection() async {
try {
debugPrint("🔌 Connecting to: $_wsUrl");
_channel = WebSocketChannel.connect(Uri.parse(_wsUrl!));
// Listen to messages
_subscription = _channel!.stream.listen(
_handleMessage,
onError: _handleError,
onDone: _handleDisconnection,
cancelOnError: false,
);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
_isConnected = true;
_reconnectAttempts = 0;
debugPrint("✅ WebSocket connected");
// Subscribe to chat channel
_subscribeToChannel();
// Start ping timer
_startPingTimer();
onConnected?.call();
} catch (e) {
debugPrint("❌ Failed to establish connection: $e");
_isConnected = false;
rethrow;
}
}
/// Handle incoming messages
void _handleMessage(dynamic data) {
try {
debugPrint("📨 Received: $data");
final Map<String, dynamic> message = json.decode(data);
// Handle Pusher ping
if (message['event'] == 'pusher:ping') {
_sendPong();
return;
}
// Handle connection established
if (message['event'] == 'pusher:connection_established') {
debugPrint("🔗 Connection established");
return;
}
// Handle subscription succeeded
if (message['event'] == 'pusher_internal:subscription_succeeded') {
debugPrint("✅ Subscribed to chat.${_currentChatId}");
return;
}
// Handle custom events (your chat messages)
if (message['event'] != null &&
!message['event'].toString().startsWith('pusher')) {
onMessageReceived?.call(message);
}
} catch (e) {
debugPrint("❌ Error parsing message: $e");
onError?.call(e);
}
}
/// Handle WebSocket errors
void _handleError(dynamic error) {
debugPrint("❌ WebSocket error: $error");
_isConnected = false;
onError?.call(error);
if (!_isIntentionalClose) {
_scheduleReconnect();
}
}
/// Handle disconnection
void _handleDisconnection() {
debugPrint("🔌 WebSocket disconnected");
_isConnected = false;
_stopPingTimer();
onDisconnected?.call();
if (!_isIntentionalClose) {
_scheduleReconnect();
}
}
/// Subscribe to chat channel
void _subscribeToChannel() {
if (_currentChatId == null) return;
final subscribeMessage = {
'event': 'pusher:subscribe',
'data': {'channel': 'chat.$_currentChatId'},
};
_sendMessage(subscribeMessage);
debugPrint("📡 Subscribing to chat.$_currentChatId");
}
/// Send pong response to ping
void _sendPong() {
final pongMessage = {'event': 'pusher:pong', 'data': {}};
_sendMessage(pongMessage);
debugPrint("🏓 Pong sent");
}
/// Start periodic ping timer
void _startPingTimer() {
_pingTimer?.cancel();
_pingTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
if (_isConnected) {
// Server will send ping, we just respond with pong
debugPrint("💓 Heartbeat check");
}
});
}
/// Stop ping timer
void _stopPingTimer() {
_pingTimer?.cancel();
_pingTimer = null;
}
/// Schedule reconnection attempt
void _scheduleReconnect() {
if (_isIntentionalClose) return;
if (_reconnectAttempts >= _maxReconnectAttempts) {
debugPrint("❌ Max reconnection attempts reached");
onError?.call(
"Failed to reconnect after $_maxReconnectAttempts attempts",
);
return;
}
_reconnectTimer?.cancel();
_reconnectAttempts++;
debugPrint(
"🔄 Scheduling reconnect attempt $_reconnectAttempts in ${_reconnectDelay.inSeconds}s",
);
_reconnectTimer = Timer(_reconnectDelay, () async {
if (!_isIntentionalClose && _wsUrl != null && _currentChatId != null) {
debugPrint("🔄 Attempting to reconnect...");
try {
await _establishConnection();
} catch (e) {
debugPrint("❌ Reconnect failed: $e");
_scheduleReconnect();
}
}
});
}
/// Send a message through WebSocket
void _sendMessage(Map<String, dynamic> message) {
if (_channel == null || !_isConnected) {
debugPrint("⚠️ Cannot send message: Not connected");
return;
}
try {
final jsonMessage = json.encode(message);
_channel!.sink.add(jsonMessage);
debugPrint("📤 Sent: $jsonMessage");
} catch (e) {
debugPrint("❌ Error sending message: $e");
onError?.call(e);
}
}
/// Send a custom event to the channel
void sendEvent({
required String eventName,
required Map<String, dynamic> data,
}) {
final message = {
'event': eventName,
'data': data,
'channel': 'chat.$_currentChatId',
};
_sendMessage(message);
}
/// Disconnect from WebSocket
Future<void> disconnect() async {
_isIntentionalClose = true;
_reconnectTimer?.cancel();
_stopPingTimer();
await _subscription?.cancel();
await _channel?.sink.close();
_channel = null;
_subscription = null;
_isConnected = false;
_currentChatId = null;
debugPrint("🔌 WebSocket disconnected intentionally");
}
/// Switch to a different chat channel
Future<void> switchChannel(String newChatId) async {
if (newChatId == _currentChatId) return;
debugPrint("🔄 Switching from chat.$_currentChatId to chat.$newChatId");
// Unsubscribe from current channel
if (_currentChatId != null) {
final unsubscribeMessage = {
'event': 'pusher:unsubscribe',
'data': {'channel': 'chat.$_currentChatId'},
};
_sendMessage(unsubscribeMessage);
}
// Update chat ID and subscribe to new channel
_currentChatId = newChatId;
_subscribeToChannel();
}
/// Dispose and cleanup
void dispose() {
disconnect();
}
}
@@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/comman_webscoket.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/consts/notification_webscoket.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CommanChatBox extends ConsumerStatefulWidget {
final String? chatId;
const CommanChatBox({super.key, this.chatId});
@override
ConsumerState<CommanChatBox> createState() => _CommanChatBoxState();
}
class _CommanChatBoxState extends ConsumerState<CommanChatBox> {
bool _isWebSocketInitialized = false;
@override
void initState() {
super.initState();
_initializeWebSocket();
}
/// ✅ Initialize WebSocket on widget load
Future<void> _initializeWebSocket() async {
if (_isWebSocketInitialized) return;
try {
final String? userId = await LocalStore().getUserId();
if (userId != null && userId.isNotEmpty) {
final wsService = CommonWebSocketService();
// Setup message handler to refresh count
wsService.onMessageReceived = (message) {
debugPrint("📨 WebSocket message received: $message");
_refreshCount();
};
// Connect WebSocket
await wsService.connect(userId: userId);
_isWebSocketInitialized = true;
debugPrint("✅ WebSocket initialized for chat box");
// ✅ Subscribe NotificationWebSocket to the chat channel early
// so notifications arrive from ANY screen, not just the chat screen.
final String chatId =
widget.chatId ?? await LocalStore().getGeneralChatId() ?? '0';
if (chatId != '0' && chatId.isNotEmpty) {
NotificationWebSocket().watchChatChannel(chatId);
debugPrint(
"👁 CommanChatBox: watching chat channel $chatId for notifications",
);
}
}
} catch (e) {
debugPrint("❌ Failed to initialize WebSocket: $e");
}
}
/// ✅ Refresh count provider
void _refreshCount() {
try {
final chatId = widget.chatId ?? "0";
// Invalidate the count provider to trigger API call
ref.invalidate(countProvider(chatId));
debugPrint("🔄 Count refreshed for chatId: $chatId");
} catch (e) {
debugPrint("❌ Error refreshing count: $e");
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String?>(
future: widget.chatId != null
? Future.value(widget.chatId)
: LocalStore().getGeneralChatId(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final id = snapshot.data ?? "0";
final countAsync = ref.watch(countProvider(id));
return Positioned(
right: 20,
bottom: 120,
child: Stack(
clipBehavior: Clip.none,
children: [
// Main Chat Button
GestureDetector(
onTap: () async {
int cid = int.tryParse(id) ?? 0;
ref.invalidate(chatMessagesProvider);
// Navigate to chat screen
Get.to(() => LiveChatScreen(chatid: cid, fileid: '0'))?.then((
_,
) {
ref.read(notificationTriggerProvider.notifier).state++;
});
},
child: Container(
width: 53,
height: 53,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFBD49F7), Color(0xFF630A73)],
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.2,
offset: const Offset(0, 1),
),
],
),
child: Container(
margin: const EdgeInsets.all(3),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Image.asset(AppAssets.generalchatimage),
),
),
),
// 🔴 Count Badge
Positioned(
right: -2,
top: -2,
child: countAsync.when(
loading: () => const SizedBox(),
error: (_, __) => const SizedBox(),
data: (data) {
if (data.count == 0) return const SizedBox();
return Container(
padding: const EdgeInsets.all(5),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
data.count.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
);
},
),
),
],
),
);
},
);
}
@override
void dispose() {
// Don't disconnect WebSocket here as it's shared across app
super.dispose();
}
}
@@ -0,0 +1,451 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/controller/api_contoller.dart'; // ✅ Needed for providers
import 'package:taxglide/view/Main_controller/comman_chat_box.dart';
import 'package:taxglide/view/screens/history/serivces_status_screen.dart'
show ServicesStatusScreen;
import 'package:taxglide/view/screens/home_screen.dart';
import 'package:taxglide/view/screens/list_service_screen.dart';
import 'package:taxglide/view/screens/profile/employee_profile/employee_profile_screen.dart';
import 'package:taxglide/view/screens/profile/profile_screen.dart';
class MainController extends ConsumerStatefulWidget {
final Widget? child; // Optional child (like ServiceRequestScreen)
final int? initialIndex; // Which tab to show initially
final int? sourceTabIndex; // Source tab (Home=0, Services=1)
const MainController({
Key? key,
this.child,
this.initialIndex,
this.sourceTabIndex,
}) : super(key: key);
@override
ConsumerState<MainController> createState() => _MainControllerState();
}
class _MainControllerState extends ConsumerState<MainController> {
int currentIndex = 0;
DateTime? lastBackPressed;
String? userRole;
/// 📌 Bottom bar labels
final List<String> _labels = [
'Home',
'Services',
'Service Details',
'Profile',
];
/// 📌 Screens list
List<Widget> _screens = [];
@override
void initState() {
super.initState();
currentIndex = widget.initialIndex ?? 0;
_loadRole();
}
/// 🔹 Load user role from local storage
Future<void> _loadRole() async {
userRole = await LocalStore().getRole(); // "employee" or "user"
_initScreens();
setState(() {});
}
/// 🔹 Initialize screens based on role
void _initScreens() {
_screens = [
const HomeScreen(),
ListServiceScreen(key: UniqueKey()),
ServicesStatusScreen(),
_getProfileScreen(), // ✅ role based
];
}
/// 🔹 Role based profile screen
Widget _getProfileScreen() {
if (userRole == "employee") {
return const EmployeeProfileScreen(); // 👈 employee profile
}
return const ProfileScreen(); // 👈 normal user profile
}
/// 🔄 Change tab index
void setBottomBarIndex(int index) {
setState(() {
currentIndex = index;
});
_refreshTab(index);
}
/// ✅ Refresh API data based on the current tab
void _refreshTab(int index) {
final container = ProviderScope.containerOf(context, listen: false);
switch (index) {
case 0:
// 🔁 Refresh Home screen APIs
container.refresh(profileProvider);
container.refresh(serviceListProvider);
ref.invalidate(chatMessagesProvider);
ref.invalidate(notificationCountProvider);
ref.invalidate(dashboardProvider);
break;
case 1:
// 🔁 Refresh ListServiceScreen APIs
container.refresh(serviceListProvider);
ref.invalidate(chatMessagesProvider);
ref.invalidate(dashboardProvider);
break;
case 2:
// 🔁 Refresh Service Status APIs
try {
ref.invalidate(serviceHistoryNotifierProvider);
ref.invalidate(serviceDetailProvider);
ref.invalidate(chatMessagesProvider);
ref.invalidate(dashboardProvider);
} catch (_) {}
break;
case 3:
// 🔁 Refresh Profile screen APIs
ref.invalidate(chatMessagesProvider);
container.refresh(profileProvider);
ref.invalidate(staffListProvider);
ref.invalidate(dashboardProvider);
break;
}
}
/// ✅ Handle Back button presses
Future<bool> _onWillPop() async {
// If inside ServiceRequestScreen → go back to originating tab
if (widget.child != null) {
Get.offAll(
() => MainController(initialIndex: widget.sourceTabIndex ?? 0),
);
return false;
}
// If not in Home tab, go back to Home
if (currentIndex != 0) {
setState(() => currentIndex = 0);
return false;
}
// Double back press to exit
DateTime now = DateTime.now();
if (lastBackPressed == null ||
now.difference(lastBackPressed!) > const Duration(seconds: 2)) {
lastBackPressed = now;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Press back again to exit"),
duration: Duration(seconds: 2),
),
);
return false;
}
await SystemNavigator.pop();
return true;
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
// ✅ Show loading while screens are being initialized
if (_screens.isEmpty) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
final Widget mainContent = widget.child ?? _screens[currentIndex];
final dashboardAsync = ref.watch(dashboardProvider);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white.withAlpha(55),
body: Stack(
children: [
mainContent,
/// ✅ Bottom Navigation Bar
Positioned(
bottom: 0,
left: 0,
child: SafeArea(
top: false,
child: Container(
width: size.width,
height: 80,
child: Stack(
clipBehavior: Clip.none,
children: [
CustomPaint(
size: Size(size.width, 80),
painter: BNBCustomPainter(),
),
Center(
heightFactor: 0.6,
child: GestureDetector(
onTap: () {},
child: CustomPaint(
painter: PentagonPainter(
color: const Color(0xFF61277A),
),
child: Container(
width: 70,
height: 70,
alignment: Alignment.center,
child: Image.asset(
AppAssets.maincontroller,
color: Colors.white,
fit: BoxFit.contain,
height: 40,
width: 40,
),
),
),
),
),
SizedBox(
width: size.width,
height: 80,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(Icons.home, 0, _labels[0]),
_buildNavItem(Icons.list_alt_sharp, 1, _labels[1]),
SizedBox(width: size.width * 0.20),
_buildNavItem(Icons.history_edu, 2, _labels[2]),
_buildNavItem(Icons.person_2, 3, _labels[3]),
],
),
),
],
),
),
),
),
dashboardAsync.when(
data: (dashboard) {
final int? chatIdInt = dashboard.generalChatId;
if (chatIdInt == null || chatIdInt == 0) {
return const SizedBox(); // ❌ No chat box
}
return CommanChatBox(
chatId: chatIdInt.toString(), // ✅ INT → STRING
);
},
loading: () => const SizedBox(),
error: (e, _) => const SizedBox(),
),
],
),
),
);
}
/// ✅ Navigation Logic with Refresh
Widget _buildNavItem(IconData icon, int index, String label) {
bool isSelected = currentIndex == index;
return GestureDetector(
onTap: () {
// 🔹 If inside ServiceRequestScreen
if (widget.child != null) {
if (widget.initialIndex == index) return;
if (widget.sourceTabIndex == 0 && index == 1) {
Get.offAll(() => const MainController(initialIndex: 1));
return;
}
if (widget.sourceTabIndex == 1 && index == 1) return;
Get.offAll(() => MainController(initialIndex: index));
return;
}
// 🔹 If same tab tapped again → refresh
if (currentIndex == index) {
_refreshTab(index);
return;
}
// 🔹 Switch to a different tab
setBottomBarIndex(index);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: isSelected
? Border.all(color: const Color(0xFF61277A), width: 2)
: null,
),
child: Icon(
icon,
color: isSelected
? const Color(0xFF61277A)
: const Color(0xFF6C7278),
size: isSelected ? 19.5 : 20.8,
),
),
if (isSelected)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
label,
style: const TextStyle(
color: Color(0xFF61277A),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
}
/// ✅ Custom Curved Bottom Navigation Painter
class BNBCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
Path path = Path();
path.moveTo(0, 20);
path.quadraticBezierTo(size.width * 0.20, 0, size.width * 0.35, 0);
path.quadraticBezierTo(size.width * 0.40, 0, size.width * 0.40, 20);
path.arcToPoint(
Offset(size.width * 0.60, 20),
radius: const Radius.circular(20.0),
clockwise: false,
);
path.quadraticBezierTo(size.width * 0.60, 0, size.width * 0.65, 0);
path.quadraticBezierTo(size.width * 0.80, 0, size.width, 20);
path.lineTo(size.width, size.height);
path.lineTo(0, size.height);
path.close();
canvas.drawShadow(path, Colors.black.withOpacity(0.2), 5, true);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
/// ✅ Pentagon Button Painter
/// ✅ Pentagon Button Painter
class PentagonPainter extends CustomPainter {
final Color color;
final double cornerRadius;
PentagonPainter({
required this.color,
this.cornerRadius = 8.0, // Adjust this value for more/less rounding
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final double w = size.width;
final double cx = w / 2;
final double cy = size.height / 2;
final double r = w / 2;
// Calculate pentagon vertices
List<Offset> vertices = [];
for (int i = 0; i < 5; i++) {
double angle = (72 * i - 90) * math.pi / 180;
double x = cx + r * 0.95 * math.cos(angle);
double y = cy + r * 0.95 * math.sin(angle);
vertices.add(Offset(x, y));
}
// Create path with rounded corners
final path = Path();
for (int i = 0; i < vertices.length; i++) {
final current = vertices[i];
final next = vertices[(i + 1) % vertices.length];
final prev = vertices[(i - 1 + vertices.length) % vertices.length];
// Calculate direction vectors
final toCurrent = Offset(current.dx - prev.dx, current.dy - prev.dy);
final toNext = Offset(next.dx - current.dx, next.dy - current.dy);
// Normalize and scale by corner radius
final lengthToCurrent = math.sqrt(
toCurrent.dx * toCurrent.dx + toCurrent.dy * toCurrent.dy,
);
final lengthToNext = math.sqrt(
toNext.dx * toNext.dx + toNext.dy * toNext.dy,
);
final normalizedToCurrent = Offset(
toCurrent.dx / lengthToCurrent,
toCurrent.dy / lengthToCurrent,
);
final normalizedToNext = Offset(
toNext.dx / lengthToNext,
toNext.dy / lengthToNext,
);
// Points before and after the corner
final beforeCorner = Offset(
current.dx - normalizedToCurrent.dx * cornerRadius,
current.dy - normalizedToCurrent.dy * cornerRadius,
);
final afterCorner = Offset(
current.dx + normalizedToNext.dx * cornerRadius,
current.dy + normalizedToNext.dy * cornerRadius,
);
if (i == 0) {
path.moveTo(beforeCorner.dx, beforeCorner.dy);
} else {
path.lineTo(beforeCorner.dx, beforeCorner.dy);
}
// Draw rounded corner using quadratic bezier
path.quadraticBezierTo(
current.dx,
current.dy,
afterCorner.dx,
afterCorner.dy,
);
}
path.close();
canvas.drawShadow(path, Colors.black.withOpacity(0.3), 4, true);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
+204
View File
@@ -0,0 +1,204 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/router/consts_routers.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/services/notification_service.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart';
import 'package:taxglide/view/screens/history/detail_screen.dart'
show DetailScreen;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'Mahi_chat/live_chat_screen.dart';
class FlashScreen extends ConsumerStatefulWidget {
const FlashScreen({super.key});
@override
ConsumerState<FlashScreen> createState() => _FlashScreenState();
}
class _FlashScreenState extends ConsumerState<FlashScreen> {
final LocalStore _localStore = LocalStore();
@override
void initState() {
super.initState();
_navigateNext();
}
Future<void> _navigateNext() async {
try {
await Future.delayed(const Duration(seconds: 3));
if (!mounted) return;
String? token;
String? role;
try {
token = await _localStore.getToken();
role = await _localStore.getRole();
} catch (e) {
debugPrint("⚠️ Error reading stored data: $e");
// Clear corrupted data
await _localStore.clearAll();
}
if (!mounted) return;
// Check if token and role are valid
if (token != null &&
token.isNotEmpty &&
token != 'null' &&
role != null &&
role.isNotEmpty &&
role != 'null') {
debugPrint("✅ User logged in → Checking for initial notification");
try {
// Trigger notification refresh
ref.read(notificationTriggerProvider.notifier).state++;
ref.invalidate(chatMessagesProvider);
// ✅ Check if app was opened from notification (terminated state)
final notificationService = Get.find<NotificationService>();
RemoteMessage? initialMessage = notificationService.initialMessage;
if (initialMessage != null) {
debugPrint("📥 Processing initial notification");
await _handleInitialNotification(initialMessage.data);
} else {
// Navigate to main controller normally
Get.offAll(() => MainController());
}
} catch (e) {
debugPrint("⚠️ Error checking initial notification: $e");
// If no initial notification or error, go to MainController
Get.offAll(() => MainController());
}
} else {
debugPrint("🔒 No valid login data → Navigating to Login");
// Clear any invalid stored data
await _localStore.clearAll();
Get.offNamed(ConstRouters.login);
}
} catch (e) {
debugPrint("❌ Critical error in splash screen: $e");
// Fallback: always go to login if there's any error
if (mounted) {
await _localStore.clearAll();
Get.offNamed(ConstRouters.login);
}
}
}
/// Handle notification when app was opened from terminated state
Future<void> _handleInitialNotification(Map<String, dynamic> data) async {
String? type = data['type'];
String? idStr = data['id']?.toString();
if (type == null || idStr == null) {
debugPrint("⚠️ Invalid initial notification data");
Get.offAll(() => MainController());
return;
}
debugPrint("📍 Initial notification type: $type, ID: $idStr");
if (type == 'chat') {
// Navigate to chat
Get.offAll(() => MainController());
await Future.delayed(const Duration(milliseconds: 300));
Get.to(() => LiveChatScreen(chatid: int.parse(idStr), fileid: '0'));
} else if (type == 'service') {
// Navigate to service detail
int serviceId = int.tryParse(idStr) ?? 0;
Get.offAll(
() => MainController(
initialIndex: 2,
child: DetailScreen(id: serviceId, sourceTabIndex: 2),
),
);
} else {
// Unknown type
Get.offAll(() => MainController());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFF8F0),
body: Stack(
children: [
// Background gradient
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFF8F0), Color(0xFFF5F0FF)],
),
),
),
// Top blur circle
Positioned(
top: -431,
left: -256,
child: Container(
width: 582,
height: 582,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.backgroundgrainetflashtop.withOpacity(0.9),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 368.3, sigmaY: 368.3),
child: Container(color: Colors.transparent),
),
),
),
// Bottom blur circle
Positioned(
top: 638,
left: 149,
child: Container(
width: 934,
height: 934,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.backgroundgrainetflashbottom.withOpacity(0.94),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 591.0, sigmaY: 591.0),
child: Container(color: Colors.transparent),
),
),
),
// Center logo
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Image.asset(
AppAssets.taxgildelogo,
width: 361.09,
height: 333.94,
fit: BoxFit.contain,
),
),
),
],
),
);
}
}
@@ -0,0 +1,696 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/model/chat_model.dart';
import 'package:taxglide/view/Mahi_chat/chat_profile_screen.dart';
import 'package:taxglide/view/Mahi_chat/file_images_screen.dart';
import 'package:taxglide/view/Mahi_chat/webscoket.dart';
// ⭐ Provider to hold the tagged message
final taggedMessageProvider = StateProvider<MessageModel?>((ref) => null);
// ⭐ Global key map to track message positions (if you want from outside)
final messageKeysProvider = StateProvider<Map<int, GlobalKey>>((ref) => {});
class CompletedLiveChatScreen extends ConsumerStatefulWidget {
final int chatid;
final String? fileid;
const CompletedLiveChatScreen({
super.key,
required this.chatid,
required this.fileid,
});
@override
ConsumerState<CompletedLiveChatScreen> createState() =>
_CompletedLiveChatScreenState();
}
class _CompletedLiveChatScreenState
extends ConsumerState<CompletedLiveChatScreen>
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
bool get wantKeepAlive => true;
final ScrollController scrollController = ScrollController();
final ChatWebSocketService _wsService = ChatWebSocketService();
bool showScrollButton = false;
bool userIsReading = false;
bool _isInitialLoad = true;
bool _isPaginationLoading = false;
bool _hasNewMessage = false;
// For reply scroll - FIXED: Use unique key per message
final Map<String, GlobalKey> _messageKeys = {};
// ⭐⭐⭐ Key variables for smooth pagination
final double _paginationTriggerOffset = 300; // Start loading 300px before top
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initializeWebSocket();
scrollController.addListener(_handleScroll);
}
// ⭐⭐⭐ Enhanced scroll handler with early pagination trigger
void _handleScroll() {
if (!scrollController.hasClients) return;
double position = scrollController.position.pixels;
double bottom = scrollController.position.maxScrollExtent;
double top = scrollController.position.minScrollExtent;
// ⭐⭐⭐ EARLY PAGINATION: Load before reaching exact top
if (position <= top + _paginationTriggerOffset && !_isPaginationLoading) {
_triggerPagination();
}
// Show scroll to bottom button
if (position < bottom - 50) {
if (!userIsReading) {
setState(() {
userIsReading = true;
showScrollButton = true;
});
}
} else {
if (userIsReading) {
setState(() {
userIsReading = false;
showScrollButton = false;
});
}
}
}
// ⭐⭐⭐ Smooth pagination with scroll position preservation (NO JUMP TO BOTTOM)
Future<void> _triggerPagination() async {
final notifier = ref.read(chatMessagesProvider(widget.chatid).notifier);
if (!notifier.hasNextPage) return;
if (_isPaginationLoading) return;
if (!scrollController.hasClients) return;
setState(() {
_isPaginationLoading = true;
// userIsReading remains true while scrolling old messages
});
// Save current scroll metrics BEFORE loading more messages
final double beforeMaxExtent = scrollController.position.maxScrollExtent;
final double currentOffset = scrollController.position.pixels;
try {
await notifier.loadMessages(); // This should prepend older messages
// Restore scroll position after new messages loaded
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !scrollController.hasClients) return;
final double afterMaxExtent = scrollController.position.maxScrollExtent;
final double diff = afterMaxExtent - beforeMaxExtent;
// Keep user at same visual message position
final double newOffset = currentOffset + diff;
scrollController.jumpTo(
newOffset.clamp(
scrollController.position.minScrollExtent,
scrollController.position.maxScrollExtent,
),
);
if (mounted) {
setState(() {
_isPaginationLoading = false;
});
}
});
} catch (e) {
debugPrint("❌ Pagination error: $e");
if (mounted) {
setState(() {
_isPaginationLoading = false;
});
}
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
debugPrint("📱 App Lifecycle State: $state");
if (state == AppLifecycleState.resumed) {
debugPrint("🔄 App resumed - checking for new messages");
if (_hasNewMessage) {
_silentRefresh();
_hasNewMessage = false;
}
}
}
Future<void> _silentRefresh() async {
try {
debugPrint("🔄 Starting silent refresh...");
await ref.read(chatMessagesProvider(widget.chatid).notifier).refresh();
debugPrint("✅ Silent refresh completed");
// Only auto-scroll to bottom on new messages if user is NOT reading old ones
if (!userIsReading && mounted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
} catch (e) {
debugPrint("❌ Error during silent refresh: $e");
}
}
Future<void> _initializeWebSocket() async {
try {
_wsService.onMessageReceived = (Map<String, dynamic> message) async {
debugPrint("📨 WebSocket message received: $message");
try {
final event = message['event'] as String?;
if (event == 'message.sent' || event == 'message.received') {
debugPrint("🔔 New message detected!");
if (WidgetsBinding.instance.lifecycleState ==
AppLifecycleState.resumed) {
debugPrint("✅ Screen active - refreshing now");
await _silentRefresh();
} else {
debugPrint("⏳ Screen not active - will refresh on resume");
_hasNewMessage = true;
}
}
} catch (e) {
debugPrint("❌ Error handling WebSocket message: $e");
}
};
_wsService.onConnected = () {
debugPrint("✅ WebSocket Connected");
};
_wsService.onDisconnected = () {
debugPrint("🔌 WebSocket Disconnected");
};
_wsService.onError = (error) {
debugPrint("❌ WebSocket Error: $error");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Connection error: $error"),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
}
};
await _wsService.connect(chatId: widget.chatid.toString());
} catch (e) {
debugPrint("❌ Error initializing WebSocket: $e");
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_wsService.disconnect();
scrollController.dispose();
super.dispose();
}
void scrollToBottom({bool animated = false}) {
if (!scrollController.hasClients) return;
setState(() {
_isPaginationLoading = false;
userIsReading = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!scrollController.hasClients) return;
if (animated) {
scrollController
.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
)
.then((_) {
Future.delayed(const Duration(milliseconds: 50), () {
if (scrollController.hasClients && mounted) {
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
}
});
});
} else {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
Future.delayed(const Duration(milliseconds: 10), () {
if (scrollController.hasClients && mounted) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
});
}
// ⭐ FIXED: Use unique string key instead of int
void scrollToMessage(String messageKey) {
final key = _messageKeys[messageKey];
if (key != null && key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
alignment: 0.3,
);
}
}
// ⭐ FIXED: Generate unique string key for each message
GlobalKey _getKeyForMessage(String messageKey) {
if (!_messageKeys.containsKey(messageKey)) {
_messageKeys[messageKey] = GlobalKey();
}
return _messageKeys[messageKey]!;
}
// ⭐ FIXED: Generate unique key string from message
String _getMessageKeyString(MessageModel msg, int index) {
// Use combination of ID, index, and document count for uniqueness
final id = msg.id;
final docCount = msg.uploadedDocuments.length;
final createdAt = msg.createdAt ?? '';
return 'msg_${id}_${index}_${docCount}_$createdAt';
}
String formatMessageDate(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final msgDay = DateTime(date.year, date.month, date.day);
if (msgDay == today) return "Today";
if (msgDay == today.subtract(const Duration(days: 1))) return "Yesterday";
return "${date.day}-${date.month}-${date.year}";
}
String formatTime(DateTime time) {
final hour = time.hour % 12 == 0 ? 12 : time.hour % 12;
final minute = time.minute.toString().padLeft(2, '0');
final period = time.hour >= 12 ? "PM" : "AM";
return "$hour:$minute $period";
}
Widget buildTick(int isDelivered, int isRead) {
if (isRead == 1) {
return const Icon(Icons.done_all, size: 16, color: Colors.blue);
} else if (isDelivered == 1) {
return const Icon(Icons.done_all, size: 16, color: Colors.grey);
} else {
return const Icon(Icons.check, size: 16, color: Colors.grey);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
final chatAsync = ref.watch(chatMessagesProvider(widget.chatid));
final taggedMessage = ref.watch(taggedMessageProvider);
return Scaffold(
appBar: AppBar(
title: Text(
(widget.fileid != null &&
widget.fileid.toString().isNotEmpty &&
widget.fileid.toString() != "0")
? 'File ID : ${widget.fileid.toString()}'
: "Live Chat",
style: const TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.white,
elevation: 1,
foregroundColor: Colors.black,
centerTitle: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 12), // ✅ Right end spacing
child: GestureDetector(
onTap: () {
Get.to(ChatProfileScreen(chatid: widget.chatid));
},
child: Container(
width: 37.446807861328125,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(6.81),
border: Border.all(
color: const Color(0xFF1E780C),
width: 0.85,
),
),
child: const Icon(Icons.person, color: Color(0xFF1E780C)),
),
),
),
],
),
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.backgroundimages),
fit: BoxFit.cover,
),
),
),
Column(
children: [
Expanded(
child: chatAsync.when(
loading: () => const Center(child: SizedBox.shrink()),
error: (e, _) => Center(child: Text(e.toString())),
data: (messages) {
// ✅ Only first load scrolls to bottom, NOT during pagination / scroll up
if (_isInitialLoad && !_isPaginationLoading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients && mounted) {
scrollController.jumpTo(
scrollController.position.maxScrollExtent,
);
}
});
_isInitialLoad = false;
}
return Stack(
children: [
ListView.builder(
controller: scrollController,
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.only(
left: 15,
right: 15,
top: 15,
bottom: 15,
),
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
final msgDate = DateTime.parse(msg.createdAt!);
final showDateHeader =
index == 0 ||
formatMessageDate(
DateTime.parse(
messages[index - 1].createdAt!,
),
) !=
formatMessageDate(msgDate);
// ⭐ FIXED: Generate unique key string
final messageKeyString = _getMessageKeyString(
msg,
index,
);
return Column(
key: _getKeyForMessage(messageKeyString),
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (showDateHeader)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.06),
borderRadius: BorderRadius.circular(
12,
),
),
child: Text(
formatMessageDate(msgDate),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
),
SwipeableMessageBubble(
message: msg,
msgDate: msgDate,
formatTime: formatTime,
buildTick: buildTick,
onSwipe: () {
ref
.read(
taggedMessageProvider.notifier,
)
.state =
msg;
},
onParentTagTap: (tagId) {
if (tagId != null) {
// ⭐ FIXED: Find the message key string for the tagId
final tagIndex = messages.indexWhere(
(m) => m.id == tagId,
);
if (tagIndex != -1) {
final taggedMsg = messages[tagIndex];
final tagKeyString =
_getMessageKeyString(
taggedMsg,
tagIndex,
);
scrollToMessage(tagKeyString);
}
}
},
// ⭐ FIXED: Use messageKeyString as ValueKey
key: ValueKey(messageKeyString),
),
],
);
},
),
// ⭐⭐⭐ Subtle loading indicator at top
if (_isPaginationLoading)
Positioned(
top: 5,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text(
"Loading...",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
],
);
},
),
),
if (taggedMessage != null)
GestureDetector(
onTap: () {
if (taggedMessage != null) {
// ⭐ FIXED: Need to find the message in the list to get proper key
final chatAsync = ref.read(
chatMessagesProvider(widget.chatid),
);
chatAsync.whenData((messages) {
final tagIndex = messages.indexWhere(
(m) => m.id == taggedMessage.id,
);
if (tagIndex != -1) {
final tagKeyString = _getMessageKeyString(
taggedMessage,
tagIndex,
);
scrollToMessage(tagKeyString);
}
});
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.grey[200],
border: Border(top: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
Container(
width: 4,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF630A73),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
taggedMessage.user?.name ?? "Unknown",
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
color: Color(0xFF630A73),
),
),
const SizedBox(height: 2),
if (taggedMessage.type == "file" &&
taggedMessage.fileName != null)
Row(
children: [
Icon(
Icons.attach_file,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
taggedMessage.fileName!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
),
),
),
],
)
else
Text(
taggedMessage.message,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
padding: EdgeInsets.zero,
onPressed: () {
ref.read(taggedMessageProvider.notifier).state =
null;
},
),
],
),
),
),
],
),
if (showScrollButton)
Positioned(
bottom: 150,
right: 20,
child: FloatingActionButton(
heroTag: "scrollBtn",
backgroundColor: Colors.green,
mini: true,
onPressed: () {
scrollToBottom(animated: true);
},
child: const Icon(Icons.arrow_downward, color: Colors.white),
),
),
],
),
);
}
}
File diff suppressed because it is too large. Load diff
+243
View File
@@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/controller/api_contoller.dart';
class FlitterPopup {
static void show(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return const _FilterBottomSheet();
},
);
}
}
class _FilterBottomSheet extends StatefulWidget {
const _FilterBottomSheet({Key? key}) : super(key: key);
@override
State<_FilterBottomSheet> createState() => _FilterBottomSheetState();
}
class _FilterBottomSheetState extends State<_FilterBottomSheet>
with SingleTickerProviderStateMixin {
DateTime? fromDate;
DateTime? toDate;
late AnimationController _controller;
late Animation<Offset> _slideAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> pickFromDate() async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() => fromDate = picked);
}
}
Future<void> pickToDate() async {
final picked = await showDatePicker(
context: context,
initialDate: fromDate ?? DateTime.now(),
firstDate: fromDate ?? DateTime(2020),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() => toDate = picked);
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: SlideTransition(
position: _slideAnimation,
child: Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Filter By Date",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
/// FROM DATE
InkWell(
onTap: pickFromDate,
child: _dateBox(title: "From Date", value: fromDate),
),
const SizedBox(height: 12),
/// TO DATE
InkWell(
onTap: pickToDate,
child: _dateBox(title: "To Date", value: toDate),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: SizedBox(
width: double.infinity,
height: 57,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(
0xFFF7E9FF,
), // ✅ background
elevation: 6, // ✅ shadow depth
shadowColor: const Color(
0x40000000,
), // ✅ shadow color
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6), // ✅ radius
side: const BorderSide(
color: Color(0xFFD39FEA), // ✅ border color
width: 1, // ✅ border width
),
),
),
child: const Text(
"Cancel",
style: TextStyle(
fontFamily: "Gilroy-SemiBold", // ✅ Font
fontWeight: FontWeight.w400,
fontSize: 16,
height: 1.3, // ✅ line-height 130%
letterSpacing: 0.64, // ✅ 4%
color: Color(0xFF61277A), // ✅ text color
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Consumer(
builder: (context, ref, _) {
return CommanButton(
text: 'Submit',
onPressed: () {
final from = fromDate == null
? null
: "${fromDate!.year}-${fromDate!.month.toString().padLeft(2, '0')}-${fromDate!.day.toString().padLeft(2, '0')}";
final to = toDate == null
? null
: "${toDate!.year}-${toDate!.month.toString().padLeft(2, '0')}-${toDate!.day.toString().padLeft(2, '0')}";
// ✅ Apply filter to ALL TABS
ref
.read(
serviceHistoryNotifierProvider(
"pending",
).notifier,
)
.applyDateFilter(from: from, to: to);
ref
.read(
serviceHistoryNotifierProvider(
"inprogress",
).notifier,
)
.applyDateFilter(from: from, to: to);
ref
.read(
serviceHistoryNotifierProvider(
"completed",
).notifier,
)
.applyDateFilter(from: from, to: to);
ref
.read(
serviceHistoryNotifierProvider(
"cancelled",
).notifier,
)
.applyDateFilter(from: from, to: to);
Navigator.pop(context); // ✅ Close popup
},
);
},
),
),
],
),
const SizedBox(height: 10),
],
),
),
),
);
}
Widget _dateBox({required String title, DateTime? value}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
value == null ? title : "${value.day}-${value.month}-${value.year}",
style: const TextStyle(fontSize: 14),
),
const Icon(Icons.calendar_month, size: 20),
],
),
);
}
}
@@ -0,0 +1,551 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart';
import 'package:taxglide/view/screens/history/detail_screen.dart';
class PendingScreen extends ConsumerWidget {
final String status;
const PendingScreen({super.key, required this.status});
@override
Widget build(BuildContext context, WidgetRef ref) {
final pendingAsync = ref.watch(serviceHistoryNotifierProvider(status));
final size = MediaQuery.of(context).size;
final width = size.width;
final height = size.height;
return pendingAsync.when(
data: (data) {
final list = data.data ?? [];
if (list.isEmpty) {
// Using Get's capitalize extension explicitly
return Center(child: Text("No ${status.capitalizeFirst} Requests"));
}
return ListView.separated(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(
width * 0.04,
height * 0.01,
width * 0.04,
height * 0.2,
),
itemCount: list.length,
separatorBuilder: (_, __) => SizedBox(height: height * 0.01),
itemBuilder: (context, index) {
final item = list[index];
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE1E1E1), width: 1),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(117, 192, 192, 189),
blurRadius: 15,
offset: const Offset(0, 1),
),
],
),
padding: EdgeInsets.all(width * 0.03),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 File ID Row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'File ID : ',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: width * 0.04,
color: const Color(0xFF111827),
),
),
TextSpan(
text: '${item.id}',
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: width * 0.04,
color: const Color(0xFF111827),
),
),
],
),
),
// 🔹 Status Badges
if (item.status == 'Waiting for Admin' ||
item.status == 'Payment Pending' ||
item.status == 'Cancelled By Admin')
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(
item.status.toString(),
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w400,
fontSize: 11.03,
color: Color(0xFFFF0F0F),
),
),
),
),
// 🔹 In Progress Badge
if (item.status == 'In Progress')
Container(
width: 98,
height: 34.9,
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: const Center(
child: Text(
'In Progress',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w400,
fontSize: 12.43,
letterSpacing: 0.03,
color: Color(0xFF12800C),
),
),
),
),
// 🔹 Completed Badge
if (item.status == 'Completed')
Container(
width: 87,
height: 31,
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: const Center(
child: Text(
'Completed',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w400,
fontSize: 11.03,
letterSpacing: 0.03,
color: Color(0xFFFF630F),
),
),
),
),
],
),
SizedBox(height: height * 0.015),
// 🔹 Request Type
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Request Type : ',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
TextSpan(
text: item.service,
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
],
),
),
SizedBox(height: height * 0.015),
// 🔹 Date and Time Row
Row(
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Date : ',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
TextSpan(
text: item.createdDate,
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
],
),
),
SizedBox(width: width * 0.05),
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Time : ',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
TextSpan(
text: item.createdTime,
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
],
),
),
],
),
SizedBox(height: height * 0.015),
// 🔹 Message
RichText(
text: TextSpan(
children: [
TextSpan(
text: 'Message : ',
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
TextSpan(
text: item.message,
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontWeight: FontWeight.w500,
fontSize: width * 0.035,
color: const Color(0xFF111827),
),
),
],
),
),
SizedBox(height: height * 0.015),
const Divider(),
SizedBox(height: height * 0.015),
// 🔹 Conditional Buttons & Amount
if (item.paymentStatus == "Unpaid") ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Total Amount",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: width * 0.038,
color: const Color(0xFF111827),
),
),
Text(
'₹ ${item.paymentAmount.toString()}',
style: TextStyle(
fontFamily: 'Roboto',
fontWeight: FontWeight.w600,
fontSize: width * 0.05,
color: const Color(0xFF111827),
),
),
],
),
SizedBox(height: height * 0.015),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () {
// Add payment functionality here
},
child: Container(
height: height * 0.055,
decoration: BoxDecoration(
color: const Color(0xFFF7E9FF),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: const Color(0xFFD39FEA),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.8,
offset: const Offset(0, 4),
),
],
),
child: const Center(
child: Text(
"Pay Now",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
color: Color(0xFF61277A),
),
),
),
),
),
),
SizedBox(width: width * 0.03),
Expanded(
child: GestureDetector(
onTap: () {
ref.invalidate(serviceDetailProvider);
Get.offAll(
() => MainController(
initialIndex: 2, // ✅ History tab
child: DetailScreen(
id: int.parse(item.id.toString()),
sourceTabIndex: 2, // ✅ Remember source
),
),
);
},
child: Container(
height: height * 0.055,
decoration: BoxDecoration(
color: const Color(0xFF5F297B),
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.8,
offset: const Offset(0, 4),
),
],
),
child: const Center(
child: Text(
"View Details",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white,
),
),
),
),
),
),
],
),
] else if (item.paymentStatus == "Paid") ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 149,
height: 33,
decoration: BoxDecoration(
color: const Color(0xFF04690B),
borderRadius: BorderRadius.circular(4.53),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 5.89,
offset: const Offset(0, 3.02),
),
],
),
alignment: Alignment.center,
child: const Text(
"Payment Status: Paid",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w400,
fontSize: 12.08,
letterSpacing: 0.04,
height: 1.3,
color: Colors.white,
),
),
),
GestureDetector(
onTap: () {
ref.invalidate(serviceDetailProvider);
Get.offAll(
() => MainController(
initialIndex: 2, // ✅ History tab
child: DetailScreen(
id: int.parse(item.id.toString()),
sourceTabIndex: 2, // ✅ Remember source
),
),
);
},
child: Container(
height: height * 0.055,
width: width * 0.35,
decoration: BoxDecoration(
color: const Color(0xFF5F297B),
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.8,
offset: const Offset(0, 4),
),
],
),
child: const Center(
child: Text(
"View Details",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white,
),
),
),
),
),
],
),
] else if (item.paymentStatus == "Waiting") ...[
Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onTap: () {
ref.invalidate(serviceDetailProvider);
Get.offAll(
() => MainController(
initialIndex: 2, // ✅ History tab
child: DetailScreen(
id: int.parse(item.id.toString()),
sourceTabIndex: 2, // ✅ Remember source
),
),
);
},
child: Container(
height: height * 0.055,
width: width * 0.35,
decoration: BoxDecoration(
color: const Color(0xFF5F297B),
borderRadius: BorderRadius.circular(6),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 7.8,
offset: const Offset(0, 4),
),
],
),
child: const Center(
child: Text(
"View Details",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 16,
color: Colors.white,
),
),
),
),
),
),
],
SizedBox(height: height * 0.02),
],
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(
child: Text("Error: $error", style: const TextStyle(color: Colors.red)),
),
);
}
}
@@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/view/screens/history/flitter_popup.dart';
import 'package:taxglide/view/screens/history/pending_screen.dart';
class ServicesStatusScreen extends ConsumerStatefulWidget {
const ServicesStatusScreen({super.key});
@override
ConsumerState<ServicesStatusScreen> createState() =>
_ServicesStatusScreenState();
}
class _ServicesStatusScreenState extends ConsumerState<ServicesStatusScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<String> _tabs = [
"Pending",
"In Progress",
"Completed",
"Cancelled Request",
];
@override
void initState() {
super.initState();
_tabController = TabController(
length: _tabs.length,
vsync: this,
animationDuration: Duration.zero,
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ✅ Title
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 21),
child: SizedBox(
height: 50,
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(width: 40), // ✅ Left side space balance
// ✅ CENTER TITLE
const Text(
"Service Status",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 24,
color: Color(0xFF111827),
),
),
// ✅ RIGHT SIDE FILTER ICON
GestureDetector(
onTap: () {
FlitterPopup.show(context);
},
child: Container(
width: 39,
height: 39,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF055976),
width: 0.8,
),
),
child: const Center(
child: Icon(
Icons.filter_list,
color: Color(0xFF055976),
size: 22,
),
),
),
),
],
),
),
),
// ✅ Tabs
Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFDEDEDE), width: 1),
),
),
child: Align(
alignment: Alignment.centerLeft,
child: TabBar(
controller: _tabController,
isScrollable: true,
padding: EdgeInsets.zero,
labelPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
tabAlignment: TabAlignment.start,
splashFactory: NoSplash.splashFactory,
overlayColor: const MaterialStatePropertyAll(
Colors.transparent,
),
indicatorColor: const Color(0xFF5F297B),
indicator: const UnderlineTabIndicator(
borderSide: BorderSide(width: 3, color: Color(0xFF5F297B)),
insets: EdgeInsets.fromLTRB(6, 0, 4, 10),
),
labelColor: const Color(0xFF5F297B),
unselectedLabelColor: const Color(0xFF6C7278),
labelStyle: const TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w600,
fontSize: 16,
height: 1.4,
letterSpacing: 0.03,
color: Color(0xFF5F297B),
),
unselectedLabelStyle: const TextStyle(
fontFamily: "Gilroy-Regular",
fontWeight: FontWeight.w400,
fontSize: 16,
height: 1.4,
letterSpacing: 0.03,
color: Color(0xFF6C7278),
),
tabs: _tabs.map((tab) => Tab(text: tab)).toList(),
),
),
),
// ✅ Tab Views
Expanded(
child: TabBarView(
controller: _tabController,
physics: const NeverScrollableScrollPhysics(),
children: const [
ColoredBox(
color: Color.fromARGB(255, 230, 229, 229),
child: PendingScreen(status: "pending"),
),
ColoredBox(
color: Color.fromARGB(255, 230, 229, 229),
child: PendingScreen(status: "inprogress"),
),
ColoredBox(
color: Color.fromARGB(255, 230, 229, 229),
child: PendingScreen(status: "completed"),
),
ColoredBox(
color: Color.fromARGB(255, 230, 229, 229),
child: PendingScreen(status: "cancelled"),
),
],
),
),
],
),
),
);
}
}
File diff suppressed because it is too large. Load diff
+119
View File
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/comman_serivce.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/model/serivce_list_model.dart';
class ListServiceScreen extends ConsumerStatefulWidget {
const ListServiceScreen({super.key});
@override
ConsumerState<ListServiceScreen> createState() => _ListServiceScreenState();
}
class _ListServiceScreenState extends ConsumerState<ListServiceScreen> {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final serviceAsync = ref.watch(serviceListProvider);
final screenWidth = size.width;
return Scaffold(
body: SafeArea(
child: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.backgroundimages),
fit: BoxFit.cover,
),
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"List of Services Offered",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal,
fontSize: 18,
height: 1.3,
letterSpacing: 0.72,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 26),
/// 🔹 Service List from API
serviceAsync.when(
data: (services) {
if (services.isEmpty) {
return const Padding(
padding: EdgeInsets.only(top: 40),
child: Center(
child: Text(
"No Services Available",
style: TextStyle(
fontSize: 16,
fontFamily: 'Gilroy-SemiBold',
color: Colors.black54,
),
),
),
);
}
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: services.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: screenWidth < 400
? 3
: screenWidth < 700
? 3
: 4,
mainAxisSpacing: 16,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemBuilder: (context, index) {
return CommonServiceItem(
service: ServiceListModel(
id: services[index].id,
service: services[index].service,
icon: services[index].icon,
),
sourceTabIndex: 1,
);
},
);
},
loading: () => const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(),
),
),
error: (err, _) => Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text('Error loading services: $err'),
),
),
),
],
),
),
),
),
),
);
}
}
+151
View File
@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/model/notification_model.dart';
import 'package:taxglide/view/Mahi_chat/live_chat_screen.dart';
import 'package:taxglide/view/screens/history/detail_screen.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart';
class NotificationScreen extends ConsumerStatefulWidget {
const NotificationScreen({super.key});
@override
ConsumerState<NotificationScreen> createState() => _NotificationScreenState();
}
class _NotificationScreenState extends ConsumerState<NotificationScreen> {
@override
Widget build(BuildContext context) {
final state = ref.watch(notificationProvider(1)); // page = 1
return Scaffold(
body: SafeArea(
child: Column(
children: [
_buildHeader(),
Expanded(
child: state.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text("Error: $e")),
data: (list) => ListView.builder(
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
return _buildNotificationTile(item);
},
),
),
),
],
),
),
);
}
// HEADER UI
Widget _buildHeader() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 21),
child: SizedBox(
height: 50,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
const Text(
"Notification",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 24,
color: Color(0xFF111827),
),
),
Positioned(
left: 0,
child: GestureDetector(
onTap: () {
// 🔥 Force refresh when user goes BACK
ref.read(notificationTriggerProvider.notifier).state++;
ref.invalidate(chatMessagesProvider);
Get.offAll(() => const MainController());
},
child: const Icon(Icons.arrow_back_ios_rounded),
),
),
],
),
),
);
}
// NOTIFICATION TILE
Widget _buildNotificationTile(NotificationModel item) {
return GestureDetector(
onTap: () {
if (item.page.isEmpty || item.pageId == null) return;
final String page = item.page.toLowerCase();
final int id = item.pageId!;
if (page == 'chat') {
// Navigate to chat
Get.to(() => LiveChatScreen(chatid: id, fileid: '0'))?.then((_) {
ref.read(notificationTriggerProvider.notifier).state++;
ref.invalidate(chatMessagesProvider);
ref.invalidate(notificationProvider);
});
} else if (page == 'service') {
// Navigate to service detail
Get.to(
() => MainController(
initialIndex: 2,
child: DetailScreen(id: id, sourceTabIndex: 2),
),
);
} else if (page.contains('profile')) {
// Navigate to profile (Assuming MainController index 3 is Profile)
Get.to(() => const MainController(initialIndex: 3));
}
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w700,
fontSize: 16,
color: Colors.black,
),
),
const SizedBox(height: 4),
Text(
item.description,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 8),
Text(
item.createdAt,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
);
}
}
@@ -0,0 +1,260 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/controller/api_contoller.dart';
class EmployeeProfileList extends ConsumerStatefulWidget {
const EmployeeProfileList({super.key});
@override
ConsumerState<EmployeeProfileList> createState() =>
_EmployeeProfileListState();
}
class _EmployeeProfileListState extends ConsumerState<EmployeeProfileList> {
String yesNo(dynamic value) {
return value == 1 ? "Yes" : "No";
}
String statusText(dynamic value) {
return value == 1 ? "Active" : "Inactive";
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final profileAsync = ref.watch(employeeProfileProvider);
return Scaffold(
body: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.backgroundimages),
fit: BoxFit.cover,
),
),
child: SafeArea(
child: profileAsync.when(
data: (profile) {
final data = profile.data;
if (data == null) {
return const Center(
child: Text(
"No profile data found",
style: TextStyle(color: Colors.white),
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// ---------------- HEADER ----------------
Padding(
padding: const EdgeInsets.symmetric(vertical: 21),
child: SizedBox(
height: 50,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
const Text(
"KYC Details",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight
.w600, // equivalent to 400 (Normal)
fontStyle: FontStyle.normal,
fontSize: 24,
height: 1.3, // line-height: 130%
letterSpacing:
0.04 * 24, // 4% of font size = 0.96px
color: Color(0xFF111827), // background: #111827
),
),
Positioned(
left: 0,
child: GestureDetector(
onTap: () => Get.back(),
child: const Icon(Icons.arrow_back_ios_rounded),
),
),
],
),
),
),
const SizedBox(height: 20),
/// ---------------- CARD ----------------
Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Personal Details",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontSize: 20,
color: Color(0xFF111827),
),
),
const SizedBox(height: 25),
_buildDetailRow("Name : ", data.name),
const SizedBox(height: 24),
_buildDetailRow("Mobile : ", data.mobile),
const SizedBox(height: 24),
_buildDetailRow("Email : ", data.email),
const SizedBox(height: 24),
_buildDetailRow(
"Company Name : ",
data.company?.companyName ?? "N/A",
),
const SizedBox(height: 24),
_buildDetailRow(
"Service : ",
yesNo(data.createService),
),
const SizedBox(height: 24),
_buildDetailRow("Status : ", statusText(data.status)),
const SizedBox(height: 24),
_buildDetailRow(
"General Chat : ",
yesNo(data.generalChat),
),
],
),
),
const SizedBox(height: 30),
/// ---------------- COMPANY LOGO ----------------
if (data.company?.companyLogo != null)
GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildImageBox(
"Company Logo",
data.company!.companyLogo!,
),
],
),
const SizedBox(height: 40),
],
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
error: (e, _) => Center(
child: Text(
"Error loading profile: $e",
style: const TextStyle(color: Colors.redAccent),
),
),
),
),
),
);
}
/// ---------------- TEXT ROW ----------------
Widget _buildDetailRow(String title, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 14,
height: 1.3,
letterSpacing: 0.64,
color: Colors.black87,
),
),
Flexible(
child: Text(
value,
softWrap: true,
overflow: TextOverflow.visible,
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 14,
height: 1.3,
letterSpacing: 0.64,
color: Color(0xFF111827),
),
),
),
],
);
}
/// ---------------- IMAGE BOX ----------------
Widget _buildImageBox(String label, String imageUrl) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
children: [
const SizedBox(height: 10),
Text(
label,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13),
),
const SizedBox(height: 10),
Expanded(
child: Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image),
),
),
const SizedBox(height: 10),
],
),
);
}
}
@@ -0,0 +1,450 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/controller/api_repository.dart';
import 'package:taxglide/main.dart';
import 'package:taxglide/router/consts_routers.dart';
class EmployeeProfileScreen extends ConsumerStatefulWidget {
const EmployeeProfileScreen({super.key});
@override
ConsumerState<EmployeeProfileScreen> createState() =>
_EmployeeProfileScreenState();
}
class _EmployeeProfileScreenState extends ConsumerState<EmployeeProfileScreen> {
String? selectedItem;
bool _isLoggingOut = false;
@override
Widget build(BuildContext context) {
final profileAsync = ref.watch(employeeProfileProvider);
return Scaffold(
backgroundColor: Colors.white,
body: profileAsync.when(
data: (profile) {
final data = profile.data;
return Column(
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: double.infinity,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(50),
bottomRight: Radius.circular(50),
),
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(140),
bottomRight: Radius.circular(0),
),
child: Align(
alignment: Alignment.centerRight,
child: Image.asset(
AppAssets.profilebanner,
fit: BoxFit.cover,
height: 200,
width: MediaQuery.of(context).size.width * 1.3,
),
),
),
),
Positioned(
bottom: -50,
left: 0,
right: 0,
child: Center(
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
),
child:
(data?.company?.companyLogo != null &&
data!.company!.companyLogo!.isNotEmpty)
? CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(
data.company!.companyLogo!,
),
)
: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.black87,
),
child: Center(
child: Text(
(data?.name != null &&
data!.name.isNotEmpty)
? data.name[0].toUpperCase()
: "M",
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
),
const SizedBox(height: 60),
Text(
data?.name ?? 'No Company Name',
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 20,
color: Colors.black,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
Expanded(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
_buildMenuItem(
id: 'profile',
icon: Icons.person_outline,
title: 'Profile',
onTap: () {
setState(() {
selectedItem = 'profile';
});
ref.invalidate(employeeProfileProvider);
Get.toNamed(ConstRouters.employeekycdetailslist);
},
),
const SizedBox(height: 16),
_buildMenuItem(
id: 'terms',
icon: Icons.description_outlined,
title: 'Terms and Conditions',
onTap: () {
setState(() {
selectedItem = 'terms';
});
ref.invalidate(termsProvider);
Get.toNamed(ConstRouters.terms);
},
),
const SizedBox(height: 16),
_buildMenuItem(
id: 'policy',
icon: Icons.policy_outlined,
title: 'Policy',
onTap: () {
setState(() {
selectedItem = 'policy';
});
ref.invalidate(termsProvider);
Get.toNamed(ConstRouters.policy);
},
),
const SizedBox(height: 16),
_buildMenuItem(
id: 'logout',
icon: Icons.logout,
title: 'Log out',
onTap: () {
setState(() {
selectedItem = 'logout';
});
_showLogoutPopup(context);
},
),
],
),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Error: $err')),
),
);
}
/// ----------------------- LOGOUT POPUP ---------------------
void _showLogoutPopup(BuildContext context) {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: 'Logout',
barrierColor: Colors.black.withOpacity(0.2),
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return const SizedBox.shrink();
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
final offsetAnimation =
Tween<Offset>(begin: const Offset(0, -1), end: Offset.zero).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOutBack),
);
return SlideTransition(
position: offsetAnimation,
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 60.0),
child: Material(
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: 160,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7.75),
border: Border.all(color: Colors.white, width: 3.87),
boxShadow: const [
BoxShadow(
color: Color(0x7A5E5E5E),
offset: Offset(0, 2.32),
blurRadius: 9.14,
spreadRadius: 3.1,
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Note",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 20,
color: Colors.black,
),
),
const Text(
"Are you sure you want to log out?",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w500,
fontSize: 15,
color: Colors.black87,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
"Cancel",
style: TextStyle(
color: Color(0xFF535A5B),
fontSize: 16,
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
TextButton(
onPressed: _isLoggingOut
? null
: () async {
Navigator.pop(context);
setState(() {
_isLoggingOut = true;
});
try {
await ApiRepository().logout();
_showSnackBar("Logout successful");
await Future.delayed(
const Duration(milliseconds: 800),
);
Get.offAllNamed(
ConstRouters.employeelogin,
);
} catch (e) {
_showSnackBar(
e.toString(),
isError: true,
);
} finally {
setState(() {
_isLoggingOut = false;
});
}
},
child: _isLoggingOut
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF5F297B),
),
)
: const Text(
"Log Out",
style: TextStyle(
color: Color(0xFF5F297B),
fontSize: 16,
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
),
),
),
);
},
);
}
/// ----------------------- MENU ITEM ---------------------
Widget _buildMenuItem({
required String id,
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
final isSelected = selectedItem == id;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
child: Row(
children: [
SizedBox(
width: 24,
height: 48,
child: isSelected
? CustomPaint(
size: const Size(24, 48),
painter: BeveledTrianglePainter(),
)
: const SizedBox.shrink(),
),
Expanded(
child: Container(
height: 58,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFEEFAFF)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(icon, size: 24, color: Colors.black87),
const SizedBox(width: 16),
Expanded(
child: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
],
),
),
),
],
),
);
}
/// ----------------------- SNACK BAR ---------------------
void _showSnackBar(String msg, {bool isError = false}) {
rootScaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
backgroundColor: isError ? Colors.red : AppColors.commanbutton,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
content: Row(
children: [
Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
msg,
style: const TextStyle(color: Colors.white, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}
class BeveledTrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF5E2976)
..style = PaintingStyle.fill;
final path = Path()
..moveTo(0, 4)
..lineTo(size.width - 6, size.height / 2)
..lineTo(0, size.height - 4)
..close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,449 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/download_helper.dart';
import 'package:taxglide/controller/api_consts.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/router/consts_routers.dart';
class KycDetailsList extends ConsumerStatefulWidget {
const KycDetailsList({super.key});
@override
ConsumerState<KycDetailsList> createState() => _KycDetailsListState();
}
class _KycDetailsListState extends ConsumerState<KycDetailsList> {
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final profileAsync = ref.watch(profileProvider);
return Scaffold(
body: Container(
height: size.height,
width: size.width,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.backgroundimages),
fit: BoxFit.cover,
),
),
child: SafeArea(
child: profileAsync.when(
data: (profile) {
final data = profile.data;
if (data == null) {
return const Center(
child: Text(
"No KYC data found",
style: TextStyle(color: Colors.white),
),
);
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 21),
child: SizedBox(
height: 50,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
const Text(
"KYC Details",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight
.w600, // equivalent to 400 (Normal)
fontStyle: FontStyle.normal,
fontSize: 24,
height: 1.3, // line-height: 130%
letterSpacing:
0.04 * 24, // 4% of font size = 0.96px
color: Color(0xFF111827), // background: #111827
),
),
Positioned(
left: 0,
child: GestureDetector(
onTap: () => Get.back(),
child: const Icon(Icons.arrow_back_ios_rounded),
),
),
],
),
),
),
const SizedBox(height: 20),
// White Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Personal Details",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight
.w400, // equivalent to 400 (Normal)
fontStyle: FontStyle.normal,
fontSize: 20,
height: 1.3, // line-height: 130%
letterSpacing:
0.04 * 20, // 4% of font size = 0.8px
color: Color(
0xFF111827,
), // background: #111827
),
),
GestureDetector(
onTap: () {
Get.toNamed(ConstRouters.kycdetails);
},
child: Container(
width: 109,
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: const Color(
0xFFFFFFFF,
), // background: #FFFFFF
borderRadius: BorderRadius.circular(
8,
), // border-radius: 8px
border: Border.all(
color: const Color(
0xFFE3E3E3,
), // border: 1px solid #E3E3E3
width: 1,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF767373)
.withOpacity(
0.25,
), // #76737340 ≈ 25% opacity
blurRadius: 10,
offset: const Offset(0, 0),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment
.center, // center horizontally
crossAxisAlignment: CrossAxisAlignment
.center, // center vertically
children: const [
Icon(
Icons.edit_outlined,
size: 18,
color: Color(
0xFF111827,
), // matches your text color
),
SizedBox(width: 6),
Text(
'Edit',
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal,
fontSize: 16,
height: 1.3, // 130%
letterSpacing: 0.04 * 16, // 4%
color: Color(0xFF111827),
),
),
],
),
),
),
],
),
SizedBox(height: 25),
_buildDetailRow('Company Name : ', data.companyName),
const SizedBox(height: 24),
_buildDetailRow('PAN Number : ', data.panNumber),
const SizedBox(height: 24),
_buildDetailRow('GST Number : ', data.gstNumber),
const SizedBox(height: 24),
_buildDetailRow(
'Year of Incorporation : ',
data.yearOfIncorporation?.toString(),
),
const SizedBox(height: 24),
_buildDetailRow('Email : ', data.companyEmail),
const SizedBox(height: 24),
_buildDetailRow(
'Phone Number : ',
data.companyMobile,
),
const SizedBox(height: 24),
_buildDetailRow('Address : ', data.companyAddress),
const SizedBox(height: 24),
_buildDetailRow('Country : ', data.countryName),
const SizedBox(height: 24),
_buildDetailRow('State : ', data.stateName),
const SizedBox(height: 24),
_buildDetailRow('City : ', data.districtName),
const SizedBox(height: 24),
_buildDetailRow('PinCode : ', data.companyPincode),
],
),
),
const SizedBox(height: 30),
// 4 Image Containers
GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildImageBox("GST Certificate ", data.gstFile),
_buildImageBox("PAN File", data.panFile),
_buildImageBox(
"Incorporation File",
data.incorporationFile,
),
_buildImageBox("Profile Image", data.logo),
],
),
const SizedBox(height: 40),
],
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(color: Colors.white),
),
error: (e, st) => Center(
child: Text(
"Error loading KYC details: $e",
style: const TextStyle(color: Colors.redAccent),
),
),
),
),
),
);
}
Widget _buildDetailRow(String title, String? value) {
if (value == null || value.isEmpty) return const SizedBox.shrink();
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 14,
height: 1.3,
letterSpacing: 0.64,
color: Colors.black87,
),
),
Flexible(
child: Text(
value,
softWrap: true,
overflow: TextOverflow.visible,
style: const TextStyle(
fontFamily: 'Gilroy-Regular',
fontWeight: FontWeight.w400,
fontSize: 14,
height: 1.3,
letterSpacing: 0.64,
color: Color(0xFF111827),
),
),
),
],
);
}
Widget _buildImageBox(String label, String? imageUrl) {
final fullUrl = imageUrl != null && imageUrl.isNotEmpty
? imageUrl.startsWith('http')
? imageUrl
: "${ConstsApi.baseUrl}/api/$imageUrl"
: null;
final isDocument =
label.toLowerCase().contains('gst') ||
label.toLowerCase().contains('pan') ||
label.toLowerCase().contains('incorporation');
return Container(
width: 175,
height: 155,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
/// ✅ LABEL
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'Roboto',
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
const SizedBox(height: 8),
/// ✅ IMAGE / DOCUMENT PREVIEW
Expanded(
child: InkWell(
onTap: fullUrl == null
? null
: () async {
await DownloadHelper.downloadFileToReceived(fullUrl);
setState(() {});
},
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: fullUrl != null
? isDocument
? const Center(
child: Icon(
Icons.insert_drive_file_rounded,
size: 50,
color: Colors.blueGrey,
),
)
: Image.network(
fullUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const Center(
child: Icon(Icons.broken_image),
),
)
: const Center(
child: Icon(Icons.image_not_supported, size: 40),
),
),
),
),
),
const SizedBox(height: 8),
/// ✅ DOWNLOAD / VIEW BUTTON
if (fullUrl != null)
FutureBuilder<String?>(
future: DownloadHelper.checkFileExistsInReceived(fullUrl),
builder: (context, snapshot) {
final existsPath = snapshot.data;
return SizedBox(
height: 36,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: existsPath == null
? Colors.blue
: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
onPressed: () async {
if (existsPath == null) {
await DownloadHelper.downloadFileToReceived(fullUrl);
setState(() {});
} else {
await DownloadHelper.openDownloadedFile(existsPath);
}
},
icon: Icon(
existsPath == null
? Icons.download_rounded
: Icons.remove_red_eye_rounded,
size: 18,
color: Colors.white,
),
label: Text(
existsPath == null ? "Download" : "View",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
);
},
),
],
),
),
);
}
}
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/controller/api_contoller.dart';
class PolicyScreen extends ConsumerStatefulWidget {
const PolicyScreen({super.key});
@override
ConsumerState<PolicyScreen> createState() => _PolicyScreenState();
}
class _PolicyScreenState extends ConsumerState<PolicyScreen> {
@override
Widget build(BuildContext context) {
final policyAsync = ref.watch(policyProvider);
return Scaffold(
backgroundColor: const Color.fromARGB(255, 230, 229, 229),
body: SafeArea(
child: policyAsync.when(
data: (policy) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 Header Row with Back Arrow + Centered Title
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.arrow_back_ios_new,
color: Colors.black,
size: 22,
),
),
Expanded(
child: Center(
child: Text(
policy.data?.title ?? 'Privacy Policy',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
),
// Invisible icon to balance the layout
const Opacity(
opacity: 0,
child: Icon(Icons.arrow_back_ios_new, size: 22),
),
],
),
const SizedBox(height: 24),
// 🔹 Policy Content
Text(
policy.data?.content ?? '',
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 16,
height: 1.6,
color: Colors.black87,
),
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Text(
'Error: $err',
style: const TextStyle(color: Colors.red),
),
),
),
),
);
}
}
@@ -0,0 +1,539 @@
import 'dart:math' as math show sqrt;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/app_colors.dart';
import 'package:taxglide/consts/local_store.dart';
import 'package:taxglide/controller/api_repository.dart';
import 'package:taxglide/router/consts_routers.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/main.dart';
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> {
String? selectedItem;
bool _isLoggingOut = false;
String? userRole; // <-- role variable
@override
void initState() {
super.initState();
_loadRole(); // <-- load role on start
}
Future<void> _loadRole() async {
userRole = await LocalStore().getRole();
setState(() {});
}
@override
Widget build(BuildContext context) {
final profileAsync = ref.watch(profileProvider);
return Scaffold(
backgroundColor: Colors.white,
body: profileAsync.when(
data: (profile) {
final data = profile.data;
return SingleChildScrollView(
child: Column(
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: double.infinity,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(50),
bottomRight: Radius.circular(50),
),
),
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(140),
bottomRight: Radius.circular(0),
),
child: Align(
alignment: Alignment.centerRight,
child: Image.asset(
AppAssets.profilebanner,
fit: BoxFit.cover,
height: 200,
width: MediaQuery.of(context).size.width * 1.3,
),
),
),
),
Positioned(
bottom: -50,
left: 0,
right: 0,
child: Center(
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
),
child: data?.logo != null && data!.logo!.isNotEmpty
? CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(data.logo!),
)
: Container(
width: 100,
height: 100,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.black87,
),
child: Center(
child: Text(
(data?.companyName != null &&
data!.companyName!.isNotEmpty)
? data.companyName![0].toUpperCase()
: "M",
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
],
),
const SizedBox(height: 60),
Text(
data?.companyName ?? 'No Company Name',
style: const TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 20,
color: Colors.black,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
_buildMenuItem(
id: 'profile',
icon: Icons.person_outline,
title: 'Profile',
onTap: () {
setState(() {
selectedItem = 'profile';
});
final pan = profile.data?.panNumber ?? '';
ref.invalidate(profileProvider);
if (pan.isNotEmpty) {
Get.toNamed(ConstRouters.kycdetailslist);
} else {
Get.toNamed(ConstRouters.kycdetails);
}
},
),
const SizedBox(height: 16),
if (userRole != "employee")
_buildMenuItem(
id: 'Staff',
icon: Icons.list_alt_outlined,
title: 'Sub - Staff List',
onTap: () {
setState(() {
selectedItem = 'staff';
});
ref.invalidate(staffListProvider);
Get.toNamed(ConstRouters.staff);
},
),
const SizedBox(height: 16),
_buildMenuItem(
id: 'terms',
icon: Icons.description_outlined,
title: 'Terms and Conditions',
onTap: () {
setState(() {
selectedItem = 'terms';
});
ref.invalidate(termsProvider);
Get.toNamed(ConstRouters.terms);
},
),
const SizedBox(height: 16),
_buildMenuItem(
id: 'policy',
icon: Icons.policy_outlined,
title: 'Policy',
onTap: () {
setState(() {
selectedItem = 'policy';
});
ref.invalidate(termsProvider);
Get.toNamed(ConstRouters.policy);
},
),
const SizedBox(height: 16),
_buildMenuItem(
id: 'logout',
icon: Icons.logout,
title: 'Log out',
onTap: () {
setState(() {
selectedItem = 'logout';
});
_showLogoutPopup(context);
},
),
],
),
const SizedBox(height: 50),
],
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Error: $err')),
),
);
}
/// ----------------------- LOGOUT POPUP ---------------------
void _showLogoutPopup(BuildContext context) {
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: 'Logout',
barrierColor: Colors.black.withOpacity(0.2),
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return const SizedBox.shrink();
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
final offsetAnimation =
Tween<Offset>(begin: const Offset(0, -1), end: Offset.zero).animate(
CurvedAnimation(parent: animation, curve: Curves.easeOutBack),
);
return SlideTransition(
position: offsetAnimation,
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 60.0),
child: Material(
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
height: 160,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7.75),
border: Border.all(color: Colors.white, width: 3.87),
boxShadow: const [
BoxShadow(
color: Color(0x7A5E5E5E),
offset: Offset(0, 2.32),
blurRadius: 9.14,
spreadRadius: 3.1,
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Note",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: 20,
color: Colors.black,
),
),
const Text(
"Are you sure you want to log out?",
textAlign: TextAlign.center,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w500,
fontSize: 15,
color: Colors.black87,
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
"Cancel",
style: TextStyle(
color: Color(0xFF535A5B),
fontSize: 16,
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 8),
TextButton(
onPressed: _isLoggingOut
? null
: () async {
Navigator.pop(context);
setState(() {
_isLoggingOut = true;
});
try {
await ApiRepository().logout();
_showSnackBar("Logout successful");
await Future.delayed(
const Duration(milliseconds: 800),
);
Get.offAllNamed(ConstRouters.login);
} catch (e) {
_showSnackBar(
e.toString(),
isError: true,
);
} finally {
setState(() {
_isLoggingOut = false;
});
}
},
child: _isLoggingOut
? const SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF5F297B),
),
)
: const Text(
"Log Out",
style: TextStyle(
color: Color(0xFF5F297B),
fontSize: 16,
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
),
),
),
),
);
},
);
}
/// ----------------------- MENU ITEM ---------------------
Widget _buildMenuItem({
required String id,
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
final isSelected = selectedItem == id;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
child: Row(
children: [
SizedBox(
width: 24,
height: 48,
child: isSelected
? CustomPaint(
size: const Size(24, 48),
painter: BeveledTrianglePainter(),
)
: const SizedBox.shrink(),
),
Expanded(
child: Container(
height: 58,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFFEEFAFF)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(icon, size: 24, color: Colors.black87),
const SizedBox(width: 16),
Expanded(
child: Text(
title,
style: const TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
],
),
),
),
],
),
);
}
/// ----------------------- SNACK BAR ---------------------
void _showSnackBar(String msg, {bool isError = false}) {
rootScaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
backgroundColor: isError ? Colors.red : AppColors.commanbutton,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
content: Row(
children: [
Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
msg,
style: const TextStyle(color: Colors.white, fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
}
class BeveledTrianglePainter extends CustomPainter {
final double cornerRadius;
BeveledTrianglePainter({
this.cornerRadius = 4.0, // Adjust this value for more/less rounding
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF5E2976)
..style = PaintingStyle.fill;
// Triangle vertices
final vertices = [
Offset(0, cornerRadius), // Top left (adjusted for bevel)
Offset(size.width - 6, size.height / 2), // Right point
Offset(0, size.height - cornerRadius), // Bottom left (adjusted for bevel)
];
final path = Path();
for (int i = 0; i < vertices.length; i++) {
final current = vertices[i];
final next = vertices[(i + 1) % vertices.length];
final prev = vertices[(i - 1 + vertices.length) % vertices.length];
// Calculate direction vectors
final toCurrent = Offset(current.dx - prev.dx, current.dy - prev.dy);
final toNext = Offset(next.dx - current.dx, next.dy - current.dy);
// Normalize
final lengthToCurrent = math.sqrt(
toCurrent.dx * toCurrent.dx + toCurrent.dy * toCurrent.dy,
);
final lengthToNext = math.sqrt(
toNext.dx * toNext.dx + toNext.dy * toNext.dy,
);
final normalizedToCurrent = Offset(
toCurrent.dx / lengthToCurrent,
toCurrent.dy / lengthToCurrent,
);
final normalizedToNext = Offset(
toNext.dx / lengthToNext,
toNext.dy / lengthToNext,
);
// Points before and after the corner
final beforeCorner = Offset(
current.dx - normalizedToCurrent.dx * cornerRadius,
current.dy - normalizedToCurrent.dy * cornerRadius,
);
final afterCorner = Offset(
current.dx + normalizedToNext.dx * cornerRadius,
current.dy + normalizedToNext.dy * cornerRadius,
);
if (i == 0) {
path.moveTo(beforeCorner.dx, beforeCorner.dy);
} else {
path.lineTo(beforeCorner.dx, beforeCorner.dy);
}
// Draw rounded corner
path.quadraticBezierTo(
current.dx,
current.dy,
afterCorner.dx,
afterCorner.dy,
);
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/controller/api_contoller.dart';
class StaffListScreen extends ConsumerStatefulWidget {
const StaffListScreen({super.key});
@override
ConsumerState<StaffListScreen> createState() => _StaffListScreenState();
}
class _StaffListScreenState extends ConsumerState<StaffListScreen> {
@override
Widget build(BuildContext context) {
final staffAsync = ref.watch(staffListProvider);
const commonTextStyle = TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w600,
fontSize: 14.7,
height: 1.30, // 130%
letterSpacing: 0.04,
color: Color(0xFF111827),
);
const commonTextStylevalue = TextStyle(
fontFamily: "Gilroy-SemiBold",
fontWeight: FontWeight.w400,
fontSize: 14.7,
height: 1.30, // 130%
letterSpacing: 0.04,
color: Color(0xFF111827),
);
return Scaffold(
backgroundColor: const Color.fromARGB(255, 230, 229, 229),
body: SafeArea(
child: staffAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Text(
'Error: $err',
style: const TextStyle(color: Colors.red),
),
),
data: (staffList) {
if (staffList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"No Staff Found",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: CommanButton(
text: 'Go Back',
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.arrow_back_ios_new,
color: Colors.black,
size: 22,
),
),
const Expanded(
child: Center(
child: Text(
'Sub - Staff List',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
),
const Opacity(
opacity: 0,
child: Icon(Icons.arrow_back_ios_new, size: 22),
),
],
),
const SizedBox(height: 50),
...staffList.map((staff) {
String statusText = staff.status == 1
? "Active"
: "Inactive";
String generalChatText = staff.generalChat == 1
? "Yes"
: "No";
String createServiceText = staff.createService == 1
? "Yes"
: "No";
Color statusColor = staff.status == 1
? Colors.green
: Colors.red;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// ✅ Name
Row(
children: [
Text("Name : ", style: commonTextStyle),
Expanded(
child: Text(
staff.name,
style: commonTextStylevalue,
),
),
],
),
const SizedBox(height: 6),
/// ✅ Mobile
Row(
children: [
Text("Mobile : ", style: commonTextStyle),
Expanded(
child: Text(
staff.mobile,
style: commonTextStylevalue,
),
),
],
),
const SizedBox(height: 6),
/// ✅ Email
Row(
children: [
Text("Email : ", style: commonTextStyle),
Expanded(
child: Text(
staff.email,
style: commonTextStylevalue,
),
),
],
),
const SizedBox(height: 10),
/// ✅ Status
Row(
children: [
Text("Status : ", style: commonTextStyle),
Text(
statusText,
style: commonTextStylevalue.copyWith(
color: statusColor,
),
),
],
),
const SizedBox(height: 6),
/// ✅ General Chat
Row(
children: [
Text("General Chat : ", style: commonTextStyle),
Text(
generalChatText,
style: commonTextStylevalue,
),
],
),
const SizedBox(height: 6),
/// ✅ Create Service
Row(
children: [
Text(
"Create Service : ",
style: commonTextStyle,
),
Text(
createServiceText,
style: commonTextStylevalue,
),
],
),
],
),
);
}).toList(),
],
),
);
},
),
),
);
}
}
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:taxglide/controller/api_contoller.dart';
class TermsAndConditionScreen extends ConsumerStatefulWidget {
const TermsAndConditionScreen({super.key});
@override
ConsumerState<TermsAndConditionScreen> createState() =>
_TermsAndConditionScreenState();
}
class _TermsAndConditionScreenState
extends ConsumerState<TermsAndConditionScreen> {
@override
Widget build(BuildContext context) {
final termsAsync = ref.watch(termsProvider);
return Scaffold(
backgroundColor: const Color.fromARGB(255, 230, 229, 229),
body: SafeArea(
child: termsAsync.when(
data: (terms) => SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 🔹 Header Row with Back Arrow + Centered Title
Row(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: const Icon(
Icons.arrow_back_ios_new,
color: Colors.black,
size: 22,
),
),
Expanded(
child: Center(
child: Text(
terms.data?.title ?? 'Terms & Conditions',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
),
),
// To balance layout so title stays centered
const Opacity(
opacity: 0,
child: Icon(Icons.arrow_back_ios_new, size: 22),
),
],
),
const SizedBox(height: 24),
// 🔹 Terms Content
Text(
terms.data?.content ?? '',
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 16,
height: 1.6,
color: Colors.black87,
),
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Text(
'Error: $err',
style: const TextStyle(color: Colors.red),
),
),
),
),
);
}
}
@@ -0,0 +1,910 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as path;
import 'package:taxglide/consts/comman_button.dart';
import 'package:taxglide/consts/responsive_helper.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/controller/api_repository.dart';
import 'package:taxglide/view/Main_controller/main_controller.dart';
class ServiceRequestScreen extends ConsumerStatefulWidget {
final int id;
final String service;
final String icon;
final int? returnTabIndex;
const ServiceRequestScreen({
super.key,
required this.id,
required this.service,
required this.icon,
this.returnTabIndex,
});
@override
ConsumerState<ServiceRequestScreen> createState() =>
_ServiceRequestScreenState();
}
class _ServiceRequestScreenState extends ConsumerState<ServiceRequestScreen> {
final List<File> _selectedFiles = [];
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _isFileError = false;
bool _isTermsAccepted = false;
bool _isCheckboxError = false;
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
void _showSnackBar(String msg, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 2),
backgroundColor: isError ? Colors.red : Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 6,
margin: const EdgeInsets.symmetric(horizontal: 50, vertical: 25),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
content: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isError ? Icons.error_outline : Icons.check_circle_outline,
color: Colors.white,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
msg,
style: const TextStyle(color: Colors.white, fontSize: 12),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
),
);
}
bool _validateForm() {
setState(() {
_isFileError = false;
_isCheckboxError = false;
});
bool isValid = true;
if (_selectedFiles.isEmpty) {
setState(() => _isFileError = true);
_showSnackBar("Please upload at least one file", isError: true);
isValid = false;
}
if (!_isTermsAccepted) {
setState(() => _isCheckboxError = true);
_showSnackBar("Please accept terms and conditions", isError: true);
isValid = false;
}
return isValid;
}
Future<void> _submitRequest() async {
if (_validateForm()) {
try {
_showSnackBar("Submitting request...");
await ApiRepository().updateServiceRequest(
serviceId: widget.id,
message: _messageController.text.trim(),
documents: _selectedFiles,
);
_showSnackBar("Request submitted successfully!");
ref.invalidate(serviceHistoryNotifierProvider);
Get.offAll(() => const MainController());
} catch (e) {
debugPrint("❌ Submit error: $e");
_showSnackBar("Something went wrong. Please try again.", isError: true);
}
}
}
Future<void> _pickFile() async {
final choice = await showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('Select File Type'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt, color: Colors.blue),
title: const Text('Camera'),
onTap: () => Navigator.pop(ctx, 'camera'),
),
ListTile(
leading: const Icon(Icons.photo, color: Colors.blue),
title: const Text('Gallery'),
onTap: () => Navigator.pop(ctx, 'gallery'),
),
ListTile(
leading: const Icon(Icons.picture_as_pdf, color: Colors.red),
title: const Text('Select PDF'),
onTap: () => Navigator.pop(ctx, 'pdf'),
),
ListTile(
leading: const Icon(Icons.folder_zip, color: Colors.orange),
title: const Text('Select ZIP'),
onTap: () => Navigator.pop(ctx, 'zip'),
),
],
),
),
);
if (choice == null) return;
try {
if (choice == 'camera') {
final hasPermission = await _requestPermission(ImageSource.camera);
if (!hasPermission) return;
final picked = await ImagePicker().pickImage(
source: ImageSource.camera,
);
if (picked != null) {
setState(() {
_selectedFiles.add(File(picked.path));
_isFileError = false;
});
}
} else if (choice == 'gallery') {
final hasPermission = await _requestPermission(ImageSource.gallery);
if (!hasPermission) return;
final picked = await ImagePicker().pickMultiImage();
if (picked.isNotEmpty) {
setState(() {
for (var image in picked) {
_selectedFiles.add(File(image.path));
}
_isFileError = false;
});
}
} else if (choice == 'pdf' || choice == 'zip') {
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: choice == 'pdf' ? ['pdf'] : ['zip', 'rar', '7z'],
);
if (result != null && result.files.isNotEmpty) {
for (var file in result.files) {
if (file.path != null) {
setState(() {
_selectedFiles.add(File(file.path!));
_isFileError = false;
});
}
}
}
}
} catch (e) {
debugPrint("❌ File picking error: $e");
_showSnackBar("Failed to pick file: $e", isError: true);
}
}
Future<void> _showSettingsDialog(String permission) async {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('$permission Permission Required'),
content: Text(
'Allow $permission access to capture and upload your documents easily. You can enable it in the app settings.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(ctx);
openAppSettings();
},
child: const Text('Go to Settings'),
),
],
),
);
}
Future<bool> _requestPermission(ImageSource source) async {
try {
Permission permission;
String name;
if (source == ImageSource.camera) {
permission = Permission.camera;
name = 'Camera';
} else {
if (Platform.isAndroid) {
final info = await DeviceInfoPlugin().androidInfo;
if (info.version.sdkInt >= 33) {
permission = Permission.photos;
name = 'Photos';
} else {
permission = Permission.storage;
name = 'Storage';
}
} else {
permission = Permission.photos;
name = 'Photos';
}
}
var status = await permission.status;
print("status $status");
status = await permission.request();
if (status.isGranted || status.isLimited) return true;
if (status.isPermanentlyDenied || (Platform.isIOS && status.isDenied)) {
await _showSettingsDialog(name);
return false;
}
await _showSettingsDialog(name);
return false;
} catch (e) {
debugPrint("Permission error: $e");
return Platform.isIOS;
}
}
bool _isImage(String pathStr) {
final ext = path.extension(pathStr).toLowerCase();
return ['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext);
}
bool _isPdf(String pathStr) =>
path.extension(pathStr).toLowerCase() == '.pdf';
bool _isZip(String pathStr) =>
['.zip', '.rar', '.7z'].contains(path.extension(pathStr).toLowerCase());
@override
Widget build(BuildContext context) {
final r = ResponsiveUtils(context);
// Responsive values
final hPadding = r.spacing(mobile: 10, tablet: 16, desktop: 24);
final hPaddingInner = r.spacing(mobile: 20, tablet: 24, desktop: 32);
final headerFontSize = r.fontSize(mobile: 20, tablet: 24, desktop: 28);
final labelFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18);
final readOnlyFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
final uploadFontSize = r.fontSize(mobile: 11.5, tablet: 12.78, desktop: 14);
final uploadSubFontSize = r.fontSize(mobile: 9, tablet: 10, desktop: 11);
final checkboxFontSize = r.fontSize(
mobile: 11.5,
tablet: 12.78,
desktop: 14,
);
final messageFontSize = r.fontSize(
mobile: 10.5,
tablet: 11.31,
desktop: 12.5,
);
final serviceNameFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15);
final uploadedCountFontSize = r.fontSize(
mobile: 13,
tablet: 14,
desktop: 15,
);
final serviceIconContainerW = r.getValue<double>(
mobile: 90,
tablet: 110,
desktop: 130,
);
final serviceIconContainerH = r.getValue<double>(
mobile: 70,
tablet: 84,
desktop: 100,
);
final serviceIconSize = r.getValue<double>(
mobile: 36,
tablet: 42,
desktop: 50,
);
final messageBoxHeight = r.getValue<double>(
mobile: 100,
tablet: 117,
desktop: 140,
);
final fileCardWidth = r.getValue<double>(
mobile: 90,
tablet: 110,
desktop: 130,
);
final fileCardHeight = r.getValue<double>(
mobile: 100,
tablet: 120,
desktop: 140,
);
final fileIconSize = r.getValue<double>(
mobile: 34,
tablet: 40,
desktop: 46,
);
final spacingSM = r.spacing(mobile: 10, tablet: 10, desktop: 12);
final spacingMD = r.spacing(mobile: 16, tablet: 20, desktop: 24);
final spacingLG = r.spacing(mobile: 20, tablet: 20, desktop: 24);
return Scaffold(
backgroundColor: const Color.fromARGB(255, 230, 229, 229),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Padding(
padding: EdgeInsets.symmetric(
horizontal: hPadding,
vertical: 21,
),
child: SizedBox(
height: 50,
width: double.infinity,
child: Stack(
alignment: Alignment.center,
children: [
Text(
"New Service Request",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: headerFontSize,
color: const Color(0xFF111827),
),
),
Positioned(
left: 0,
child: GestureDetector(
onTap: () => Get.offAll(() => const MainController()),
child: const Icon(Icons.arrow_back_ios_rounded),
),
),
],
),
),
),
SizedBox(height: spacingLG),
// Service icon and name
Center(
child: Column(
children: [
Container(
width: serviceIconContainerW,
height: serviceIconContainerH,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE1E1E1)),
boxShadow: const [
BoxShadow(
color: Color(0x66757575),
offset: Offset(0, 1),
blurRadius: 7.3,
),
],
),
padding: const EdgeInsets.all(8),
child: Image.network(
widget.icon,
width: serviceIconSize,
height: serviceIconSize,
),
),
SizedBox(height: spacingMD),
Text(
widget.service,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: serviceNameFontSize,
color: const Color(0xFF3F3F3F),
),
),
],
),
),
SizedBox(height: spacingLG),
// Request Type
_buildLabel("Request Type*", hPaddingInner, labelFontSize),
_buildReadOnlyBox(
widget.service,
hPaddingInner,
readOnlyFontSize,
),
SizedBox(height: spacingSM),
// File Upload Header
Padding(
padding: EdgeInsets.symmetric(horizontal: hPaddingInner),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"File Upload *",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: labelFontSize,
color: const Color(0xFF111827),
),
),
GestureDetector(
onTap: _pickFile,
child: Container(
width: 20,
height: 20,
decoration: const BoxDecoration(
color: Colors.black,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
size: 18,
color: Colors.white,
),
),
),
],
),
),
SizedBox(height: spacingLG),
// Upload Box
_buildUploadBox(hPaddingInner, uploadFontSize, uploadSubFontSize),
// Selected Files
if (_selectedFiles.isNotEmpty)
_buildEnhancedSelectedFiles(
hPaddingInner,
fileCardWidth,
fileCardHeight,
fileIconSize,
uploadedCountFontSize,
),
SizedBox(height: spacingLG),
// Message
_buildLabel("Message", hPaddingInner, labelFontSize),
_buildMessageBox(
hPaddingInner,
messageBoxHeight,
messageFontSize,
),
// Terms Checkbox
Padding(
padding: EdgeInsets.symmetric(horizontal: hPadding),
child: Row(
children: [
Checkbox(
value: _isTermsAccepted,
onChanged: (v) {
setState(() {
_isTermsAccepted = v ?? false;
if (v == true) _isCheckboxError = false;
});
},
activeColor: Colors.green,
side: BorderSide(
color: _isCheckboxError ? Colors.red : Colors.grey,
width: _isCheckboxError ? 2 : 1,
),
),
Expanded(
child: Text(
"I accept the terms and conditions *",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal,
fontSize: checkboxFontSize,
height: 25.56 / 12.78,
letterSpacing: 0.8,
color: _isCheckboxError
? Colors.red
: const Color(0xFF4F4C4C),
),
),
),
],
),
),
// Submit Button
Padding(
padding: EdgeInsets.symmetric(
horizontal: r.spacing(mobile: 30, tablet: 41, desktop: 60),
vertical: spacingLG,
),
child: CommanButton(
text: 'Submit Request',
onPressed: _submitRequest,
),
),
const SizedBox(height: 250),
],
),
),
),
);
}
Widget _buildLabel(String title, double hPadding, double fontSize) => Padding(
padding: EdgeInsets.symmetric(horizontal: hPadding),
child: Text(
title,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: fontSize,
color: const Color(0xFF111827),
),
),
);
Widget _buildReadOnlyBox(String text, double hPadding, double fontSize) =>
Padding(
padding: EdgeInsets.symmetric(horizontal: hPadding, vertical: 15),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFFDFDFDF)),
boxShadow: const [
BoxShadow(
color: Color(0x66BDBDBD),
offset: Offset(0, 1),
blurRadius: 7,
),
],
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 21),
child: Text(
text,
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: fontSize,
color: const Color(0xFF3F3F3F),
),
),
),
);
Widget _buildUploadBox(
double hPadding,
double fontSize,
double subFontSize,
) => Padding(
padding: EdgeInsets.symmetric(horizontal: hPadding),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: _isFileError ? Colors.red : const Color(0xFFDFDFDF),
width: _isFileError ? 2 : 1,
),
),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 21),
child: Column(
children: [
Text(
"Upload PDF, IMG, JPG, ZIP",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: fontSize,
height: 25.56 / 12.78,
letterSpacing: 0.8,
color: _isFileError ? Colors.red : const Color(0xFF4F4C4C),
),
),
Text(
"(40 MB only allowed File)",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: subFontSize,
height: 25.56 / 12.78,
letterSpacing: 0.8,
color: _isFileError ? Colors.red : const Color(0xFF4F4C4C),
),
),
if (_isFileError)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
"Please upload at least one file",
style: TextStyle(color: Colors.red, fontSize: 11),
),
),
],
),
),
);
Widget _buildEnhancedSelectedFiles(
double hPadding,
double cardWidth,
double cardHeight,
double iconSize,
double countFontSize,
) => Container(
margin: EdgeInsets.symmetric(horizontal: hPadding, vertical: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE1E1E1)),
boxShadow: const [
BoxShadow(
color: Color(0x1A000000),
offset: Offset(0, 2),
blurRadius: 8,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.check_circle,
color: Colors.green.shade600,
size: 18,
),
),
const SizedBox(width: 8),
Text(
"Uploaded Files (${_selectedFiles.length})",
style: TextStyle(
fontFamily: 'Gilroy-SemiBold',
fontWeight: FontWeight.w600,
fontSize: countFontSize,
color: const Color(0xFF111827),
),
),
],
),
if (_selectedFiles.length > 2)
Row(
children: [
GestureDetector(
onTap: () {
_scrollController.animateTo(
_scrollController.offset - 120,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_back_ios_new,
size: 14,
color: Colors.black87,
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
_scrollController.animateTo(
_scrollController.offset + 120,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_forward_ios,
size: 14,
color: Colors.black87,
),
),
),
],
),
],
),
const SizedBox(height: 12),
SizedBox(
height: cardHeight,
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
itemCount: _selectedFiles.length,
itemBuilder: (context, index) {
final file = _selectedFiles[index];
final pathStr = file.path;
Widget preview;
Color bgColor;
if (_isImage(pathStr)) {
preview = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
file,
fit: BoxFit.cover,
width: cardWidth,
height: cardHeight,
),
);
bgColor = Colors.transparent;
} else if (_isPdf(pathStr)) {
bgColor = Colors.red.shade50;
preview = Icon(
Icons.picture_as_pdf,
size: iconSize,
color: Colors.red.shade600,
);
} else if (_isZip(pathStr)) {
bgColor = Colors.orange.shade50;
preview = Icon(
Icons.folder_zip,
size: iconSize,
color: Colors.orange.shade600,
);
} else {
bgColor = Colors.blue.shade50;
preview = Icon(
Icons.insert_drive_file,
size: iconSize,
color: Colors.blue.shade600,
);
}
return Padding(
padding: const EdgeInsets.only(right: 12),
child: Stack(
children: [
Container(
width: cardWidth,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.grey.shade300,
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
offset: const Offset(0, 2),
blurRadius: 4,
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Expanded(child: Center(child: preview))],
),
),
Positioned(
right: -2,
top: -2,
child: GestureDetector(
onTap: () {
setState(() {
_selectedFiles.removeAt(index);
if (_selectedFiles.isEmpty) _isFileError = true;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.red.shade600,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
offset: const Offset(0, 2),
blurRadius: 4,
),
],
),
padding: const EdgeInsets.all(4),
child: const Icon(
Icons.close,
size: 14,
color: Colors.white,
),
),
),
),
],
),
);
},
),
),
],
),
);
Widget _buildMessageBox(double hPadding, double boxHeight, double fontSize) =>
Padding(
padding: EdgeInsets.symmetric(horizontal: hPadding, vertical: 15),
child: Container(
width: double.infinity,
height: boxHeight,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFCFCFCF)),
),
child: TextFormField(
controller: _messageController,
maxLines: null,
expands: true,
style: TextStyle(
fontFamily: 'Gilroy-Medium',
fontSize: fontSize,
color: const Color(0xFF6C7278),
),
decoration: const InputDecoration(
hintText: "Enter your message",
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
),
);
}