initial commit: push everything

This commit is contained in:
2026-07-06 20:27:53 +05:30
commit df4e044d74
329 changed files with 36620 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
class FaqController extends GetxController {
WebViewController? webViewController;
var isLoading = true.obs;
@override
void onInit() {
super.onInit();
initializeWebView();
}
void initializeWebView() {
webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
isLoading.value = true;
print('Started loading: $url');
},
onPageFinished: (url) {
isLoading.value = false;
print('Finished loading: $url');
},
onWebResourceError: (error) {
isLoading.value = false;
print('WebView error: ${error.description}');
},
),
);
loadFaqUrl();
}
Future<void> loadFaqUrl() async {
if (webViewController != null) {
try {
await webViewController!.loadRequest(
Uri.parse('https://nearle.in/faq'),
);
} catch (e) {
print('Error loading URL: $e');
}
}
}
}
class FaqPage extends StatelessWidget {
const FaqPage({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(FaqController());
return Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70,
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
), // :small_blue_diamond: white back arrow
onPressed: () {
Navigator.pop(context); // goes back to previous screen
},
),
title: const Text(
'FAQ',
style: TextStyle(
fontSize: 26, // :small_blue_diamond: larger font size
color: Colors.white, // :small_blue_diamond: white text
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
elevation: 4,
),
body: Obx(() {
final wvc = controller.webViewController;
return Stack(
children: [
if (wvc != null) WebViewWidget(controller: wvc),
if (controller.isLoading.value)
const LinearProgressIndicator(minHeight: 2),
],
);
}),
);
}
}

View File

