460 lines
14 KiB
Dart
460 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
|
import 'package:real_estate_mobile/core/network/api_client.dart';
|
|
import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart';
|
|
|
|
// ── Data models ──
|
|
|
|
class FaqItem {
|
|
final int id;
|
|
final String icon;
|
|
final String question;
|
|
final String answer;
|
|
final String category;
|
|
|
|
const FaqItem({
|
|
required this.id,
|
|
required this.icon,
|
|
required this.question,
|
|
required this.answer,
|
|
required this.category,
|
|
});
|
|
|
|
factory FaqItem.fromJson(Map<String, dynamic> json) {
|
|
return FaqItem(
|
|
id: json['id'] as int? ?? 0,
|
|
icon: json['icon'] as String? ?? '',
|
|
question: json['question'] as String? ?? '',
|
|
answer: json['answer'] as String? ?? '',
|
|
category: json['category'] as String? ?? '',
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Default data (same as web) ──
|
|
|
|
const _defaultFaqs = <FaqItem>[
|
|
FaqItem(
|
|
id: 1,
|
|
icon: 'assets/icons/home_faq_icon.svg',
|
|
question: 'How do I update my listing status?',
|
|
answer:
|
|
'To update your listing status, log in to your Agent Dashboard. Locate the property in your "Active Listings" tab, click the three-dot menu on the right, and select "Update Status." You can change it to Pending, Sold, or Temporarily Off Market. Changes are reflected immediately.',
|
|
category: 'agent',
|
|
),
|
|
FaqItem(
|
|
id: 2,
|
|
icon: 'assets/icons/home_faq_icon.svg',
|
|
question: 'What is the pre-approval process?',
|
|
answer:
|
|
'The pre-approval process involves submitting your financial documents to a lender who will review your credit history, income, assets, and debts. Once approved, you\'ll receive a pre-approval letter stating how much you can borrow, which strengthens your position when making offers on properties.',
|
|
category: 'buying',
|
|
),
|
|
FaqItem(
|
|
id: 3,
|
|
icon: 'assets/icons/home_faq_icon.svg',
|
|
question: 'How do I schedule an open house?',
|
|
answer:
|
|
'To schedule an open house, navigate to your Agent Dashboard and select the property you want to showcase. Click on "Schedule Open House" and choose your preferred date and time.',
|
|
category: 'agent',
|
|
),
|
|
FaqItem(
|
|
id: 4,
|
|
icon: 'assets/icons/home_faq_icon.svg',
|
|
question: 'How do I reset my password?',
|
|
answer:
|
|
'To reset your password, click on the "Login" button and then select "Forgot Password." Enter your registered email address and we\'ll send you a password reset link. Follow the link to create a new password.',
|
|
category: 'platform',
|
|
),
|
|
FaqItem(
|
|
id: 5,
|
|
icon: 'assets/icons/home_faq_icon.svg',
|
|
question: 'How do I contact a listing agent directly?',
|
|
answer:
|
|
'You can contact a listing agent by visiting the property page and clicking the "Contact Agent" button. You\'ll be able to send a message directly to the agent through our secure messaging system.',
|
|
category: 'buying',
|
|
),
|
|
FaqItem(
|
|
id: 6,
|
|
icon: 'assets/icons/home_faq_icon.svg',
|
|
question: 'What if I find inaccurate data on a listing?',
|
|
answer:
|
|
'If you find inaccurate information on a listing, please use the "Report Issue" button on the property page. Describe the inaccuracy and our team will review and correct it within 24-48 hours.',
|
|
category: 'platform',
|
|
),
|
|
];
|
|
|
|
// ── Provider ──
|
|
|
|
final faqProvider =
|
|
StateNotifierProvider.autoDispose<FaqNotifier, FaqState>((ref) {
|
|
return FaqNotifier();
|
|
});
|
|
|
|
class FaqState {
|
|
final List<FaqItem> faqs;
|
|
final bool isLoading;
|
|
final int? expandedId;
|
|
|
|
const FaqState({
|
|
this.faqs = const [],
|
|
this.isLoading = true,
|
|
this.expandedId,
|
|
});
|
|
|
|
FaqState copyWith({
|
|
List<FaqItem>? faqs,
|
|
bool? isLoading,
|
|
int? expandedId,
|
|
bool clearExpanded = false,
|
|
}) {
|
|
return FaqState(
|
|
faqs: faqs ?? this.faqs,
|
|
isLoading: isLoading ?? this.isLoading,
|
|
expandedId: clearExpanded ? null : (expandedId ?? this.expandedId),
|
|
);
|
|
}
|
|
}
|
|
|
|
class FaqNotifier extends StateNotifier<FaqState> {
|
|
FaqNotifier() : super(const FaqState()) {
|
|
_load();
|
|
}
|
|
|
|
Future<void> _load() async {
|
|
try {
|
|
final response = await ApiClient.instance.dio.get('/cms/page/faq');
|
|
final data = response.data['data'] as List<dynamic>?;
|
|
if (data != null) {
|
|
for (final record in data) {
|
|
final map = record as Map<String, dynamic>;
|
|
if (map['sectionKey'] == 'faqContent') {
|
|
final content = map['content'] as Map<String, dynamic>?;
|
|
if (content != null) {
|
|
final faqsList = content['faqs'] as List<dynamic>?;
|
|
if (faqsList != null && faqsList.isNotEmpty) {
|
|
final faqs = faqsList
|
|
.map((e) => FaqItem.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
state = state.copyWith(
|
|
faqs: faqs,
|
|
isLoading: false,
|
|
expandedId: faqs.first.id,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (_) {
|
|
// Fall through to defaults
|
|
}
|
|
|
|
// Use defaults
|
|
state = state.copyWith(
|
|
faqs: _defaultFaqs,
|
|
isLoading: false,
|
|
expandedId: _defaultFaqs.first.id,
|
|
);
|
|
}
|
|
|
|
void toggleFaq(int id) {
|
|
if (state.expandedId == id) {
|
|
state = state.copyWith(clearExpanded: true);
|
|
} else {
|
|
state = state.copyWith(expandedId: id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Screen ──
|
|
|
|
class FaqScreen extends ConsumerWidget {
|
|
const FaqScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final state = ref.watch(faqProvider);
|
|
|
|
if (state.isLoading) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(color: AppColors.accentOrange),
|
|
);
|
|
}
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(0, 20, 0, 30),
|
|
children: [
|
|
// Title
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 24),
|
|
child: Text(
|
|
'Frequently Asked Questions ?',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// FAQ items
|
|
...state.faqs.map((faq) => _FaqAccordion(
|
|
faq: faq,
|
|
isExpanded: state.expandedId == faq.id,
|
|
onTap: () =>
|
|
ref.read(faqProvider.notifier).toggleFaq(faq.id),
|
|
)),
|
|
|
|
const SizedBox(height: 30),
|
|
|
|
// Still Need Help section
|
|
const _StillNeedHelpSection(),
|
|
],
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
// ── Accordion item ──
|
|
|
|
class _FaqAccordion extends StatelessWidget {
|
|
final FaqItem faq;
|
|
final bool isExpanded;
|
|
final VoidCallback onTap;
|
|
|
|
const _FaqAccordion({
|
|
required this.faq,
|
|
required this.isExpanded,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
width: 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Question row
|
|
GestureDetector(
|
|
onTap: onTap,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
|
child: Row(
|
|
children: [
|
|
// Icon
|
|
SvgPicture.asset(
|
|
'assets/icons/home_faq_icon.svg',
|
|
width: 22,
|
|
height: 22,
|
|
placeholderBuilder: (_) => const Icon(
|
|
Icons.home_outlined,
|
|
color: AppColors.accentOrange,
|
|
size: 22,
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
|
|
// Question text
|
|
Expanded(
|
|
child: Text(
|
|
faq.question,
|
|
style: TextStyle(
|
|
fontFamily: isExpanded ? 'Fractul' : 'SourceSerif4',
|
|
fontSize: 16,
|
|
fontWeight:
|
|
isExpanded ? FontWeight.w500 : FontWeight.w600,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
|
|
// Chevron
|
|
Icon(
|
|
isExpanded
|
|
? Icons.keyboard_arrow_down
|
|
: Icons.keyboard_arrow_right,
|
|
color: AppColors.primaryDark,
|
|
size: 20,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// Answer (expanded)
|
|
if (isExpanded) ...[
|
|
// Divider
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Divider(
|
|
height: 1,
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(56, 14, 20, 18),
|
|
child: Text(
|
|
faq.answer,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Still Need Help section ──
|
|
|
|
class _StillNeedHelpSection extends ConsumerWidget {
|
|
const _StillNeedHelpSection();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 0),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
width: 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
padding: const EdgeInsets.fromLTRB(24, 24, 20, 24),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Left side: text
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Still Need Help ?',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'Our Support Team Is Available\nMon-Fri, 9am-6pm IST',
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Right side: buttons
|
|
Column(
|
|
children: [
|
|
// Start Live Chat
|
|
_buildSupportButton(
|
|
icon: Icons.chat_bubble_outline,
|
|
label: 'Start Live Chat',
|
|
onTap: () {
|
|
final authState = ref.read(authProvider);
|
|
if (authState.status != AuthStatus.authenticated) {
|
|
context.push('/login');
|
|
return;
|
|
}
|
|
context.push('/support/chat');
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Email Support
|
|
_buildSupportButton(
|
|
icon: Icons.email_outlined,
|
|
label: 'Email Support',
|
|
onTap: () async {
|
|
final uri = Uri(
|
|
scheme: 'mailto',
|
|
path: 'support@requesn.com',
|
|
queryParameters: {
|
|
'subject': 'Support Request',
|
|
},
|
|
);
|
|
if (await canLaunchUrl(uri)) {
|
|
await launchUrl(uri);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSupportButton({
|
|
required IconData icon,
|
|
required String label,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 162,
|
|
height: 38,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(7),
|
|
border: Border.all(
|
|
color: AppColors.primaryDark.withValues(alpha: 0.15),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 18, color: AppColors.primaryDark),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|