Files
2026-05-26 18:01:57 +05:30

596 lines
20 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import 'package:nearledaily/constants/color_constants.dart';
import '../../constants/font_constants.dart';
import '../../widgets/text_widget.dart';
// ─────────────────────────────────────────────
// MODEL
// ─────────────────────────────────────────────
class TenantDetails {
final int tenantid;
final String tenantname;
final String tenanttype; // "D" = delivery-only
final String registrationno;
final String companyname;
final String primaryemail;
final String primarycontact;
final String address;
final String city;
final String state;
final String postcode;
final String latitude;
final String longitude;
final String status; // "Active" / else
const TenantDetails({
required this.tenantid,
required this.tenantname,
required this.tenanttype,
required this.registrationno,
required this.companyname,
required this.primaryemail,
required this.primarycontact,
required this.address,
required this.city,
required this.state,
required this.postcode,
required this.latitude,
required this.longitude,
required this.status,
});
factory TenantDetails.fromJson(Map<String, dynamic> j) => TenantDetails(
tenantid: j['tenantid'] ?? 0,
tenantname: j['tenantname'] ?? '',
tenanttype: j['tenanttype'] ?? '',
registrationno: j['registrationno'] ?? '',
companyname: j['companyname'] ?? '',
primaryemail: j['primaryemail'] ?? '',
primarycontact: j['primarycontact'] ?? '',
address: j['address'] ?? '',
city: j['city'] ?? '',
state: j['state'] ?? '',
postcode: j['postcode'] ?? '',
latitude: j['latitude'] ?? '',
longitude: j['longitude'] ?? '',
status: j['status'] ?? '',
);
bool get isDeliveryOnly => tenanttype == 'D';
bool get isActive => status == 'Active';
}
// ─────────────────────────────────────────────
// API SERVICE
// ─────────────────────────────────────────────
class TenantApiService {
static Future<TenantDetails> fetch(int tenantId) async {
final res = await http.get(Uri.parse(
'https://fiesta.nearle.app/live/api/v1/mob/tenants/gettenantinfo/?tenantid=$tenantId'));
if (res.statusCode == 200) {
final body = jsonDecode(res.body);
if (body['status'] == true) {
return TenantDetails.fromJson(body['details']);
}
throw Exception(body['message']);
}
throw Exception('HTTP ${res.statusCode}');
}
}
// ─────────────────────────────────────────────
// SCREEN
// ─────────────────────────────────────────────
class StoreOverviewScreen extends StatefulWidget {
final int tenantId;
const StoreOverviewScreen({super.key, this.tenantId = 1091});
@override
State<StoreOverviewScreen> createState() => _StoreOverviewScreenState();
}
class _StoreOverviewScreenState extends State<StoreOverviewScreen> {
late Future<TenantDetails> _future;
@override
void initState() {
super.initState();
_future = TenantApiService.fetch(widget.tenantId);
}
// ── Dialer ───────────────────────────────────
Future<void> _launchDialer(String phone) async {
final uri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open dialer')),
);
}
}
// ── Maps ─────────────────────────────────────
Future<void> _openMap(String lat, String lng, String label) async {
final encoded = Uri.encodeComponent(label);
final uri = Uri.parse(
'https://www.google.com/maps/search/?api=1&query=$lat,$lng($encoded)',
);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not open maps')),
);
}
}
// ── Bad-experience bottom sheet ──────────────────
void _showBadExperienceSheet(TenantDetails tenant) {
final reasons = [
'Wrong items delivered',
'Poor food quality',
'Late delivery',
'Rude behaviour',
'Other',
];
String? selected;
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => StatefulBuilder(
builder: (ctx, setSheet) => Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: MediaQuery.of(ctx).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
ReusableTextWidget(
text: 'What went wrong?',
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 17,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 4),
ReusableTextWidget(
text: 'Tell us about your experience at ${tenant.tenantname}',
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w500,
),
const SizedBox(height: 16),
// Reason chips
Wrap(
spacing: 8,
runSpacing: 8,
children: reasons.map((r) {
final picked = selected == r;
return GestureDetector(
onTap: () => setSheet(() => selected = r),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: picked
? const Color(0xFF6A1B9A)
: Colors.grey.shade100,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: picked
? const Color(0xFF6A1B9A)
: Colors.grey.shade300,
),
),
child: ReusableTextWidget(
text: r,
color: picked ? Colors.white : Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}).toList(),
),
const SizedBox(height: 20),
// Hide store option
const SizedBox(height: 14),
// Submit button
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6A1B9A),
disabledBackgroundColor: Colors.grey.shade200,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13),
),
),
onPressed: selected == null
? null
: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Feedback submitted: $selected'),
behavior: SnackBarBehavior.floating,
),
);
},
child: ReusableTextWidget(
text: 'Submit Feedback',
color: selected == null ? Colors.grey : Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
),
);
}
// ─────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFF9F9F9), Color(0xFFF1F1F1)],
),
),
child: SafeArea(
child: FutureBuilder<TenantDetails>(
future: _future,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline,
color: Colors.red, size: 48),
const SizedBox(height: 12),
Text('Failed to load\n${snap.error}',
textAlign: TextAlign.center),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => setState(
() => _future = TenantApiService.fetch(widget.tenantId)),
child: const Text('Retry'),
),
],
),
);
}
final t = snap.data!;
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
children: [
_topBar(),
const SizedBox(height: 16),
_storeCard(t),
const SizedBox(height: 12),
_badExperienceCard(t),
const SizedBox(height: 12),
_legalCard(t),
],
),
),
),
_bottomButton(),
],
);
},
),
),
),
);
}
// ── Top bar ──────────────────────────────────
Widget _topBar() => Row(
children: [
CircleAvatar(
backgroundColor: Colors.white,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
const Spacer(),
],
);
// ── Store card ───────────────────────────────
Widget _storeCard(TenantDetails t) {
return Container(
padding: const EdgeInsets.all(16),
decoration: _card(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// tenantname
ReusableTextWidget(
text: t.tenantname,
color: Colors.black.withOpacity(0.75),
fontFamily: FontConstants.fontFamily,
fontSize: 23,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
const SizedBox(height: 8),
// address
ReusableTextWidget(
text: t.address,
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// Call & Directions
Row(
children: [
GestureDetector(
onTap: () => _launchDialer(t.primarycontact),
child: _circleIcon(Icons.call),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () => _openMap(t.latitude, t.longitude, t.tenantname),
child: _circleIcon(Icons.near_me_outlined),
),
],
),
const Divider(height: 24, thickness: 0.5),
// status → Open / Closed
_infoRow(
icon: Icons.access_time,
title: t.isActive ? 'Open now' : 'Currently Closed',
titleColor: t.isActive ? Colors.green : Colors.red,
),
// tenanttype == "D" → delivery-only row
if (t.isDeliveryOnly) ...[
const Divider(thickness: 0.5),
_infoRow(
icon: Icons.store_mall_directory_outlined,
title: 'This is a delivery-only kitchen',
subtitle:
'There are multiple brands delivering from this kitchen',
),
],
const Divider(thickness: 0.5),
// city + state + postcode
_infoRow(
icon: Icons.location_city_outlined,
title: '${t.city}, ${t.state} ${t.postcode}',
),
],
),
);
}
// ── Bad experience card ──────────────────────
Widget _badExperienceCard(TenantDetails t) => Container(
decoration: _card(),
child: ListTile(
leading: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.red.shade50,
shape: BoxShape.circle,
),
child: Icon(Icons.sentiment_dissatisfied_outlined,
color: Colors.red.shade400, size: 20),
),
title: ReusableTextWidget(
text: 'Had a bad experience here?',
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 13,
fontWeight: FontWeight.w600,
),
subtitle: ReusableTextWidget(
text: 'Report an issue or hide this store',
color: Colors.black54,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w400,
),
trailing: const Icon(Icons.chevron_right, color: Colors.black87),
onTap: () => _showBadExperienceSheet(t),
),
);
// ── Legal card — only real non-empty API fields ──
Widget _legalCard(TenantDetails t) => Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: _card(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_labelText('Legal Name', t.companyname),
const SizedBox(height: 12),
_labelText('GST Number', t.registrationno),
const SizedBox(height: 12),
_labelText('Contact', t.primarycontact),
const SizedBox(height: 12),
_labelText('Email', t.primaryemail),
],
),
);
// ── Bottom button ────────────────────────────
Widget _bottomButton() => Container(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 13),
color: Colors.white,
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6A1B9A),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13)),
),
onPressed: () => Navigator.pop(context),
child: ReusableTextWidget(
text: 'Go back to menu',
color: Colors.white,
fontFamily: FontConstants.fontFamily,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
);
// ── Helpers ──────────────────────────────────
Widget _circleIcon(IconData icon) => Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white,
border: Border.all(color: Colors.grey.shade200),
),
child: Center(
child: Icon(icon, size: 22, color: ColorConstants.primaryColor),
),
);
Widget _infoRow({
required IconData icon,
required String title,
String? subtitle,
Color? titleColor,
}) =>
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.black87),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: title,
color: titleColor ?? Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 12,
fontWeight: FontWeight.w600,
),
if (subtitle != null)
ReusableTextWidget(
text: subtitle,
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
maxLines: 2,
),
],
),
),
const Icon(Icons.chevron_right, color: Colors.black87),
],
);
Widget _labelText(String label, String value) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ReusableTextWidget(
text: label,
color: Colors.grey,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
),
const SizedBox(height: 4),
ReusableTextWidget(
text: value,
color: Colors.black87,
fontFamily: FontConstants.fontFamily,
fontSize: 11,
fontWeight: FontWeight.w600,
),
],
);
BoxDecoration _card() => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
);
}