@@ -0,0 +1,380 @@
import 'package:flutter/material.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
class HelpCenter extends StatelessWidget {
const HelpCenter({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () {
Navigator.pop(context);
},
),
title: const Text(
'Help Center',
style: TextStyle(
fontSize: 26,
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
elevation: 4,
),
body: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header text
Text(
"We're here to help you with anything and \neverything on Nearle Xpress",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
fontFamily: FontConstants.fontFamily
),
),
const SizedBox(height: 8),
Text(
"We make sure your delivery experience is smooth and clear. Whether youre on your first trip or your hundredth, weve got your back. Browse through frequently asked questions or reach out directly if you need further help.",
style: TextStyle(fontSize: 18,color: Colors.grey.shade700, height: 1.4, fontFamily: FontConstants.fontFamily),
),
const SizedBox(height: 16),
TextField(
decoration: InputDecoration(
hintText: 'Search help',
prefixIcon: const Icon(Icons.search),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
),
),
),
const SizedBox(height: 15),
Text(
'FAQ',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily,color: ColorConstants.primaryColor),
),
Divider(),
_FaqTile(
title: 'What is Nearle Xpress?',
initiallyExpanded: true,
child: const Text(
'Nearle Xpress is a delivery app for riders who complete local deliveries for nearby stores and markets. It helps riders accept tasks, manage pickup and drop points, and update deliveries in real time.',
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
),
),
_FaqTile(
title: 'How do I accept a delivery task?',
child: const Text(
'You can accept tasks from the Home screen when a new order appears. Tap on the order to view details and then press Accept.',
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
),
),
_FaqTile(
title: 'How do I update the delivery status?',
child: const Text(
'Open the active task and use the status buttons to mark Pickup, On the way, and Delivered. Ensure accurate updates for better tracking.',
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
),
),
_FaqTile(
title: 'Can I view my past deliveries?',
child: const Text(
'Yes. Go to the History section from your dashboard to see completed deliveries and earnings.',
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
),
),
_FaqTile(
title: 'What if I face an issue during delivery?',
child: const Text(
'Use the Help Center to report an issue or contact support. Provide order details and a short description of the problem.',
style: TextStyle(fontSize: 16, color: Colors.black87, height: 1.4),
),
),
SizedBox(height: 10,),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Still stuck? Help is a mail away!",style: TextStyle(fontSize: 18,fontFamily: FontConstants.fontFamily,fontWeight: FontWeight.bold,color: ColorConstants.primaryColor),),
],
)
],
),
),
bottomNavigationBar: Padding(padding: EdgeInsets.all(16),
child: SizedBox(
height: 55,
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
foregroundColor: ColorConstants.primaryColor,
side: BorderSide(color: ColorConstants.primaryColor, width: 1.2),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const HelpCenterMessage(),
),
);
},
child: Text(
'Send a message',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold,fontFamily: FontConstants.fontFamily,color: Colors.white),
),
),
),),
),
);
}
}
class _FaqTile extends StatelessWidget {
final String title;
final Widget child;
final bool initiallyExpanded;
const _FaqTile({
required this.title,
required this.child,
this.initiallyExpanded = false,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: initiallyExpanded,
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: Text(
title,
style: TextStyle(fontWeight: FontWeight.w600,fontSize: 18,fontFamily: FontConstants.fontFamily),
),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
child: child,
),
],
),
),
);
}
}
// -------------------------------message page------------------------------------
class HelpCenterMessage extends StatefulWidget {
const HelpCenterMessage({super.key});
@override
State<HelpCenterMessage> createState() => _HelpCenterMessageState();
}
class _HelpCenterMessageState extends State<HelpCenterMessage> {
final _subjectController = TextEditingController();
final _messageController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void dispose() {
_subjectController.dispose();
_messageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 80,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Help Centre',
style: TextStyle(
fontSize: 26,
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
elevation: 4,
),
body: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Heading
Text(
'Send Us a Message',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 8),
Text(
"Not finding what you're looking for in the FAQs? Don't worry—we're here to help!",
style: TextStyle(
fontSize: 18,
color: Colors.grey.shade700,
height: 1.4,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 20),
// Subject label
Text(
'Subject',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _subjectController,
decoration: InputDecoration(
hintText: 'Type Something',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
),
),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter a subject' : null,
),
const SizedBox(height: 18),
// Message label
Text(
'Your Message',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _messageController,
minLines: 5,
maxLines: 8,
decoration: InputDecoration(
hintText: 'Type Something',
filled: true,
fillColor: Colors.white,
alignLabelWithHint: true,
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5),
),
),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter your message' : null,
),
SizedBox(height: 30,),
SizedBox(
height: 55,
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Message sent')),
);
Navigator.pop(context);
}
},
child: Text(
'Send a message',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,176 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
class NotificationsPage extends StatefulWidget {
const NotificationsPage({super.key});
@override
State<NotificationsPage> createState() => _NotificationsPageState();
}
class _NotificationsPageState extends State<NotificationsPage> {
List<Map<String, dynamic>> _items = const [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString('notifications_log');
List<Map<String, dynamic>> parsed = [];
if (raw != null && raw.isNotEmpty) {
try {
final list = jsonDecode(raw) as List<dynamic>;
parsed = list.map((e) => (e as Map).map((k, v) => MapEntry(k.toString(), v))).toList();
} catch (_) {}
}
if (!mounted) return;
setState(() {
_items = parsed;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70, // increases AppBar height
elevation: 4,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white), // white back arrow
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
'Notifications',
style: const TextStyle(
fontSize: 26, // larger font size
color: Colors.white, // white text
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
).copyWith(fontFamily: FontConstants.fontFamily), // keep your font
),
actions: [
IconButton(
icon: const Icon(Icons.delete_sweep, color: Colors.white), // white icon
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('notifications_log');
if (!mounted) return;
setState(() => _items = const []);
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Notifications cleared')),
);
},
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _items.isEmpty
? Center(child: Text('No notifications yet',style: TextStyle(fontSize: 20,fontFamily: FontConstants.fontFamily),))
: RefreshIndicator(
onRefresh: _load,
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: _items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final it = _items[index];
final title = (it['title'] ?? 'Nearle').toString();
final body = (it['body'] ?? '').toString();
final time = (it['time'] ?? '').toString();
final imageUrl = (it['imageUrl'] ?? '').toString();
final imagePath = (it['imagePath'] ?? '').toString();
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 1.5,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.notifications_active, color: Colors.purple),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
if (time.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
time,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
],
),
),
],
),
if (body.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
body,
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 14),
),
),
if (imagePath.isNotEmpty || imageUrl.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 10),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: imagePath.isNotEmpty
? Image.file(
File(imagePath),
height: 170,
width: double.infinity,
fit: BoxFit.cover,
)
: Image.network(
imageUrl,
height: 170,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
],
),
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,206 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
class OrderAlertSoundPage extends StatefulWidget {
const OrderAlertSoundPage({super.key});
@override
State<OrderAlertSoundPage> createState() => _OrderAlertSoundPageState();
}
class _OrderAlertSoundPageState extends State<OrderAlertSoundPage> {
static const String _prefsKey = 'order_alert_sound';
static const String _defaultSound = 'assets/audio/alert-1.mp3';
final AudioPlayer _player = AudioPlayer();
String _selected = _defaultSound;
bool _loading = true;
// Available sounds from assets/audio/ folder
final List<_SoundOption> _options = const [
_SoundOption(
label: 'Alert 1 (Default)',
assetPath: 'assets/audio/alert-1.mp3',
),
_SoundOption(label: 'Alert 2', assetPath: 'assets/audio/alert-2.mp3'),
_SoundOption(label: 'Alert 3', assetPath: 'assets/audio/alert-3.mp3'),
_SoundOption(label: 'Alert 4', assetPath: 'assets/audio/alert-4.mp3'),
_SoundOption(label: 'Alert 5', assetPath: 'assets/audio/alert-5.mp3'),
_SoundOption(label: 'Alert 6', assetPath: 'assets/audio/alert-6.mp3'),
_SoundOption(label: 'Alert 7', assetPath: 'assets/audio/alert-7.mp3'),
_SoundOption(label: 'Alert 8', assetPath: 'assets/audio/alert-8.mp3'),
_SoundOption(label: 'Alert 9', assetPath: 'assets/audio/alert-9.mp3'),
_SoundOption(label: 'Alert 10', assetPath: 'assets/audio/alert-10.mp3'),
];
@override
void initState() {
super.initState();
_loadSelection();
}
Future<void> _loadSelection() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_prefsKey);
setState(() {
_selected = (saved != null && saved.isNotEmpty) ? saved : _defaultSound;
_loading = false;
});
}
Future<void> _saveSelection(BuildContext context, String assetPath) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsKey, assetPath);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Order alert sound updated'),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
duration: const Duration(seconds: 2),
),
);
}
Future<void> _preview(BuildContext context, String assetPath) async {
try {
await _player.stop();
await _player.play(AssetSource(assetPath.replaceFirst('assets/', '')));
// Note: AssetSource expects relative to assets/ root; hence replaceFirst
} catch (_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Preview unavailable for: $assetPath'),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: const Duration(seconds: 2),
),
);
}
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70, // :small_blue_diamond: increases app bar height
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
), // :small_blue_diamond: white back arrow
onPressed: () {
Navigator.pop(context); // goes back to previous screen
},
),
title: const Text(
'Orders alert Sound',
style: TextStyle(
fontSize: 26, // :small_blue_diamond: larger font size
color: Colors.white, // :small_blue_diamond: white text
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
elevation: 4,
),
body: _loading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
color: Colors.grey.shade200,
child: Row(
children: [
const Icon(Icons.volume_up, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
'Current: ${_options.firstWhereOrNull((o) => o.assetPath == _selected)?.label ?? 'Unknown'}',
style: TextStyle(
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
const Text(
'Tap a sound to select',
style: TextStyle(fontSize: 12),
),
],
),
),
const Divider(height: 1),
Expanded(
child: ListView.separated(
itemCount: _options.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final opt = _options[index];
final isSelected = _selected == opt.assetPath;
return ListTile(
title: Text(
opt.label,
style: TextStyle(
fontFamily: FontConstants.fontFamily,
),
),
leading: Radio<String>(
value: opt.assetPath,
groupValue: _selected,
onChanged: (value) {
if (value == null) return;
setState(() => _selected = value);
_saveSelection(context, value);
},
),
trailing: IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _preview(context, opt.assetPath),
),
onTap: () {
setState(() => _selected = opt.assetPath);
_saveSelection(context, opt.assetPath);
},
subtitle: isSelected
? const Text(
'Selected',
style: TextStyle(fontSize: 12),
)
: null,
);
},
),
),
],
),
);
}
}
class _SoundOption {
final String label;
final String assetPath;
const _SoundOption({required this.label, required this.assetPath});
}

