Commit 5ac94fe2 authored by Sai Srinivas's avatar Sai Srinivas
Browse files

New Screen and apis

parent df116895
......@@ -25,7 +25,7 @@ android {
applicationId = "in.webgrid.genrentals"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 23
targetSdk = 36
versionCode = flutter.versionCode
versionName = flutter.versionName
......
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:label="Gen Rentals"
android:name="${applicationName}"
......
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.4376 1.70711C13.8281 1.31658 13.8281 0.683417 13.4376 0.292893C13.0471 -0.0976311 12.4139 -0.097631 12.0234 0.292893L12.7305 1L13.4376 1.70711ZM-3.43156e-05 12.7305C-3.40162e-05 13.2828 0.447681 13.7305 0.999966 13.7305L9.99997 13.7305C10.5523 13.7305 11 13.2828 11 12.7305C11 12.1782 10.5523 11.7305 9.99997 11.7305L1.99997 11.7305L1.99997 3.7305C1.99997 3.17822 1.55225 2.7305 0.999965 2.7305C0.447681 2.7305 -3.46572e-05 3.17822 -3.42735e-05 3.7305L-3.43156e-05 12.7305ZM12.7305 1L12.0234 0.292893L0.292859 12.0234L0.999966 12.7305L1.70707 13.4376L13.4376 1.70711L12.7305 1Z" fill="#4CAF50"/>
<path d="M13.4376 1.70711C13.8281 1.31658 13.8281 0.683417 13.4376 0.292893C13.0471 -0.0976311 12.4139 -0.097631 12.0234 0.292893L12.7305 1L13.4376 1.70711ZM-3.43156e-05 12.7305C-3.40162e-05 13.2828 0.447681 13.7305 0.999966 13.7305L9.99997 13.7305C10.5523 13.7305 11 13.2828 11 12.7305C11 12.1782 10.5523 11.7305 9.99997 11.7305L1.99997 11.7305L1.99997 3.7305C1.99997 3.17822 1.55225 2.7305 0.999965 2.7305C0.447681 2.7305 -3.46572e-05 3.17822 -3.42735e-05 3.7305L-3.43156e-05 12.7305ZM12.7305 1L12.0234 0.292893L0.292859 12.0234L0.999966 12.7305L1.70707 13.4376L13.4376 1.70711L12.7305 1Z" fill="#F00000"/>
</svg>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.292893 12.0234C-0.0976311 12.4139 -0.0976311 13.0471 0.292893 13.4376C0.683418 13.8281 1.31658 13.8281 1.70711 13.4376L1 12.7305L0.292893 12.0234ZM13.7305 0.999965C13.7305 0.44768 13.2828 -3.50516e-05 12.7305 -3.53045e-05L3.7305 -3.45037e-05C3.17822 -3.48408e-05 2.7305 0.447681 2.7305 0.999965C2.7305 1.55225 3.17822 1.99996 3.7305 1.99997L11.7305 1.99996L11.7305 9.99996C11.7305 10.5522 12.1782 11 12.7305 11C13.2828 11 13.7305 10.5522 13.7305 9.99997L13.7305 0.999965ZM1 12.7305L1.70711 13.4376L13.4376 1.70707L12.7305 0.999965L12.0234 0.292858L0.292893 12.0234L1 12.7305Z" fill="#ED3424"/>
<path d="M0.292893 12.0234C-0.0976311 12.4139 -0.0976311 13.0471 0.292893 13.4376C0.683418 13.8281 1.31658 13.8281 1.70711 13.4376L1 12.7305L0.292893 12.0234ZM13.7305 0.999965C13.7305 0.44768 13.2828 -3.50516e-05 12.7305 -3.53045e-05L3.7305 -3.45037e-05C3.17822 -3.48408e-05 2.7305 0.447681 2.7305 0.999965C2.7305 1.55225 3.17822 1.99996 3.7305 1.99997L11.7305 1.99996L11.7305 9.99996C11.7305 10.5522 12.1782 11 12.7305 11C13.2828 11 13.7305 10.5522 13.7305 9.99997L13.7305 0.999965ZM1 12.7305L1.70711 13.4376L13.4376 1.70707L12.7305 0.999965L12.0234 0.292858L0.292893 12.0234L1 12.7305Z" fill="#4CAF50"/>
</svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_591_316)">
<mask id="mask0_591_316" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_591_316)">
<path d="M14.6666 8.66699V10.0003C14.6666 13.3337 13.3333 14.667 9.99992 14.667H5.99992C2.66659 14.667 1.33325 13.3337 1.33325 10.0003V8.98699" stroke="#2D2D2D" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.33325 1.33301H5.99992C2.66659 1.33301 1.33325 2.66634 1.33325 5.99967" stroke="#2D2D2D" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.2866 6.00586L13.9866 5.30586C14.8933 4.39919 15.3199 3.34586 13.9866 2.01253C12.6533 0.679192 11.5999 1.10586 10.6933 2.01253L5.43992 7.26587C5.23992 7.46587 5.03992 7.85921 4.99992 8.14587L4.71325 10.1525C4.60659 10.8792 5.11992 11.3859 5.84659 11.2859L7.85327 10.9992C8.13327 10.9592 8.5266 10.7592 8.73327 10.5592L10.8533 8.43921L11.3399 7.95254" stroke="#2D2D2D" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.94019 2.7666C10.3869 4.35993 11.6335 5.6066 13.2335 6.05993" stroke="#2D2D2D" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<defs>
<clipPath id="clip0_591_316">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_383_1601)">
<path d="M4.00647 4.47004C4.00647 6.93496 6.0116 8.94009 8.47651 8.94009C10.9414 8.94009 12.9466 6.93496 12.9466 4.47004C12.9466 2.00513 10.9414 0 8.47651 0C6.0116 0 4.00647 2.00513 4.00647 4.47004Z" fill="#45C1F1"/>
<path d="M12.9466 4.47004C12.9466 2.00513 10.9415 0 8.47656 0V8.94009C10.9415 8.94009 12.9466 6.93496 12.9466 4.47004Z" fill="#44A4EC"/>
<path d="M1.02649 16.4565C1.02649 16.7311 1.24863 16.9532 1.52316 16.9532H15.43C15.7045 16.9532 15.9266 16.7311 15.9266 16.4565C15.9266 12.8964 13.03 9.93359 9.4699 9.93359H7.48322C3.92308 9.93359 1.02649 12.8964 1.02649 16.4565Z" fill="#45C1F1"/>
<path d="M9.46991 9.93359H8.47656V16.9532H15.43C15.7045 16.9532 15.9266 16.7311 15.9266 16.4565C15.9266 12.8964 13.03 9.93359 9.46991 9.93359Z" fill="#44A4EC"/>
</g>
<defs>
<clipPath id="clip0_383_1601">
<rect width="16.9531" height="16.9531" fill="white"/>
</clipPath>
</defs>
</svg>
......@@ -6,16 +6,17 @@ class CommonResponse {
CommonResponse({this.error, this.balance, this.message});
CommonResponse.fromJson(Map<String, dynamic> json) {
error = json['error'];
balance = json['balance'];
message = json['message'];
error = int.tryParse(json['error']?.toString() ?? '');
balance = int.tryParse(json['balance']?.toString() ?? '');
message = json['message']?.toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['error'] = this.error;
data['balance'] = this.balance;
data['message'] = this.message;
return data;
return {
'error': error,
'balance': balance,
'message': message,
};
}
}
class TicketChatDisplayResponse {
String? status;
String? ticketId;
List<FeedBacks>? feedBacks;
String? error;
String? message;
TicketChatDisplayResponse(
{this.status, this.ticketId, this.feedBacks, this.error, this.message});
TicketChatDisplayResponse.fromJson(Map<String, dynamic> json) {
status = json['status'];
ticketId = json['ticket_id'];
if (json['feed_backs'] != null) {
feedBacks = <FeedBacks>[];
json['feed_backs'].forEach((v) {
feedBacks!.add(new FeedBacks.fromJson(v));
});
}
error = json['error'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['status'] = this.status;
data['ticket_id'] = this.ticketId;
if (this.feedBacks != null) {
data['feed_backs'] = this.feedBacks!.map((v) => v.toJson()).toList();
}
data['error'] = this.error;
data['message'] = this.message;
return data;
}
}
class FeedBacks {
String? id;
String? text;
List<String>? imgNames;
List<String>? images;
String? user;
String? userName;
String? createdDatetime;
String? userImg;
FeedBacks(
{this.id,
this.text,
this.imgNames,
this.images,
this.user,
this.userName,
this.createdDatetime});
FeedBacks.fromJson(Map<String, dynamic> json) {
id = json['id'];
text = json['text'];
imgNames = json['img_names'].cast<String>();
images = json['images'].cast<String>();
user = json['user'];
userName = json['user_name'];
createdDatetime = json['created_datetime'];
userImg = json['user_img'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['text'] = this.text;
data['img_names'] = this.imgNames;
data['images'] = this.images;
data['user'] = this.user;
data['user_name'] = this.userName;
data['created_datetime'] = this.createdDatetime;
data['user_img'] = this.userImg;
return data;
}
}
class TicketListResponse {
final Tickets? tickets;
final String? error;
final String? message;
TicketListResponse({this.tickets, this.error, this.message});
factory TicketListResponse.fromJson(Map<String, dynamic> json) {
return TicketListResponse(
tickets:
json['tickets'] != null ? Tickets.fromJson(json['tickets']) : null,
error: json['error']?.toString(),
message: json['message']?.toString(),
);
}
Map<String, dynamic> toJson() => {
if (tickets != null) 'tickets': tickets!.toJson(),
'error': error,
'message': message,
};
}
class Tickets {
final List<TicketItem>? closed;
final List<TicketItem>? inProgress;
Tickets({this.closed, this.inProgress});
factory Tickets.fromJson(Map<String, dynamic> json) {
return Tickets(
closed: (json['closed'] as List?)
?.map((v) => TicketItem.fromJson(v))
.toList(),
inProgress: (json['in_progress'] as List?)
?.map((v) => TicketItem.fromJson(v))
.toList(),
);
}
Map<String, dynamic> toJson() => {
if (closed != null)
'closed': closed!.map((v) => v.toJson()).toList(),
if (inProgress != null)
'in_progress': inProgress!.map((v) => v.toJson()).toList(),
};
}
class TicketItem {
final String? id;
final String? ticketNumber;
final String? type;
final String? date;
TicketItem({this.id, this.ticketNumber, this.type, this.date});
factory TicketItem.fromJson(Map<String, dynamic> json) {
return TicketItem(
id: json['id']?.toString(),
ticketNumber: json['ticket_number']?.toString(),
type: json['type']?.toString(),
date: json['date']?.toString(),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'ticket_number': ticketNumber,
'type': type,
'date': date,
};
}
class TicketChatDisplayResponse {
List<Ticket>? ticket;
int? error;
String? message;
TicketChatDisplayResponse({this.ticket, this.error, this.message});
TicketChatDisplayResponse.fromJson(Map<String, dynamic> json) {
if (json['ticket'] != null) {
ticket = <Ticket>[];
json['ticket'].forEach((v) {
ticket!.add(new Ticket.fromJson(v));
});
}
error = json['error'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.ticket != null) {
data['ticket'] = this.ticket!.map((v) => v.toJson()).toList();
}
data['error'] = this.error;
data['message'] = this.message;
return data;
}
}
class Ticket {
String? tid;
String? datetime;
String? msg;
String? type;
Ticket({this.tid, this.datetime, this.msg, this.type});
Ticket.fromJson(Map<String, dynamic> json) {
tid = json['tid'];
datetime = json['datetime'];
msg = json['msg'];
type = json['type'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['tid'] = this.tid;
data['datetime'] = this.datetime;
data['msg'] = this.msg;
data['type'] = this.type;
return data;
}
}
class TicketListResponse {
List<Ticket>? ticket;
int? error;
String? message;
TicketListResponse({this.ticket, this.error, this.message});
TicketListResponse.fromJson(Map<String, dynamic> json) {
if (json['ticket'] != null) {
ticket = <Ticket>[];
json['ticket'].forEach((v) {
ticket!.add(new Ticket.fromJson(v));
});
}
error = json['error'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this.ticket != null) {
data['ticket'] = this.ticket!.map((v) => v.toJson()).toList();
}
data['error'] = this.error;
data['message'] = this.message;
return data;
}
}
class Ticket {
String? tid;
String? datetime;
String? status;
String? subject;
Ticket({this.tid, this.datetime, this.status, this.subject});
Ticket.fromJson(Map<String, dynamic> json) {
tid = json['tid'];
datetime = json['datetime'];
status = json['status'];
subject = json['subject'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['tid'] = this.tid;
data['datetime'] = this.datetime;
data['status'] = this.status;
data['subject'] = this.subject;
return data;
}
}
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:gen_rentals/Services/api_calling.dart';
import '../Models/HelpAndEnquiryModels/TicketChatDisplayResponse.dart';
import '../Models/HelpAndEnquiryModels/ticketListResponse.dart';
import '../Utility/CustomSnackbar.dart';
class HelpAndEnquiryProvider extends ChangeNotifier {
bool _isLoading = false;
bool get isLoading => _isLoading;
String? _message;
String? get message => _message;
int? _balance;
int? get balance => _balance;
Future<bool> submitEnquiry({
required String sessionId,
required String accId,
required String name,
required String email,
required String mobile,
required String requirement,
required String note,
}) async {
try {
_isLoading = true;
_message = null;
notifyListeners();
final response = await ApiCalling.submitEnquiryApi(
sessionId,
accId,
name,
email,
mobile,
requirement,
note,
);
_isLoading = false;
if (response != null) {
_message = response.message;
_balance = response.balance;
notifyListeners();
if (response.error == 0) {
// success
debugPrint("✅ Enquiry submitted successfully: ${response.message}");
return true;
} else {
// failed
debugPrint("⚠️ Failed to submit enquiry: ${response.message}");
return false;
}
} else {
_isLoading = false;
notifyListeners();
debugPrint("❌ Null response received.");
return false;
}
} catch (e) {
_isLoading = false;
notifyListeners();
debugPrint("❌ Exception in submitEnquiry: $e");
return false;
}
}
String? _errorMessage;
TicketListResponse? _ticketListResponse;
String? get errorMessage => _errorMessage;
TicketListResponse? get ticketListResponse => _ticketListResponse;
/// ✅ Fetch ticket list from API
Future<void> fetchTicketList({
required String sessionId,
required String accId,
}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final response =
await ApiCalling.fetchTicketListApi(sessionId, accId,);
if (response != null && response.error == "0") {
_ticketListResponse = response;
} else {
_errorMessage = response?.message ?? "Something went wrong";
}
} catch (e) {
debugPrint("❌ Provider Error (fetchTicketList): $e");
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}
/// ✅ Optional: Clear state (useful on logout or refresh)
void clearTickets() {
_ticketListResponse = null;
_errorMessage = null;
notifyListeners();
}
TicketChatDisplayResponse? _chatResponse;
TicketChatDisplayResponse? get chatResponse => _chatResponse;
/// Fetch chat details for a specific ticket
Future<void> fetchTicketChatDisplay({
required String sessionId,
required String accId,
required String ticketId,
}) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();
try {
final response = await ApiCalling.fetchTicketChatDisplayApi(
sessionId,
accId,
ticketId,
);
if (response != null && response.error == "0") {
_chatResponse = response;
_errorMessage = null;
} else {
_chatResponse = null;
_errorMessage = response?.message ?? "Something went wrong";
}
} catch (e) {
debugPrint("❌ Provider Error (fetchTicketChatDisplay): $e");
_errorMessage = e.toString();
_chatResponse = null;
} finally {
_isLoading = false;
notifyListeners();
}
}
/// Optional: Clear data (for refresh or reset)
void clearChatData() {
_chatResponse = null;
_errorMessage = null;
notifyListeners();
}
bool _isSending = false;
bool get isSending => _isSending;
void _setSending(bool value) {
_isSending = value;
notifyListeners();
}
Future<void> sendMessage(
BuildContext context, {
required String sessionId,
required String accId,
required String ticketId,
required String msgText,
required List<File> images,
}) async {
_setSending(true);
try {
final response = await ApiCalling.addMessageApi(
sessionId,
accId,
ticketId,
msgText,
images,
);
// Check if widget is still mounted before showing dialogs
if (!context.mounted) return;
if (response != null || response?.error == 0) {
CustomSnackBar.showSuccess(
context: context,
message: response?.message ?? "Message sent successfully!",
);
// Refresh the chat after sending message
if (context.mounted) {
fetchTicketChatDisplay(
sessionId: sessionId,
accId: accId,
ticketId: ticketId,
);
}
} else {
CustomSnackBar.showError(
context: context,
message: response?.message ?? "Failed to send message!",
);
}
} catch (e) {
debugPrint("❌ sendMessage error: $e");
// Check if widget is still mounted before showing error
if (context.mounted) {
CustomSnackBar.showError(
context: context,
message: "Error sending message",
);
}
} finally {
_setSending(false);
}
}
}
......@@ -17,25 +17,24 @@ class _BillDetailListScreenState extends State<BillDetailListScreen> {
top: false,
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
elevation: 0,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context),
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/continue_left_ic.svg",
height: 25,
width: 25,
),
),
const SizedBox(width: 12),
const SizedBox(width: 10),
const Text(
"Bill Details",
"Bill List",
style: TextStyle(
fontSize: 16,
fontFamily: "Poppins",
fontWeight: FontWeight.w500,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
......@@ -126,16 +125,41 @@ class _BillDetailListScreenState extends State<BillDetailListScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Bill Cycle",
"Invoice raised against",
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.normalText,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
Text(
"#1253", // Fixed date range
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.amountText,
fontWeight: FontWeight.w400, // Medium for dates
fontSize: 12,
),
),
],
),
SizedBox(height: 4,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"order ID: 1253",
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.subtitleText,
color: AppColors.normalText,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
Text(
"7th Sep 2025 - 7th Oct 2025", // Fixed date range
"7th Oct 2025", // Fixed date range
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.normalText,
......@@ -190,7 +214,8 @@ class _BillDetailListScreenState extends State<BillDetailListScreen> {
}
Widget _buildBillItem({
String title = "Bill Cycle",
String orderId = "#1253",
String title = "Invoice raised against",
required String fromDate,
required String toDate,
required String amount,
......@@ -217,16 +242,42 @@ class _BillDetailListScreenState extends State<BillDetailListScreen> {
children: [
Text(
title,
orderId,
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.subtitleText,
color: AppColors.amountText,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.normal,
fontSize: 12,
),
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.normalText,
fontWeight: FontWeight.w400,
fontStyle: FontStyle.normal,
fontSize: 14,
),
),
Text(
"₹$amount",
style: TextStyle(
fontFamily: "Poppins",
color: AppColors.amountText,
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
......
This diff is collapsed.
......@@ -17,6 +17,28 @@ class _HelpTicketScreenState extends State<HelpTicketScreen> {
List<String> _selectedImages = [];
String _selectedReason = 'Payment Issue';
// Dummy data for help - with proper null safety
final List<Map<String, dynamic>> createNewTickets = [
{
'title': 'Payment Issues',
'description': 'Get help with payment related problems',
'icon': "assets/svg/rupee_coin_ic.svg",
'color': Color(0xFFFFEFBE),
},
{
'title': 'Bill Related Issues',
'description': 'Resolve bill and invoice matters',
'icon': "assets/svg/know_pay.svg",
'color': Color(0xFFCEF9FF),
},
{
'title': 'Other Issues',
'description': 'Any other support you need',
'icon': 'assets/svg/help_ic.svg',
'color': Color(0xFFE4E5FF),
},
];
@override
void initState() {
super.initState();
......@@ -27,7 +49,6 @@ class _HelpTicketScreenState extends State<HelpTicketScreen> {
@override
Widget build(BuildContext context) {
final isEditable = widget.reason == null;
final showOtherReasonField = _selectedReason == 'Other Issues';
return SafeArea(
......@@ -66,34 +87,50 @@ class _HelpTicketScreenState extends State<HelpTicketScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Section Title
const SectionHeading(title: 'Create New Ticket'),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SectionHeading(title: 'Create New Ticket'),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Text(
"order #1235",
style: TextStyle(
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
],
),
const SizedBox(height: 12),
/// Reason Label
_fieldLabel("Reason"),
const SizedBox(height: 6),
/// Reason Dropdown
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Color(0xffE0E0E0),
borderRadius: BorderRadius.circular(12),
),
child: isEditable
? DropdownButtonFormField<String>(
value: _selectedReason,
items: [
'Payment Issue',
'Bill Related Issues',
'Other Issues',
].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(
value,
/// Reason Selection Button - Opens Bottom Sheet
GestureDetector(
onTap: _showReasonBottomSheet,
child: Container(
width: 200,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Color(0xffFFF3D1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_selectedReason,
style: const TextStyle(
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
......@@ -101,25 +138,11 @@ class _HelpTicketScreenState extends State<HelpTicketScreen> {
color: Colors.black87,
),
),
);
}).toList(),
onChanged: (newValue) {
setState(() {
_selectedReason = newValue!;
});
},
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
)
: Text(
_selectedReason,
style: const TextStyle(
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w500,
color: Colors.black87,
SvgPicture.asset(
"assets/svg/edit_ic.svg",
height: 25,
),
],
),
),
),
......@@ -267,7 +290,7 @@ class _HelpTicketScreenState extends State<HelpTicketScreen> {
return Text(
text,
style: TextStyle(
fontSize: 12,
fontSize: 14,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w500,
color: Colors.grey[700],
......@@ -341,4 +364,120 @@ class _HelpTicketScreenState extends State<HelpTicketScreen> {
_otherReasonController.dispose();
super.dispose();
}
}
void _showReasonBottomSheet() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
builder: (context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Select Your Reason",
style: TextStyle(
fontSize: 18,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.99,
),
itemCount: createNewTickets.length,
itemBuilder: (context, index) {
final ticket = createNewTickets[index];
final String title = ticket['title'] ?? 'Unknown';
final String icon = ticket['icon'] ?? 'assets/svg/help_ic.svg';
final Color color = ticket['color'] ?? Colors.grey;
return _buildReasonCard(
title: title,
icon: icon,
color: color,
);
},
),
const SizedBox(height: 24),
],
),
);
},
);
}
Widget _buildReasonCard({
required String title,
required String icon,
required Color color,
}) {
return GestureDetector(
onTap: () {
setState(() {
_selectedReason = title;
});
Navigator.pop(context); // Close the bottom sheet
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Icon container
Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: color.withOpacity(0.12), // Fixed opacity
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: SizedBox(
height: 40,
width: 40,
child: SvgPicture.asset(
icon,
fit: BoxFit.fitWidth,
),
),
),
),
const SizedBox(height: 8),
// Title
SizedBox(
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.nearDarkText,
fontSize: 14,
fontWeight: FontWeight.w400,
fontFamily: "Plus Jakarta Sans",
),
),
),
const SizedBox(height: 4),
],
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import '../../Notifier/HelpAndEnquiryProvider.dart';
import '../../Utility/AppColors.dart';
import '../../Utility/Reusablewidgets.dart';
import '../../Utility/CustomSnackbar.dart';
class EnquiryScreen extends StatefulWidget {
const EnquiryScreen({super.key});
final String sessionId;
final String accId;
const EnquiryScreen({
super.key,
required this.sessionId,
required this.accId,
});
@override
State<EnquiryScreen> createState() => _EnquiryScreenState();
}
class _EnquiryScreenState extends State<EnquiryScreen> {
final _formKey = GlobalKey<FormState>();
final TextEditingController nameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
......@@ -19,13 +30,15 @@ class _EnquiryScreenState extends State<EnquiryScreen> {
@override
Widget build(BuildContext context) {
final enquiryProvider = Provider.of<HelpAndEnquiryProvider>(context);
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: AppColors.backgroundRegular,
backgroundColor: Color(0xFFffffff),
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
backgroundColor: Color(0xFFFCFCFC),
elevation: 0,
title: Row(
children: [
......@@ -53,63 +66,147 @@ class _EnquiryScreenState extends State<EnquiryScreen> {
// Main Body
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_labelText("Name"),
_textField(nameController, "Enter Name"),
const SizedBox(height: 16),
_labelText("Email Id"),
_textField(emailController, "Enter Email ID"),
const SizedBox(height: 16),
_labelText("Phone No."),
_textField(phoneController, "Enter Phone Number",
keyboardType: TextInputType.phone),
const SizedBox(height: 16),
_labelText("Requirement"),
_textField(requirementController, "Enter Requirement"),
const SizedBox(height: 16),
_labelText("Note"),
_textField(
noteController,
"Write a short note",
maxLines: 5,
),
const SizedBox(height: 32),
// Submit button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Submit action
FocusScope.of(context).unfocus();
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_labelText("Name"),
_textField(
controller: nameController,
hint: "Enter Name",
fieldName: "Name",
),
const SizedBox(height: 16),
_labelText("Email Id"),
_textField(
controller: emailController,
hint: "Enter Email ID",
fieldName: "Email",
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Please enter your email";
}
if (!RegExp(r'^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value.trim())) {
return "Enter a valid email";
}
return null;
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.buttonColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
const SizedBox(height: 16),
_labelText("Phone No."),
_textField(
controller: phoneController,
hint: "Enter Phone Number",
fieldName: "Phone Number",
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Please enter your phone number";
}
if (value.trim().length < 10) {
return "Enter a valid phone number";
}
return null;
},
),
const SizedBox(height: 16),
_labelText("Requirement"),
_textField(
controller: requirementController,
hint: "Enter Requirement",
fieldName: "Requirement",
),
const SizedBox(height: 16),
_labelText("Note"),
_textField(
controller: noteController,
hint: "Write a short note",
fieldName: "Note",
maxLines: 5,
),
const SizedBox(height: 32),
// Submit button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: enquiryProvider.isLoading
? null
: () async {
FocusScope.of(context).unfocus();
if (!_formKey.currentState!.validate()) return;
final success =
await enquiryProvider.submitEnquiry(
sessionId: widget.sessionId,
accId: widget.accId,
name: nameController.text.trim(),
email: emailController.text.trim(),
mobile: phoneController.text.trim(),
requirement: requirementController.text.trim(),
note: noteController.text.trim(),
);
if (!mounted) return;
if (success) {
CustomSnackBar.showSuccess(
context: context,
message: enquiryProvider.message ??
"Enquiry submitted successfully!",
);
_formKey.currentState!.reset();
nameController.clear();
emailController.clear();
phoneController.clear();
requirementController.clear();
noteController.clear();
} else {
CustomSnackBar.showError(
context: context,
message: enquiryProvider.message ??
"Failed to submit enquiry!",
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.buttonColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
elevation: 0,
),
elevation: 0,
),
child: const Text(
"Submit",
style: TextStyle(
fontSize: 16,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
child: enquiryProvider.isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
"Submit",
style: TextStyle(
fontSize: 16,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 16),
],
],
),
),
),
),
......@@ -129,34 +226,69 @@ class _EnquiryScreenState extends State<EnquiryScreen> {
);
}
/// Rounded Input Field
Widget _textField(
TextEditingController controller,
String hint, {
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
fontSize: 14,
color: Colors.grey[400],
fontFamily: "Plus Jakarta Sans",
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
border: InputBorder.none,
),
),
/// Text Field with error message shown below box
Widget _textField({
required TextEditingController controller,
required String hint,
required String fieldName,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
String? Function(String?)? validator,
}) {
return FormField<String>(
validator: validator ??
(value) {
if (controller.text.trim().isEmpty) {
return '$fieldName is required';
}
return null;
},
builder: (field) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Color(0xffF6F6F8),
borderRadius: BorderRadius.circular(12),
// border: Border.all(
// color: field.hasError ? Colors.red : Colors.transparent,
// width: 1,
// ),
),
child: TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
onChanged: (_) => field.didChange(controller.text),
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(
fontSize: 14,
color: Colors.grey[400],
fontFamily: "Plus Jakarta Sans",
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
border: InputBorder.none,
),
),
),
if (field.hasError)
Padding(
padding: const EdgeInsets.only(top: 5, left: 4),
child: Text(
field.errorText ?? '',
style: const TextStyle(
color: Colors.red,
fontSize: 12,
fontFamily: "Plus Jakarta Sans",
),
),
),
],
);
},
);
}
......
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gen_rentals/Screens/HelpScreens/CreateTicketScreen.dart';
import 'package:gen_rentals/Screens/HelpScreens/OrderHelpScreen.dart';
import 'package:gen_rentals/Screens/HelpScreens/ProcessTicketScreen.dart';
import 'package:gen_rentals/Utility/Reusablewidgets.dart';
import '../../Notifier/HelpAndEnquiryProvider.dart';
import '../../Utility/AppColors.dart';
import 'package:provider/provider.dart';
class HelpScreen extends StatefulWidget {
const HelpScreen({super.key});
final String sessionId;
final String accId;
HelpScreen({
super.key,
required this.sessionId,
required this.accId,
});
@override
State<HelpScreen> createState() => _HelpScreenState();
}
class _HelpScreenState extends State<HelpScreen> {
// Dummy data for help - with proper null safety
@override
void initState() {
super.initState();
/// ✅ Fetch ticket list on screen load
Future.microtask(() async {
final provider = Provider.of<HelpAndEnquiryProvider>(context, listen: false);
await provider.fetchTicketList(
sessionId: widget.sessionId,
accId: widget.accId,
);
});
}
// ✅ (unchanged)
final List<Map<String, dynamic>> createNewTickets = [
{
'title': 'Payment Issues',
......@@ -36,16 +59,6 @@ class _HelpScreenState extends State<HelpScreen> {
},
];
final List<Map<String, String>> processingTickets = [
{'title': 'Payment Issue', 'date': '25th Jan 2025', 'status': 'In Process'},
];
final List<Map<String, String>> closedTickets = [
{'title': 'Bill Payments', 'date': '25th Jan 2025'},
{'title': 'Others', 'date': '25th Jan 2025'},
{'title': 'Payment Issue', 'date': '25th Jan 2025'},
];
@override
Widget build(BuildContext context) {
return SafeArea(
......@@ -65,7 +78,7 @@ class _HelpScreenState extends State<HelpScreen> {
),
),
const SizedBox(width: 10),
Text(
const Text(
"Help?",
style: TextStyle(
fontSize: 16,
......@@ -77,145 +90,160 @@ class _HelpScreenState extends State<HelpScreen> {
],
),
),
// Main content
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Create New Ticket Section
SectionHeading(
title: 'Create New Ticket',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
const SizedBox(height: 12),
_buildCreateNewTicketSection(),
const SizedBox(height: 12),
// Processing Tickets Section
SectionHeading(
title: 'Processing Tickets',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
const SizedBox(height: 2),
_buildProcessingTicketsSection(),
const SizedBox(height: 10),
// ✅ Provider Consumer used here
body: Consumer<HelpAndEnquiryProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(
child: Text(provider.errorMessage!,
style: const TextStyle(color: Colors.red)),
);
}
// Closed Tickets Section
SectionHeading(
title: 'Closed Tickets',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
final ticketData = provider.ticketListResponse?.tickets;
final processingTickets = ticketData?.inProgress ?? [];
final closedTickets = ticketData?.closed ?? [];
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Create New Ticket Section
const SectionHeading(
title: 'Create New Ticket',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
const SizedBox(height: 12),
_buildCreateNewTicketSection(),
const SizedBox(height: 12),
// Processing Tickets Section
const SectionHeading(
title: 'Processing Tickets',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
const SizedBox(height: 2),
_buildProcessingTicketsSection(processingTickets),
const SizedBox(height: 10),
// Closed Tickets Section
const SectionHeading(
title: 'Closed Tickets',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
const SizedBox(height: 2),
_buildClosedTicketsSection(closedTickets),
],
),
const SizedBox(height: 2),
_buildClosedTicketsSection(),
],
),
);
},
),
),
);
}
Widget _buildCreateNewTicketSection() {
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.99,
),
itemCount: createNewTickets.length,
itemBuilder: (context, index) {
final ticket = createNewTickets[index];
final String title = ticket['title'] ?? 'Unknown';
final String description = ticket['description'] ?? '';
final String icon = ticket['icon'] ?? 'assets/svg/help_ic.svg';
final Color color = ticket['color'] ?? Colors.grey;
return _buildFeatureCard(
title: title,
description: description,
icon: icon,
color: color,
);
},
);
}
Widget _buildFeatureCard({
required String title,
required String description,
required String icon,
required Color color,
}) {
return GestureDetector(
return InkResponse(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HelpTicketScreen(reason: title,))
MaterialPageRoute(builder: (context) => OrderHelpScreen(sessionId: widget.sessionId, accId: widget.accId))
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
// Icon container
Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: color.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
SizedBox(
height: 42,
width: 42,
child: SvgPicture.asset(
"assets/svg/help_ic.svg",
height: 30,
width: 30,
fit: BoxFit.contain,
),
child: Center(
child: SizedBox(
height: 40,
width: 40,
child: SvgPicture.asset(
icon,
fit: BoxFit.fitWidth,
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Get help for an order",
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: "Poppins",
fontSize: 14,
color: Colors.black,
),
),
),
SizedBox(height: 4),
Text(
"Select an order",
style: TextStyle(
fontFamily: "Poppins",
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
const SizedBox(height: 8),
// Title
SizedBox(
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.nearDarkText,
fontSize: 14,
fontWeight: FontWeight.w400,
fontFamily: "Plus Jakarta Sans",
),
height: 30,
width: 30,
child: SvgPicture.asset(
"assets/svg/continue_ic.svg",
color: Color(0xFF000000),
height: 18,
width: 18,
),
),
const SizedBox(height: 4),
],
),
),
);
}
Widget _buildProcessingTicketsSection() {
/// ✅ Processing tickets from provider
Widget _buildProcessingTicketsSection(List<dynamic> tickets) {
if (tickets.isEmpty) {
return const Center(child: Text("No processing tickets"));
}
return Container(
padding: const EdgeInsets.all(4),
child: Column(
children: processingTickets.map((ticket) {
children: tickets.map((ticket) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: CommonListItem(
title: ticket['title']!,
date: ticket['date']!,
status: ticket['status']!,
orderId: ticket.ticketNumber ?? '',
title: ticket.type ?? 'Untitled',
date: ticket.date ?? '',
status: 'In Process',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ProcessTicketChatScreen())
context,
MaterialPageRoute(
builder: (context) => TicketChatScreen(
sessionId: widget.sessionId,
accId: widget.accId,
ticketId: ticket.id,
status: "In Process",
),
),
);
},
),
......@@ -225,19 +253,35 @@ class _HelpScreenState extends State<HelpScreen> {
);
}
Widget _buildClosedTicketsSection() {
/// ✅ Closed tickets from provider
Widget _buildClosedTicketsSection(List<dynamic> tickets) {
if (tickets.isEmpty) {
return const Center(child: Text("No closed tickets"));
}
return Container(
padding: const EdgeInsets.all(4),
child: Column(
children: closedTickets.map((ticket) {
children: tickets.map((ticket) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: CommonListItem(
title: ticket['title']!,
date: ticket['date']!,
status: "", // Empty status for closed tickets
orderId: ticket.ticketNumber ?? '',
title: ticket.type ?? 'Untitled',
date: ticket.date ?? '',
status: "",
onTap: () {
// Handle closed ticket tap
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TicketChatScreen(
sessionId: widget.sessionId,
accId: widget.accId,
ticketId: ticket.id,
status: "Closed",
),
),
);
},
),
);
......@@ -247,7 +291,9 @@ class _HelpScreenState extends State<HelpScreen> {
}
}
class CommonListItem extends StatelessWidget {
final String orderId;
final String title;
final String date;
final String status;
......@@ -255,6 +301,7 @@ class CommonListItem extends StatelessWidget {
const CommonListItem({
Key? key,
required this.orderId,
required this.title,
required this.date,
required this.status,
......@@ -265,7 +312,7 @@ class CommonListItem extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 4),
const SizedBox(height: 6),
Material(
color: Colors.transparent,
child: InkWell(
......@@ -276,10 +323,20 @@ class CommonListItem extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 17),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"#${orderId}",
style: const TextStyle(
fontSize: 12,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
color: AppColors.subtitleText,
),
),
SizedBox(width: 10,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -294,6 +351,7 @@ class CommonListItem extends StatelessWidget {
),
),
const SizedBox(height: 4),
if(status.isNotEmpty)
Text(
date,
style: TextStyle(
......@@ -323,6 +381,16 @@ class CommonListItem extends StatelessWidget {
),
),
),
if(status.isEmpty)
Text(
date,
style: TextStyle(
fontSize: 12,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w400,
color: Colors.grey[600],
),
),
],
),
),
......
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:gen_rentals/Screens/HelpScreens/ProcessTicketScreen.dart';
import 'package:gen_rentals/Utility/Reusablewidgets.dart';
import '../../Models/DashboardResponse.dart';
import '../../Notifier/DashboardProvider.dart';
import '../../Notifier/HelpAndEnquiryProvider.dart';
import '../../Utility/AppColors.dart';
import 'package:provider/provider.dart';
import '../ProductsDetailScreen.dart';
import 'CreateTicketScreen.dart';
class OrderHelpScreen extends StatefulWidget {
final String sessionId;
final String accId;
OrderHelpScreen({
super.key,
required this.sessionId,
required this.accId,
});
@override
State<OrderHelpScreen> createState() => _OrderHelpScreenState();
}
class _OrderHelpScreenState extends State<OrderHelpScreen> {
@override
void initState() {
super.initState();
/// ✅ Fetch ticket list on screen load
Future.microtask(() async {
final provider = Provider.of<HelpAndEnquiryProvider>(context, listen: false);
await provider.fetchTicketList(
sessionId: widget.sessionId,
accId: widget.accId,
);
});
}
// ✅ (unchanged)
final List<Map<String, dynamic>> createNewTickets = [
{
'title': 'Payment Issues',
'description': 'Get help with payment related problems',
'icon': "assets/svg/rupee_coin_ic.svg",
'color': Color(0xFFFFEFBE),
},
{
'title': 'Bill Related Issues',
'description': 'Resolve bill and invoice matters',
'icon': "assets/svg/know_pay.svg",
'color': Color(0xFFCEF9FF),
},
{
'title': 'Other Issues',
'description': 'Any other support you need',
'icon': 'assets/svg/help_ic.svg',
'color': Color(0xFFE4E5FF),
},
];
@override
Widget build(BuildContext context) {
final dashboardProvider = Provider.of<DashboardProvider>(context);
final dashboardData = dashboardProvider.dashboardData;
double screenWidth = MediaQuery.of(context).size.width;
double screenHeight = MediaQuery.of(context).size.height;
double bottomPadding = MediaQuery.of(context).padding.bottom;
return SafeArea(
top: false,
child: Scaffold(
backgroundColor: AppColors.backgroundRegular,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.white,
title: Row(
children: [
InkResponse(
onTap: () => Navigator.pop(context, true),
child: SvgPicture.asset(
"assets/svg/continue_left_ic.svg",
height: 25,
),
),
const SizedBox(width: 10),
const Text(
"Help?",
style: TextStyle(
fontSize: 16,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
),
// ✅ Provider Consumer used here
body: Consumer<HelpAndEnquiryProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(
child: Text(provider.errorMessage!,
style: const TextStyle(color: Colors.red)),
);
}
final ticketData = provider.ticketListResponse?.tickets;
final processingTickets = ticketData?.inProgress ?? [];
final closedTickets = ticketData?.closed ?? [];
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Create New Ticket Section
const SectionHeading(
title: 'Select the order you are having issues with',
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Title
// Text(
// "Subscribed Orders",
// style: TextStyle(
// fontFamily: "Poppins",
// color: Colors.grey.shade800,
// fontSize: 18,
// fontWeight: FontWeight.w600,
// ),
// ),
const SizedBox(height: 16),
// Show loading or products list
if (dashboardProvider.isLoading && dashboardData == null)
const Center(
child: CircularProgressIndicator(),
)
else if (dashboardData?.orders == null || dashboardData!.orders!.isEmpty)
const Text(
"No products subscribed",
style: TextStyle(
fontFamily: "Poppins",
color: Colors.grey,
fontSize: 14,
),
)
else
// List of subscribed products from API
Column(
children: dashboardData!.orders!.map((product) {
return Column(
children: [
InkResponse(
onTap: () =>_showReasonBottomSheet(),
child: _buildProductItemFromApi(product),
),
const SizedBox(height: 16),
],
);
}).toList(),
),
],
),
),
],
),
);
},
),
),
);
}
// Helper widget for product item from API data
Widget _buildProductItemFromApi(Orders product) {
final bool hasPending = product.hasPendingPayment == true;
final productList = product.products ?? [];
return Container(
margin: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.15),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
// ===== Red Strip (Behind Card) =====
if (hasPending)
Positioned.fill(
top: null,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 45,
decoration: const BoxDecoration(
color: Color(0xFFFFE2E0),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15),
),
),
child: Row(
children: [
const SizedBox(width: 12),
const Icon(Icons.info_outline, color: Colors.red, size: 18),
const SizedBox(width: 6),
Expanded(
child: Text(
product.pendingPaymentText ??
"Payment Pending. Please Pay before incurring fines.",
style: const TextStyle(
fontFamily: "Poppins",
color: Colors.red,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(width: 12),
],
),
),
),
),
// ===== Main White Card =====
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// Header Row (image, order id, date, badge)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: const Color(0xffF2F2F2),
borderRadius: BorderRadius.circular(16),
),
child: Image.network(
product.productImage ?? "",
height: 40,
width: 40,
fit: BoxFit.contain,
errorBuilder: (context, error, stack) =>
Image.asset('assets/images/gene_png.png',
height: 40, width: 40),
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"#${product.orderid ?? "0"}",
style: const TextStyle(
fontFamily: "Poppins",
color: Color(0xFF008CDE),
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
Text(
product.rentedDate ?? "Rented date not available",
style: TextStyle(
fontFamily: "Poppins",
color: Colors.grey.shade600,
fontSize: 12,
),
),
],
),
],
),
// ✅ Gradient expiry badge
if (product.expiringText != null &&
product.expiringText!.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
gradient: _getGradientByColor(product.expiringInColor),
borderRadius: BorderRadius.circular(8),
),
child: Text(
product.expiringText!,
style: const TextStyle(
color: Colors.black87,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 6),
const Divider(),
/// ===== Product List (with +3 More on same line) =====
Builder(
builder: (context) {
final visibleItems = productList.take(2).toList();
final remaining = productList.length - visibleItems.length;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left side → Product list (bulleted)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (int i = 0; i < visibleItems.length; i++)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 8),
const Text(
"• ",
style: TextStyle(color: Colors.black, fontSize: 16),
),
Expanded(
child: Text(
visibleItems[i],
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w400,
),
),
),
],
),
),
],
),
),
// Right side → +x More (vertically centered)
if (remaining > 0)
Padding(
padding: const EdgeInsets.only(left: 8, right: 4),
child: Align(
alignment: Alignment.center,
child: Text(
"+$remaining More",
style: const TextStyle(
fontFamily: "Poppins",
color: Color(0xFF008CDE),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
],
);
},
),
],
),
),
],
),
);
}
// Gradient helper
LinearGradient _getGradientByColor(String? color) {
switch (color) {
case "Red":
return const LinearGradient(
colors: [Color(0xFFFFE0E0), Color(0xFFFFC0C0)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case "Green":
default:
return const LinearGradient(
colors: [Color(0xFFE9FFDD), Color(0xFFB5FFD1)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
}
void _showReasonBottomSheet() {
// Your existing bottom sheet implementation
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
builder: (context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 14),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Select Your Reason",
style: TextStyle(
fontSize: 18,
fontFamily: "Poppins",
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 24),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.99,
),
itemCount: createNewTickets.length,
itemBuilder: (context, index) {
final ticket = createNewTickets[index];
final String title = ticket['title'] ?? 'Unknown';
final String description = ticket['description'] ?? '';
final String icon = ticket['icon'] ?? 'assets/svg/help_ic.svg';
final Color color = ticket['color'] ?? Colors.grey;
return _buildFeatureCard(
title: title,
description: description,
icon: icon,
color: color,
);
},
),
const SizedBox(height: 24),
],
),
);
},
);
}
Widget _buildFeatureCard({
required String title,
required String description,
required String icon,
required Color color,
}) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HelpTicketScreen(reason: title,))
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Icon container
Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: color.withOpacity(0.7),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: SizedBox(
height: 40,
width: 40,
child: SvgPicture.asset(
icon,
fit: BoxFit.fitWidth,
),
),
),
),
const SizedBox(height: 8),
// Title
SizedBox(
child: Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.nearDarkText,
fontSize: 14,
fontWeight: FontWeight.w400,
fontFamily: "Plus Jakarta Sans",
),
),
),
const SizedBox(height: 4),
],
),
),
);
}
}
......@@ -100,7 +100,40 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
],
),
),
backgroundColor: AppColors.backgroundRegular,
body: _buildBody(provider, screenHeight, bottomPadding),
bottomNavigationBar: Container(
height: 80,
padding: EdgeInsets.symmetric(horizontal: 14, vertical: 10),
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BillDetailListScreen())
);
// Handle view bill action
FocusScope.of(context).unfocus();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.buttonColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
elevation: 0,
),
child: const Text(
"View Bill",
style: TextStyle(
fontSize: 16,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
),
),
),
),
),
);
},
......@@ -153,7 +186,7 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
final order = provider.orderDetails!;
return Container(
color: const Color(0xFFF3F3F3),
color: AppColors.backgroundRegular,
height: screenHeight,
child: SingleChildScrollView(
child: Column(
......@@ -192,19 +225,20 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
const SizedBox(height: 8),
Text(
order.rentedDate ?? 'Date not available',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontFamily: "Poppins",
fontWeight: FontWeight.w400,
color: Colors.grey,
color: AppColors.subtitleText,
),
),
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: _getExpiringColor(order.expiringInColor),
borderRadius: BorderRadius.circular(16),
gradient: _getGradientByColor(order.expiringInColor),
borderRadius: BorderRadius.circular(8),
),
child: Text(
order.expiringText ?? 'Expiring info not available',
......@@ -212,7 +246,7 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
fontSize: 12,
fontFamily: "Poppins",
fontWeight: FontWeight.w500,
color: Colors.white,
color: Colors.black87,
),
),
),
......@@ -264,51 +298,29 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
onTap: () => _showReasonBottomSheet(),
child: Row(
children: [
SvgPicture.asset(
"assets/svg/have_compaints.svg",
height: 30,
width: 30,
),
SizedBox(width: 8,),
Text(
"Need help with this order?",
style: TextStyle(
fontSize: 14,
fontFamily: "Poppins",
fontWeight: FontWeight.w500,
color: Colors.grey,
fontWeight: FontWeight.w400,
color: AppColors.amountText,
),
),
],
),
),
const SizedBox(height: 16),
// View Bill button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => BillDetailListScreen())
);
// Handle view bill action
FocusScope.of(context).unfocus();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.buttonColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
elevation: 0,
),
child: const Text(
"View Bill",
style: TextStyle(
fontSize: 16,
fontFamily: "Plus Jakarta Sans",
fontWeight: FontWeight.w600,
),
),
),
),
],
),
)
......@@ -347,12 +359,12 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
children: [
// Product ID and Name
Text(
product.idName ?? product.id ?? 'N/A',
style: const TextStyle(
"#${product.idName}",
style: TextStyle(
fontSize: 14,
fontFamily: "Poppins",
fontWeight: FontWeight.w500,
color: Colors.grey,
color: AppColors.amountText,
),
),
const SizedBox(height: 4),
......@@ -381,11 +393,11 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
product.dispatchDate != null ?
"Dispatched On ${product.dispatchDate!}" :
"Dispatch date not available",
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontFamily: "Poppins",
fontWeight: FontWeight.w400,
color: Colors.grey,
color: AppColors.subtitleText,
),
),
),
......@@ -445,16 +457,21 @@ class _ProductsDetailScreenState extends State<ProductsDetailScreen> {
);
}
Color _getExpiringColor(String? colorString) {
if (colorString == null || colorString.isEmpty) {
return const Color(0xFFFFEBEB); // Default color
}
try {
// Assuming colorString is in format like "FFFF5757"
return Color(int.parse('FF$colorString', radix: 16));
} catch (e) {
return const Color(0xFFFFEFEF); // Default color on error
LinearGradient _getGradientByColor(String? color) {
switch (color) {
case "Red":
return const LinearGradient(
colors: [Color(0xFFFFE0E0), Color(0xFFFFC0C0)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
case "Green":
default:
return const LinearGradient(
colors: [Color(0xFFE9FFDD), Color(0xFFB5FFD1)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment