taxgilde/lib/view/screens/serivce_request_screen.dart

909 lines
30 KiB
Dart

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/app_style.dart';
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: AppTextStyles.regular.copyWith(
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.insert_drive_file, color: Colors.blue),
title: const Text('Files'),
onTap: () => Navigator.pop(ctx, 'any'),
),
],
),
),
);
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 == 'any') {
final result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.any,
);
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;
// ⭐ Match Live Chat behavior: On iOS, don't force a manual request if already denied
// Let ImagePicker handle it, or only show settings dialog if permanently denied.
if (Platform.isIOS) {
if (status.isPermanentlyDenied) {
await _showSettingsDialog(name);
return false;
}
return true; // Return true to let ImagePicker try to open and trigger system prompt
}
// Android behavior
if (status.isGranted || status.isLimited) return true;
status = await permission.request();
if (status.isGranted || status.isLimited) return true;
if (status.isPermanentlyDenied) {
await _showSettingsDialog(name);
return false;
}
return false;
} catch (e) {
debugPrint("❌ Permission error: $e");
// Fallback to let the picker try on iOS, or return false on Android
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());
bool _isExcel(String pathStr) =>
['.xls', '.xlsx'].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: AppTextStyles.semiBold.copyWith(
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,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.error_outline,
color: Colors.red.shade300,
size: serviceIconSize * 0.8,
);
},
),
),
SizedBox(height: spacingMD),
Text(
widget.service,
style: AppTextStyles.semiBold.copyWith(
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: AppTextStyles.semiBold.copyWith(
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: AppTextStyles.semiBold.copyWith(
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,
),
),
SizedBox(height: r.spacing(mobile: 40, tablet: 60, desktop: 80)),
],
),
),
),
);
}
Widget _buildLabel(String title, double hPadding, double fontSize) => Padding(
padding: EdgeInsets.symmetric(horizontal: hPadding),
child: Text(
title,
style: AppTextStyles.semiBold.copyWith(
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: AppTextStyles.semiBold.copyWith(
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: const Color(0xFFDFDFDF), width: 1),
),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 21),
child: Column(
children: [
Text(
"Upload Documents or Images",
style: AppTextStyles.semiBold.copyWith(
fontSize: fontSize,
height: 25.56 / 12.78,
letterSpacing: 0.8,
color: const Color(0xFF4F4C4C),
),
),
Text(
"(40 MB only allowed File)",
style: AppTextStyles.semiBold.copyWith(
fontSize: subFontSize,
height: 25.56 / 12.78,
letterSpacing: 0.8,
color: const Color(0xFF4F4C4C),
),
),
],
),
),
);
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: AppTextStyles.semiBold.copyWith(
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 if (_isExcel(pathStr)) {
bgColor = Colors.green.shade50;
preview = Icon(
Icons.table_chart,
size: iconSize,
color: Colors.green.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: AppTextStyles.regular.copyWith(
fontSize: fontSize,
color: const Color(0xFF6C7278),
),
decoration: const InputDecoration(
hintText: "Enter your message",
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
),
),
);
}