View File

@@ -0,0 +1,280 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:nearle/controllers/profile_controller.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
class Profile extends StatefulWidget {
const Profile({super.key});
@override
State<Profile> createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
String _name = '';
String _email = '';
String _contact = '';
String _address = '';
final List<Worker> _workers = [];
late final ProfileController _profileController =
Get.isRegistered<ProfileController>()
? Get.find<ProfileController>()
: Get.put(ProfileController(), permanent: true);
@override
void initState() {
super.initState();
_loadProfile();
// Keep in sync with controller
_workers.addAll([
ever(_profileController.userName, (_) => _assignFromController()),
ever(_profileController.userEmail, (_) => _assignFromController()),
ever(_profileController.userContact, (_) => _assignFromController()),
ever(_profileController.userAddress, (_) => _assignFromController()),
]);
_profileController.loadFromPrefs();
}
@override
void dispose() {
for (final worker in _workers) {
worker.dispose();
}
super.dispose();
}
Future<void> _loadProfile() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_name = prefs.getString('user_name') ?? '';
_email = prefs.getString('user_email') ?? '';
_contact = prefs.getString('contactno') ?? '';
_address = prefs.getString('user_address') ?? '';
});
debugPrint('[PROFILE_DETAILS] Loaded - Name: "$_name", Email: "$_email", Contact: "$_contact"');
}
void _assignFromController() {
setState(() {
if (_profileController.userName.value.trim().isNotEmpty) {
_name = _profileController.userName.value.trim();
}
if (_profileController.userEmail.value.trim().isNotEmpty) {
_email = _profileController.userEmail.value.trim();
}
if (_profileController.userContact.value.trim().isNotEmpty) {
_contact = _profileController.userContact.value.trim();
}
if (_profileController.userAddress.value.trim().isNotEmpty) {
_address = _profileController.userAddress.value.trim();
}
});
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
// ignore: unused_local_variable
final height = size.height;
return Scaffold(
backgroundColor: Colors.grey.shade200,
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(width * 0.04),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Profile image
Center(
child: Stack(
children: [
// White border circle
Container(
padding: const EdgeInsets.all(4), // border thickness
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white, // white border
),
child: CircleAvatar(
radius: 60,
backgroundColor: Colors.grey.shade400,
child: const Icon(
Icons.person,
size: 60,
color: Colors.white,
),
),
),
],
),
),
const SizedBox(height: 40),
// Name
_buildLabel("Enter name", required: true),
_buildTextField(
hintText: _name.isNotEmpty ? _name : "EX: Vijayan",
value: _name.isNotEmpty ? _name : null,
readOnly: true,
disabled: true,
),
const SizedBox(height: 16),
// Contact No
_buildLabel("Contact no", required: true),
_buildTextField(
hintText: _contact.isNotEmpty
? "+91 $_contact"
: "EX: +91 8838304677",
value: _contact.isNotEmpty ? "+91 $_contact" : null,
keyboardType: TextInputType.phone,
readOnly: true,
disabled: true,
),
const SizedBox(height: 16),
// Email Id
_buildLabel("Email Id"),
_buildTextField(
hintText: _email.isNotEmpty ? _email : "EX: gmail@gmail.com",
value: _email.isNotEmpty ? _email : null,
keyboardType: TextInputType.emailAddress,
readOnly: true,
disabled: true,
),
const SizedBox(height: 16),
// Location
_buildLabel("Address"),
_buildTextField(hintText: _address.isNotEmpty ? _address : " EX: R.s puram", value: _address.isNotEmpty ? _address : null, readOnly: true, disabled: true),
const SizedBox(height: 40),
],
),
),
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(width * 0.04),
child: SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _handleBackNavigation,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF5C1D8D), // Purple button color
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
"Back",
style: TextStyle(
fontSize: 21,
color: Colors.white,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
),
),
),
),
);
}
Future<void> _handleBackNavigation() async {
final navigator = Navigator.of(context);
if (navigator.canPop()) {
navigator.pop();
return;
}
final rootNavigator = Get.key.currentState;
if (rootNavigator != null && rootNavigator.canPop()) {
rootNavigator.pop();
return;
}
if (Get.isOverlaysOpen) {
Get.back(closeOverlays: true);
return;
}
Get.back();
}
// Text label widget
Widget _buildLabel(String text, {bool required = false}) {
return Align(
alignment: Alignment.centerLeft,
child: RichText(
text: TextSpan(
text: text,
style: TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
children: required
? const [
TextSpan(
text: " *",
style: TextStyle(color: Colors.red),
),
]
: [],
),
),
);
}
// Reusable TextField
Widget _buildTextField({
required String hintText,
String? value,
TextInputType keyboardType = TextInputType.text,
required bool readOnly,
bool disabled = false,
}) {
return Container(
margin: const EdgeInsets.only(top: 6),
child: SizedBox(
height: 55,
width: 350,
child: TextFormField(
keyboardType: keyboardType,
readOnly: readOnly,
enabled: !disabled,
enableInteractiveSelection: false,
initialValue: value,
decoration: InputDecoration(
hintText: hintText,
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:nearle/controllers/rewards_controller.dart';
import 'package:nearle/views/Dashboard/profile/rewards_card.dart';
class RiderRewardsPage extends StatelessWidget {
const RiderRewardsPage({super.key});
@override
Widget build(BuildContext context) {
final RewardsController rewardsController = Get.isRegistered<RewardsController>()
? Get.find<RewardsController>()
: Get.put(RewardsController());
return Scaffold(
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
title: Text(
"REWARDS",
style: TextStyle(
color: Colors.black,
fontSize: FontConstants.xxxLarge(context).sp,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
backgroundColor: Colors.grey.shade100,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
),
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
child: RewardsCard(
controller: rewardsController,
showFullDetails: true,
),
),
),
);
}
}

View File

@@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
class SavedAddressPage extends StatefulWidget {
const SavedAddressPage({super.key});
@override
State<SavedAddressPage> createState() => _SavedAddressPageState();
}
class _SavedAddressPageState extends State<SavedAddressPage> {
final TextEditingController _addressController = TextEditingController();
@override
void initState() {
super.initState();
_loadAddress();
}
Future<void> _loadAddress() async {
final prefs = await SharedPreferences.getInstance();
final address = (prefs.getString('user_address') ?? '').trim();
_addressController.text = address;
setState(() {});
}
@override
void dispose() {
_addressController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final addressText = _addressController.text.trim();
return Scaffold(
backgroundColor: const Color(0xFFF8F9FB),
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70,
elevation: 3,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: const Text(
'Saved Address',
style: TextStyle(
fontSize: 24,
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: size.width * 0.05, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 🏠 Header Section
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.15),
spreadRadius: 1,
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: ColorConstants.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(10),
child: Icon(
Icons.location_on_rounded,
color: ColorConstants.primaryColor,
size: 26,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current Address',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
fontFamily: FontConstants.fontFamily,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Text(
addressText.isNotEmpty ? addressText : 'No address saved yet.',
style: TextStyle(
fontSize: 15,
fontFamily: FontConstants.fontFamily,
color: Colors.grey.shade700,
height: 1.4,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 28),
// ✨ Info Section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: ColorConstants.primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.info_outline_rounded,
color: ColorConstants.primaryColor, size: 24),
const SizedBox(width: 12),
Expanded(
child: Text(
'Your saved address is used for deliveries, pickups, and nearby service accuracy.',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade800,
fontFamily: FontConstants.fontFamily,
height: 1.4,
),
),
),
],
),
),
const Spacer(),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,503 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:nearle/controllers/support_ticket.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:nearle/views/helpers/constants/Font_constant.dart';
class SupportTicket extends StatefulWidget {
const SupportTicket({super.key});
@override
State<SupportTicket> createState() => _SupportTicketState();
}
class _SupportTicketState extends State<SupportTicket>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
// Form
final _formKey = GlobalKey<FormState>();
final _subjectCtrl = TextEditingController();
final _messageCtrl = TextEditingController();
String _category = 'Account';
String _priority = 'Medium';
int _attachmentCount = 0;
// Image
final ImagePicker _picker = ImagePicker();
final List<XFile> _attachments = [];
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(() => setState(() {}));
Get.put(SupportTicketController());
}
@override
void dispose() {
_subjectCtrl.dispose();
_messageCtrl.dispose();
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
'Support Ticket',
style: TextStyle(
fontSize: 26,
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
fontFamily: FontConstants.fontFamily,
),
),
elevation: 4,
),
body: Column(
children: [
Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
indicatorColor: ColorConstants.primaryColor,
labelColor: ColorConstants.primaryColor,
unselectedLabelColor: Colors.grey,
labelStyle: TextStyle(
fontFamily: FontConstants.fontFamily,
fontWeight: FontWeight.bold,
fontSize: 16,
),
tabs: [
Tab(
child: Text(
'Create Tickets',
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 20),
),
),
Tab(
child: Text(
'My Tickets',
style: TextStyle(fontFamily: FontConstants.fontFamily, fontSize: 20),
),
),
],
),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [_buildCreateForm(), _buildTicketsList()],
),
),
],
),
bottomNavigationBar: _tabController.index == 0
? Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 55,
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: ColorConstants.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: _submitTicket,
child: Text(
'Submit Ticket',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
),
),
)
: null,
),
);
}
// ===============================================
// CREATE FORM
// ===============================================
Widget _buildCreateForm() {
final controller = Get.find<SupportTicketController>();
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Create a new support ticket',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily),
),
const SizedBox(height: 8),
Text(
'Tell us what went wrong. We\'ll get back to you as soon as possible.',
style: TextStyle(fontSize: 18, color: Colors.grey.shade700, height: 1.4, fontFamily: FontConstants.fontFamily),
),
const SizedBox(height: 20),
// Category
Text('Category', style: _labelStyle()),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
// ignore: deprecated_member_use
value: _category,
items: ['Account', 'Orders', 'Payments', 'App issue', 'Other']
.map((e) => DropdownMenuItem(value: e, child: Text(e, style: TextStyle(fontFamily: FontConstants.fontFamily))))
.toList(),
onChanged: (v) => setState(() => _category = v ?? _category),
decoration: _inputDecoration(),
),
const SizedBox(height: 16),
// Priority
Text('Priority', style: _labelStyle()),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: ['Low', 'Medium', 'High'].map((p) {
final selected = _priority == p;
return ChoiceChip(
label: Text(p, style: TextStyle(fontFamily: FontConstants.fontFamily, fontWeight: FontWeight.w600, fontSize: 16)),
selected: selected,
selectedColor: ColorConstants.primaryColor.withOpacity(0.15),
labelStyle: TextStyle(
color: selected ? ColorConstants.primaryColor : Colors.black87,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
onSelected: (_) => setState(() => _priority = p),
);
}).toList(),
),
const SizedBox(height: 16),
// Subject
Text('Subject', style: _labelStyle()),
const SizedBox(height: 8),
TextFormField(
controller: _subjectCtrl,
decoration: _inputDecoration(hint: 'Type Something'),
style: TextStyle(fontFamily: FontConstants.fontFamily),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter a subject' : null,
),
const SizedBox(height: 16),
// Message
Text('Describe the issue', style: _labelStyle()),
const SizedBox(height: 8),
TextFormField(
controller: _messageCtrl,
minLines: 5,
maxLines: 8,
decoration: _inputDecoration(hint: 'Type Something'),
style: TextStyle(fontFamily: FontConstants.fontFamily),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Please enter your message' : null,
),
const SizedBox(height: 16),
// Attachments
Row(
children: [
OutlinedButton.icon(
onPressed: _addAttachment,
icon: const Icon(Icons.attach_file),
label: Text('Add screenshot', style: TextStyle(fontFamily: FontConstants.fontFamily)),
),
const SizedBox(width: 12),
if (_attachmentCount > 0)
Text('$_attachmentCount attached', style: TextStyle(fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily)),
],
),
const SizedBox(height: 8),
if (_attachments.isNotEmpty)
Wrap(
spacing: 8,
runSpacing: 8,
children: _attachments.asMap().entries.map((entry) {
final idx = entry.key;
final file = entry.value;
return Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(File(file.path), width: 80, height: 80, fit: BoxFit.cover),
),
Positioned(
top: -8,
right: -8,
child: InkWell(
onTap: () => setState(() {
_attachments.removeAt(idx);
_attachmentCount = _attachments.length;
}),
child: Container(
width: 22,
height: 22,
decoration: BoxDecoration(color: Colors.black.withOpacity(0.6), shape: BoxShape.circle),
child: const Icon(Icons.close, size: 16, color: Colors.white),
),
),
),
],
);
}).toList(),
),
// Submit loading
Obx(() => controller.isSubmitting.value
? const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
: const SizedBox.shrink()),
],
),
),
);
}
// ===============================================
// MY TICKETS LIST
// ===============================================
Widget _buildTicketsList() {
final controller = Get.find<SupportTicketController>();
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.errorMessage.value.isNotEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 12),
Text('Failed to load tickets', style: TextStyle(fontFamily: FontConstants.fontFamily, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Text(controller.errorMessage.value, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600, fontFamily: FontConstants.fontFamily)),
const SizedBox(height: 16),
ElevatedButton(onPressed: controller.fetchTickets, child: const Text('Retry')),
],
),
),
);
}
if (controller.tickets.isEmpty) {
return _buildEmptyState();
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
itemCount: controller.tickets.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, i) {
final t = controller.tickets[i];
final statusColor = _getPriorityColor(t.priority);
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 2,
child: ListTile(
contentPadding: const EdgeInsets.all(12),
title: Text(t.subject, style: TextStyle(fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text('Category: ${t.category} • Priority: ${t.priority}', style: TextStyle(fontFamily: FontConstants.fontFamily)),
const SizedBox(height: 4),
Text('Created: ${_formatDate(t.created)}', style: TextStyle(fontFamily: FontConstants.fontFamily)),
],
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(color: statusColor.withOpacity(0.15), borderRadius: BorderRadius.circular(20)),
child: Text(t.priority, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily)),
),
),
);
},
);
});
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.support_agent, size: 48, color: Colors.grey),
const SizedBox(height: 12),
Text('No tickets yet', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily)),
const SizedBox(height: 8),
Text('Create your first ticket from the Create tab.', style: TextStyle(color: Colors.grey.shade700, fontFamily: FontConstants.fontFamily)),
],
),
),
);
}
// ===============================================
// HELPERS
// ===============================================
TextStyle _labelStyle() => TextStyle(fontSize: 20, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily);
InputDecoration _inputDecoration({String? hint}) {
return InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: Colors.grey.shade300)),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: ColorConstants.primaryColor, width: 1.5)),
hintStyle: TextStyle(fontFamily: FontConstants.fontFamily),
);
}
Color _getPriorityColor(String priority) {
return switch (priority.toLowerCase()) {
'high' => Colors.red,
'medium' => Colors.orange,
'low' => Colors.green,
_ => Colors.grey,
};
}
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
}
// ===============================================
// IMAGE PICKER
// ===============================================
Future<void> _addAttachment() async {
final source = await showModalBottomSheet<ImageSource>(
context: context,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (ctx) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo_library),
title: Text('Gallery', style: TextStyle(fontFamily: FontConstants.fontFamily)),
onTap: () => Navigator.pop(ctx, ImageSource.gallery),
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: Text('Camera', style: TextStyle(fontFamily: FontConstants.fontFamily)),
onTap: () => Navigator.pop(ctx, ImageSource.camera),
),
],
),
),
);
if (source == null) return;
try {
if (source == ImageSource.gallery) {
final multi = await _picker.pickMultiImage(imageQuality: 85);
if (multi.isNotEmpty) {
setState(() => _attachments.addAll(multi));
} else {
final one = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85);
if (one != null) setState(() => _attachments.add(one));
}
} else {
final captured = await _picker.pickImage(source: ImageSource.camera, imageQuality: 85);
if (captured != null) setState(() => _attachments.add(captured));
}
} catch (_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to pick image', style: TextStyle(fontFamily: FontConstants.fontFamily))),
);
}
setState(() => _attachmentCount = _attachments.length);
}
// ===============================================
// SUBMIT TICKET
// ===============================================
Future<void> _submitTicket() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
final controller = Get.find<SupportTicketController>();
final success = await controller.createTicket(
userid: 1242,
category: _category,
priority: _priority,
subject: _subjectCtrl.text.trim(),
issue: _messageCtrl.text.trim(),
attachments: _attachments.isEmpty ? null : _attachments,
);
if (success) {
_subjectCtrl.clear();
_messageCtrl.clear();
_attachments.clear();
_attachmentCount = 0;
setState(() {});
_tabController.animateTo(1);
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Ticket Submitted!'),
content: const Text('Your ticket has been created and saved. Our team will get back to you soon.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK')),
],
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to submit ticket: ${controller.errorMessage.value}'),
backgroundColor: Colors.red,
),
);
}
}
}

View File

@@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:nearle/views/helpers/constants/Colorconstants.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
// ===== Controller =====
class TermsController extends GetxController {
WebViewController? webViewController;
var isLoading = true.obs;
@override
void onInit() {
super.onInit();
initializeWebView();
}
void initializeWebView() {
webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
isLoading.value = true;
print('Started loading: $url');
},
onPageFinished: (url) {
isLoading.value = false;
print('Finished loading: $url');
},
onWebResourceError: (error) {
isLoading.value = false;
print('WebView error: ${error.description}');
},
),
);
loadTermsUrl();
}
Future<void> loadTermsUrl() async {
if (webViewController != null) {
try {
await webViewController!.loadRequest(
Uri.parse('https://nearle.in/terms'),
);
} catch (e) {
print('Error loading URL: $e');
}
}
}
}
// ===== Page =====
class TermsCondition extends StatelessWidget {
const TermsCondition({super.key});
@override
Widget build(BuildContext context) {
final controller = Get.put(TermsController());
return Scaffold(
appBar: AppBar(
backgroundColor: ColorConstants.primaryColor,
centerTitle: true,
toolbarHeight: 70,
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context);
},
),
title: const Text(
'Terms & Conditions',
style: TextStyle(
fontSize: 26,
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
elevation: 4,
),
body: SafeArea(
child: Obx(() {
final wvc = controller.webViewController;
return Stack(
children: [
if (wvc != null) WebViewWidget(controller: wvc),
if (controller.isLoading.value)
const LinearProgressIndicator(minHeight: 2),
],
);
}),
),
);
}
}