1625 lines
58 KiB
Dart
1625 lines
58 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:lottie/lottie.dart';
|
|
|
|
import '../../modules/orders/create_order.dart';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Shimmer widget
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class _ShimmerBox extends StatefulWidget {
|
|
final double width;
|
|
final double height;
|
|
final BorderRadius borderRadius;
|
|
|
|
const _ShimmerBox({
|
|
required this.width,
|
|
required this.height,
|
|
this.borderRadius = const BorderRadius.all(Radius.circular(8)),
|
|
});
|
|
|
|
@override
|
|
State<_ShimmerBox> createState() => _ShimmerBoxState();
|
|
}
|
|
|
|
class _ShimmerBoxState extends State<_ShimmerBox>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _ctrl;
|
|
late Animation<double> _anim;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ctrl = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1200),
|
|
)..repeat();
|
|
_anim = Tween<double>(begin: -1.5, end: 2.5).animate(
|
|
CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_ctrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _anim,
|
|
builder: (_, __) {
|
|
return Container(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
decoration: BoxDecoration(
|
|
borderRadius: widget.borderRadius,
|
|
gradient: LinearGradient(
|
|
begin: Alignment(_anim.value - 1, 0),
|
|
end: Alignment(_anim.value, 0),
|
|
colors: const [
|
|
Color(0xFFE8E8E8),
|
|
Color(0xFFF5F5F5),
|
|
Color(0xFFE8E8E8),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// SearchPage
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class SearchPage extends StatefulWidget {
|
|
const SearchPage({super.key});
|
|
|
|
@override
|
|
State<SearchPage> createState() => _SearchPageState();
|
|
}
|
|
|
|
class _SearchPageState extends State<SearchPage> {
|
|
final TextEditingController searchController = TextEditingController();
|
|
|
|
bool isLoading = false;
|
|
Map<String, dynamic>? responseData;
|
|
String? errorMessage;
|
|
String? clarificationQuestion; // ← NEW: holds question when needs_clarification
|
|
|
|
File? selectedImage;
|
|
List<String> clarificationOptions = [];
|
|
|
|
Future<void> searchIngredients() async {
|
|
final query = searchController.text.trim();
|
|
if (query.isEmpty) return;
|
|
|
|
setState(() {
|
|
isLoading = true;
|
|
errorMessage = null;
|
|
responseData = null;
|
|
clarificationQuestion = null; // reset on every new search
|
|
});
|
|
|
|
try {
|
|
final response = await http
|
|
.post(
|
|
Uri.parse('http://127.0.0.1:8000/find-ingredients'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({
|
|
"user_input": query,
|
|
"customer_id": 6060,
|
|
"latitude": 11.0050728,
|
|
"longitude": 76.9508513,
|
|
}),
|
|
)
|
|
.timeout(const Duration(seconds: 15));
|
|
|
|
if (!mounted) return;
|
|
|
|
if (response.statusCode == 200) {
|
|
final decoded = jsonDecode(response.body);
|
|
|
|
if (decoded is Map<String, dynamic>) {
|
|
final status = decoded['status']?.toString();
|
|
|
|
if (status == 'needs_clarification') {
|
|
setState(() {
|
|
clarificationQuestion = decoded['question']?.toString();
|
|
|
|
clarificationOptions =
|
|
(decoded['options'] as List?)
|
|
?.map((e) => e.toString())
|
|
.toList() ??
|
|
[];
|
|
});
|
|
} else {
|
|
setState(() {
|
|
responseData = decoded;
|
|
clarificationOptions = [];
|
|
});
|
|
}
|
|
} else {
|
|
setState(() {
|
|
errorMessage = 'Unexpected response format.';
|
|
});
|
|
}
|
|
} else {
|
|
setState(() {
|
|
errorMessage = 'Server error (${response.statusCode}). Try again.';
|
|
});
|
|
debugPrint("API Error: ${response.body}");
|
|
}
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
errorMessage = 'Could not connect. Check your network and try again.';
|
|
});
|
|
debugPrint("Error: $e");
|
|
} finally {
|
|
if (mounted) setState(() => isLoading = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
searchController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> pickImage() async {
|
|
final XFile? image = await ImagePicker().pickImage(
|
|
source: ImageSource.gallery,
|
|
imageQuality: 80,
|
|
);
|
|
|
|
if (image != null) {
|
|
setState(() {
|
|
selectedImage = File(image.path);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
appBar: AppBar(
|
|
surfaceTintColor: Colors.transparent,
|
|
scrolledUnderElevation: 0,
|
|
elevation: 0,
|
|
backgroundColor: Colors.white,
|
|
leading: Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
icon: const Icon(Icons.arrow_back),
|
|
),
|
|
const Text(
|
|
"Search",
|
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
leadingWidth: 300,
|
|
),
|
|
body: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// ── Search bar ──
|
|
Column(
|
|
children: [
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.06),
|
|
blurRadius: 20,
|
|
spreadRadius: 1,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
border: Border.all(color: Colors.grey.shade300, width: 0.5),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: searchController,
|
|
decoration: const InputDecoration(
|
|
hintText: "Search ingredients...",
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 14,
|
|
),
|
|
),
|
|
onSubmitted: (_) => searchIngredients(),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.mic),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
builder: (context) => Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.camera_alt),
|
|
title: const Text("Camera"),
|
|
onTap: () => Navigator.pop(context),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.photo_library),
|
|
title: const Text("Gallery"),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
pickImage();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
icon: const Icon(Icons.camera_alt_outlined),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 8),
|
|
child: isLoading
|
|
? SizedBox(
|
|
width: 50,
|
|
height: 50,
|
|
child: Lottie.asset('assets/lotties/ai_loader.json'),
|
|
)
|
|
: ElevatedButton(
|
|
onPressed: searchIngredients,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF662582),
|
|
elevation: 0,
|
|
shape: const CircleBorder(),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 18,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
child: const Icon(Icons.search, color: Colors.white),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (clarificationQuestion != null) ...[
|
|
const SizedBox(height: 12),
|
|
|
|
Text(
|
|
clarificationQuestion!,
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
],
|
|
if (clarificationOptions.isNotEmpty)
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: clarificationOptions.map((option) {
|
|
return ActionChip(
|
|
label: Text(option),
|
|
onPressed: () {
|
|
searchController.text = option;
|
|
// Call API again with selected option
|
|
searchIngredients();
|
|
},
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Results area ──
|
|
Expanded(
|
|
child: isLoading
|
|
? _buildShimmer()
|
|
: errorMessage != null
|
|
? _buildError()
|
|
: clarificationQuestion != null // ← NEW: check clarification first
|
|
? _buildClarification() // ← NEW: show agent question
|
|
: responseData == null
|
|
? const Center(child: Text("Search for ingredients"))
|
|
: _buildResults(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Shimmer skeleton ──
|
|
Widget _buildShimmer() {
|
|
return ListView(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
children: [
|
|
const _ShimmerBox(width: 160, height: 22),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 150,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: 4,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
itemBuilder: (_, __) => const _ShimmerBox(
|
|
width: 130,
|
|
height: 150,
|
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
const _ShimmerBox(width: 140, height: 22),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 176,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: 3,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
itemBuilder: (_, __) => const _ShimmerBox(
|
|
width: 250,
|
|
height: 176,
|
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ── Error state ──
|
|
Widget _buildError() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.wifi_off_rounded, size: 48, color: Colors.grey),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
errorMessage!,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: searchIngredients,
|
|
child: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── NEW: Clarification card (only shown when status == needs_clarification) ──
|
|
Widget _buildClarification() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF3E5F5),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(
|
|
color: const Color(0xFF662582).withOpacity(0.25),
|
|
width: 1,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF662582).withOpacity(0.06),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// AI avatar icon
|
|
Container(
|
|
width: 52,
|
|
height: 52,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF662582).withOpacity(0.12),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.auto_awesome_rounded,
|
|
size: 26,
|
|
color: Color(0xFF662582),
|
|
),
|
|
),
|
|
const SizedBox(height: 14),
|
|
|
|
// "AI Assistant" label
|
|
const Text(
|
|
'AI Assistant',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF662582),
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
// The actual question from the agent
|
|
Text(
|
|
clarificationQuestion ?? 'Can you clarify your request?',
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF1A1A1A),
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Hint
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.6),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: const Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.keyboard_rounded,
|
|
size: 13, color: Color(0xFF757575)),
|
|
SizedBox(width: 5),
|
|
Text(
|
|
'Type your answer above and search again',
|
|
style: TextStyle(
|
|
fontSize: 11, color: Color(0xFF757575)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Normal results ──
|
|
Widget _buildResults() {
|
|
final ingredients = (responseData!['ingredients_needed'] as List?) ?? [];
|
|
|
|
return ListView(
|
|
children: [
|
|
const Text(
|
|
"Recommended",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1A1A1A),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
if (ingredients.isEmpty)
|
|
const Text('No ingredients found.',
|
|
style: TextStyle(color: Colors.grey))
|
|
else
|
|
SizedBox(
|
|
height: 150,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: ingredients.length,
|
|
itemBuilder: (context, index) {
|
|
final item =
|
|
ingredients[index] as Map<String, dynamic>? ?? {};
|
|
|
|
return Container(
|
|
width: 130,
|
|
margin: const EdgeInsets.only(right: 8),
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border:
|
|
Border.all(color: Colors.grey.shade300, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
(item['emoji'] as String?)?.isNotEmpty == true
|
|
? item['emoji'] as String
|
|
: '🥬',
|
|
style: const TextStyle(fontSize: 35),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
item['name']?.toString() ?? '',
|
|
textAlign: TextAlign.center,
|
|
maxLines: 2,
|
|
style: const TextStyle(
|
|
fontSize: 12, fontWeight: FontWeight.bold),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
item['quantity']?.toString() ?? '',
|
|
textAlign: TextAlign.center,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: Colors.grey,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
|
|
CompareStoresSection(
|
|
responseData: responseData,
|
|
onStoreSelected: (storeData) {
|
|
debugPrint(
|
|
'Selected: ${(storeData['store'] as Map?)?['name']}');
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 24),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// StoreCompareCard
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class StoreCompareCard extends StatelessWidget {
|
|
final String storeName;
|
|
final String location;
|
|
final double rating;
|
|
final int reviewCount;
|
|
final String discountLabel;
|
|
final double originalPrice;
|
|
final double discountedPrice;
|
|
final double savings;
|
|
final int matchCount;
|
|
final int totalIngredients;
|
|
final String? imageUrl;
|
|
final bool isBestValue;
|
|
final bool isSelected;
|
|
final VoidCallback? onSelectStore;
|
|
|
|
const StoreCompareCard({
|
|
super.key,
|
|
required this.storeName,
|
|
required this.location,
|
|
required this.rating,
|
|
required this.reviewCount,
|
|
required this.discountLabel,
|
|
required this.originalPrice,
|
|
required this.discountedPrice,
|
|
required this.savings,
|
|
required this.matchCount,
|
|
required this.totalIngredients,
|
|
this.imageUrl,
|
|
this.isBestValue = false,
|
|
this.isSelected = false,
|
|
this.onSelectStore,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: 250,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(
|
|
color:
|
|
isSelected ? const Color(0xFF2E7D32) : Colors.grey.shade300,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: imageUrl != null && imageUrl!.isNotEmpty
|
|
? Image.network(
|
|
imageUrl!,
|
|
width: 40,
|
|
height: 45,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) =>
|
|
_placeholderIcon(),
|
|
)
|
|
: _placeholderIcon(),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
storeName,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF1A1A1A),
|
|
height: 1.2,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (rating > 0)
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.star_rounded,
|
|
size: 11, color: Color(0xFFFFA000)),
|
|
const SizedBox(width: 2),
|
|
Flexible(
|
|
child: Text(
|
|
'$rating (${_formatCount(reviewCount)})',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: Color(0xFF616161)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.location_on_outlined,
|
|
size: 11, color: Color(0xFF616161)),
|
|
const SizedBox(width: 2),
|
|
Flexible(
|
|
child: Text(
|
|
location,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 8,
|
|
color: Color(0xFF616161)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (discountLabel.isNotEmpty)
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 4),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
discountLabel,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 7,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF2E7D32),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Flexible(
|
|
child: Text(
|
|
'₹${_fmt(originalPrice)}',
|
|
maxLines: 1,
|
|
overflow:
|
|
TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
decoration: TextDecoration
|
|
.lineThrough,
|
|
decorationColor: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Flexible(
|
|
child: Text(
|
|
'₹${_fmt(discountedPrice)}',
|
|
maxLines: 1,
|
|
overflow:
|
|
TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF2E7D32),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'You save ₹${_fmt(savings)}',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF2E7D32)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'$matchCount / $totalIngredients',
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF1A1A1A),
|
|
),
|
|
),
|
|
const Text('items',
|
|
style: TextStyle(
|
|
fontSize: 7,
|
|
color: Color(0xFF757575))),
|
|
const Text('available',
|
|
style: TextStyle(
|
|
fontSize: 7,
|
|
color: Color(0xFF757575))),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 7),
|
|
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: InkWell(
|
|
onTap: onSelectStore,
|
|
borderRadius: BorderRadius.circular(2),
|
|
highlightColor: Colors.transparent,
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? const Color(0xFF2E7D32)
|
|
: Colors.white,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? const Color(0xFF2E7D32)
|
|
: Colors.grey.shade300,
|
|
width: 1,
|
|
),
|
|
),
|
|
child: AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 250),
|
|
transitionBuilder: (child, animation) {
|
|
return FadeTransition(
|
|
opacity: animation,
|
|
child: ScaleTransition(
|
|
scale: animation,
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
child: Text(
|
|
isSelected ? 'Selected ✓' : 'Select Store',
|
|
key: ValueKey(isSelected),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: isSelected
|
|
? Colors.white
|
|
: const Color(0xFF2E7D32),
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
if (isBestValue)
|
|
Positioned(
|
|
top: 0,
|
|
right: 0,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 5),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF2E7D32),
|
|
borderRadius: BorderRadius.only(
|
|
topRight: Radius.circular(14),
|
|
bottomLeft: Radius.circular(10),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Best Value',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _placeholderIcon() => Container(
|
|
width: 45,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(Icons.store_rounded,
|
|
size: 20, color: Colors.grey.shade400),
|
|
);
|
|
|
|
String _fmt(double v) => v == v.truncateToDouble()
|
|
? v.toInt().toString()
|
|
: v.toStringAsFixed(2);
|
|
|
|
String _formatCount(int count) =>
|
|
count >= 1000 ? '${(count / 1000).toStringAsFixed(1)}k' : '$count';
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// CompareStoresSection
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class CompareStoresSection extends StatefulWidget {
|
|
final Map<String, dynamic>? responseData;
|
|
final void Function(Map<String, dynamic> storeData)? onStoreSelected;
|
|
|
|
const CompareStoresSection({
|
|
super.key,
|
|
required this.responseData,
|
|
this.onStoreSelected,
|
|
});
|
|
|
|
@override
|
|
State<CompareStoresSection> createState() => _CompareStoresSectionState();
|
|
}
|
|
|
|
class _CompareStoresSectionState extends State<CompareStoresSection> {
|
|
int _selectedIndex = 0;
|
|
|
|
List<dynamic> get _stores =>
|
|
(widget.responseData?['stores'] as List?) ?? [];
|
|
|
|
String _discountLabel(
|
|
Map<String, dynamic> store, Map<String, dynamic> storeData) {
|
|
final label = store['discount_label'] ?? storeData['discount_label'];
|
|
if (label != null && label.toString().isNotEmpty) {
|
|
return label.toString();
|
|
}
|
|
final orig = _toDouble(
|
|
store['total_original_price'] ?? storeData['total_original_price']);
|
|
final disc = _toDouble(store['total_discounted_price'] ??
|
|
storeData['total_discounted_price']);
|
|
if (orig > 0 && disc < orig) {
|
|
final pct = ((orig - disc) / orig * 100).toStringAsFixed(1);
|
|
return '$pct% OFF';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
double _toDouble(dynamic v) =>
|
|
v == null ? 0.0 : double.tryParse(v.toString()) ?? 0.0;
|
|
|
|
int _toInt(dynamic v) =>
|
|
v == null ? 0 : int.tryParse(v.toString()) ?? 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_stores.isEmpty) return const SizedBox.shrink();
|
|
|
|
final selectedStore = _stores[_selectedIndex];
|
|
final matchedProducts =
|
|
(selectedStore['matched_products'] as List?) ?? [];
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
"Compare stores",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1A1A1A)),
|
|
),
|
|
Text(
|
|
'${_stores.length} stores found',
|
|
style:
|
|
const TextStyle(fontSize: 13, color: Color(0xFF757575)),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
SizedBox(
|
|
height: 163,
|
|
child: ListView.separated(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: _stores.length,
|
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
itemBuilder: (context, index) {
|
|
final raw = _stores[index];
|
|
if (raw is! Map<String, dynamic>) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final storeData = raw;
|
|
final store =
|
|
(storeData['store'] as Map<String, dynamic>?) ?? {};
|
|
|
|
final isBestValue = storeData['is_best_value'] == true ||
|
|
(storeData['is_best_value'] == null && index == 0);
|
|
|
|
return StoreCompareCard(
|
|
storeName:
|
|
store['tenantname']?.toString().isNotEmpty == true
|
|
? store['tenantname'].toString()
|
|
: 'Unknown Store',
|
|
location: store['locationname']?.toString() ?? '',
|
|
rating: _toDouble(store['rating']),
|
|
reviewCount: _toInt(store['review_count']),
|
|
discountLabel: _discountLabel(store, storeData),
|
|
originalPrice: _toDouble(store['total_original_price'] ??
|
|
storeData['total_original_price']),
|
|
discountedPrice: _toDouble(
|
|
store['total_discounted_price'] ??
|
|
storeData['total_discounted_price']),
|
|
savings: _toDouble(store['total_savings'] ??
|
|
storeData['total_savings']),
|
|
matchCount: _toInt(storeData['match_count']),
|
|
totalIngredients: _toInt(storeData['total_ingredients']),
|
|
imageUrl: store['image_url']?.toString(),
|
|
isBestValue: isBestValue,
|
|
isSelected: _selectedIndex == index,
|
|
onSelectStore: () {
|
|
setState(() => _selectedIndex = index);
|
|
widget.onStoreSelected?.call(storeData);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
if (matchedProducts.isNotEmpty) ...[
|
|
const SizedBox(height: 24),
|
|
_SelectedStoreProducts(
|
|
key: ValueKey(_selectedIndex),
|
|
matchedProducts: matchedProducts,
|
|
),
|
|
],
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// _SelectedStoreProducts
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
class _SelectedStoreProducts extends StatefulWidget {
|
|
final List<dynamic> matchedProducts;
|
|
|
|
const _SelectedStoreProducts({
|
|
super.key,
|
|
required this.matchedProducts,
|
|
});
|
|
|
|
@override
|
|
State<_SelectedStoreProducts> createState() =>
|
|
_SelectedStoreProductsState();
|
|
}
|
|
|
|
class _SelectedStoreProductsState extends State<_SelectedStoreProducts> {
|
|
final Map<int, int> _cartQuantities = {};
|
|
bool _isPlacingOrder = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initCart();
|
|
}
|
|
|
|
void _initCart() {
|
|
for (final item in widget.matchedProducts) {
|
|
if (item is! Map<String, dynamic>) continue;
|
|
final product = (item['product'] as Map<String, dynamic>?) ?? {};
|
|
final productId = product['productid'] as int?;
|
|
if (productId != null) {
|
|
_cartQuantities[productId] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Products",
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1A1A1A)),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: widget.matchedProducts.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
itemBuilder: (context, index) {
|
|
final item = widget.matchedProducts[index];
|
|
if (item is! Map<String, dynamic>) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final product =
|
|
(item['product'] as Map<String, dynamic>?) ?? {};
|
|
final productId = product['productid'] as int? ?? index;
|
|
final qty = _cartQuantities[productId] ?? 0;
|
|
|
|
final imageUrl = product['productimage']?.toString() ?? '';
|
|
final name = product['productname']?.toString() ??
|
|
item['ingredient']?.toString() ??
|
|
'';
|
|
final unit = product['productunit']?.toString() ?? '';
|
|
final originalPrice = double.tryParse(
|
|
product['original_price']?.toString() ?? '') ??
|
|
0.0;
|
|
final discountedPrice = double.tryParse(
|
|
product['discounted_price']?.toString() ?? '') ??
|
|
0.0;
|
|
final discountPct = double.tryParse(
|
|
product['discount_percent']?.toString() ?? '') ??
|
|
0.0;
|
|
final hasOffer = product['has_offer'] == true;
|
|
final emoji = item['emoji']?.toString() ?? '';
|
|
final quantity = item['quantity']?.toString() ?? '';
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border:
|
|
Border.all(color: Colors.grey.shade200, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child: imageUrl.isNotEmpty
|
|
? Image.network(
|
|
imageUrl,
|
|
width: 64,
|
|
height: 64,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (_, __, ___) =>
|
|
_productPlaceholder(emoji),
|
|
)
|
|
: _productPlaceholder(emoji),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF1A1A1A),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'$quantity · $unit',
|
|
style: const TextStyle(
|
|
fontSize: 11, color: Color(0xFF757575)),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
if (hasOffer &&
|
|
originalPrice > discountedPrice) ...[
|
|
Text(
|
|
'₹${_fmt(originalPrice)}',
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.grey,
|
|
decoration: TextDecoration.lineThrough,
|
|
decorationColor: Colors.grey,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
],
|
|
Text(
|
|
'₹${_fmt(discountedPrice)}',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF2E7D32),
|
|
),
|
|
),
|
|
if (hasOffer && discountPct > 0) ...[
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
'${discountPct.toStringAsFixed(1)}% OFF',
|
|
style: const TextStyle(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w700,
|
|
color: Color(0xFF2E7D32),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
qty == 0
|
|
? OutlinedButton(
|
|
onPressed: () => setState(
|
|
() => _cartQuantities[productId] = 1),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF662582),
|
|
side: const BorderSide(
|
|
color: Color(0xFF662582), width: 1.5),
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 14, vertical: 10),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Add',
|
|
style: TextStyle(
|
|
fontSize: 13, fontWeight: FontWeight.w700),
|
|
),
|
|
)
|
|
: Container(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF662582),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_stepperBtn(
|
|
icon: Icons.remove,
|
|
onTap: () => setState(() {
|
|
if (qty <= 1) {
|
|
_cartQuantities.remove(productId);
|
|
} else {
|
|
_cartQuantities[productId] = qty - 1;
|
|
}
|
|
}),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10),
|
|
child: Text(
|
|
'$qty',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
_stepperBtn(
|
|
icon: Icons.add,
|
|
onTap: () => setState(() =>
|
|
_cartQuantities[productId] = qty + 1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
_buildOrderSummary(),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: (_cartQuantities.isEmpty || _isPlacingOrder)
|
|
? null
|
|
: _placeOrder,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF662582),
|
|
disabledBackgroundColor: Colors.grey.shade300,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
child: _isPlacingOrder
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text(
|
|
'Place Order',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildOrderSummary() {
|
|
double total = 0;
|
|
int itemCount = 0;
|
|
|
|
for (final item in widget.matchedProducts) {
|
|
if (item is! Map<String, dynamic>) continue;
|
|
final product = (item['product'] as Map<String, dynamic>?) ?? {};
|
|
final productId = product['productid'] as int?;
|
|
final qty =
|
|
productId != null ? (_cartQuantities[productId] ?? 0) : 0;
|
|
if (qty > 0) {
|
|
final price = double.tryParse(
|
|
product['discounted_price']?.toString() ?? '') ??
|
|
0.0;
|
|
total += price * qty;
|
|
itemCount += qty;
|
|
}
|
|
}
|
|
|
|
if (itemCount == 0) return const SizedBox.shrink();
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'$itemCount item${itemCount > 1 ? 's' : ''} in cart',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF2E7D32)),
|
|
),
|
|
Text(
|
|
'₹${_fmt(total)}',
|
|
style: const TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w800,
|
|
color: Color(0xFF2E7D32)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _placeOrder() async {
|
|
if (_cartQuantities.isEmpty || _isPlacingOrder) return;
|
|
|
|
setState(() => _isPlacingOrder = true);
|
|
|
|
try {
|
|
final firstItem =
|
|
widget.matchedProducts.first as Map<String, dynamic>;
|
|
final firstProduct =
|
|
(firstItem['product'] as Map<String, dynamic>?) ?? {};
|
|
|
|
final tenantid = firstProduct['tenantid'] as int? ?? 0;
|
|
final categoryid = firstProduct['categoryid'] as int? ?? 0;
|
|
final subcategoryid = firstProduct['subcategoryid'] as int? ?? 0;
|
|
|
|
final items = <OrderItem>[];
|
|
double total = 0;
|
|
|
|
for (final item in widget.matchedProducts) {
|
|
if (item is! Map<String, dynamic>) continue;
|
|
final product = (item['product'] as Map<String, dynamic>?) ?? {};
|
|
final productId = product['productid'] as int?;
|
|
if (productId == null) continue;
|
|
|
|
final qty = _cartQuantities[productId] ?? 0;
|
|
if (qty <= 0) continue;
|
|
|
|
final cost = (product['productcost'] as num?)?.toDouble() ?? 0;
|
|
final discountedPrice = double.tryParse(
|
|
product['discounted_price']?.toString() ?? '') ??
|
|
0.0;
|
|
final tax = (product['taxamount'] as num?)?.toDouble() ?? 0;
|
|
final discount =
|
|
double.tryParse(product['you_save']?.toString() ?? '') ?? 0.0;
|
|
|
|
final lineTotal = discountedPrice * qty;
|
|
total += lineTotal;
|
|
|
|
items.add(
|
|
OrderItem(
|
|
productid: productId,
|
|
productname: product['productname']?.toString() ?? '',
|
|
productdescription:
|
|
product['productdesc']?.toString() ?? '',
|
|
orderqty: qty,
|
|
price: cost,
|
|
unitid: int.tryParse(
|
|
product['unitvalue']?.toString() ?? '') ??
|
|
1,
|
|
unitname: product['productunit']?.toString() ?? 'unit',
|
|
productsumprice: lineTotal,
|
|
tax: tax,
|
|
discount: discount,
|
|
tenantfee: 0,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (items.isEmpty) return;
|
|
|
|
final now = DateTime.now();
|
|
final orderDateStr = _fmtNow(now);
|
|
final deliveryDateStr =
|
|
'${_fmtDate(now)} ${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:00';
|
|
|
|
final order = CreateOrder(
|
|
applocationid: 1,
|
|
applocation: '',
|
|
tenantid: tenantid,
|
|
partnerid: 60,
|
|
locationid: 1135,
|
|
categoryid: categoryid,
|
|
subcategoryid: subcategoryid,
|
|
moduleid: 2,
|
|
configid: 1,
|
|
orderdate: orderDateStr,
|
|
deliverydate: deliveryDateStr,
|
|
orderstatus: 'created',
|
|
deliverycharge: 0,
|
|
customerid: 6060,
|
|
pickupcustomer: 'Ragul Stores',
|
|
pickupcontactno: '7402223869',
|
|
pickupaddress:
|
|
'412-419, R.S. Puram,Coimbatore,Tamil Nadu ,India,641002.',
|
|
pickuplocationid: 1135,
|
|
pickupcity: 'Coimbatore',
|
|
deliverycustomer: 'Test User',
|
|
deliverycontactno: '9999999999',
|
|
deliveryaddress: 'Test delivery address',
|
|
deliverylocationid: 0,
|
|
deliverylat: '11.0050728',
|
|
deliverylong: '76.9508513',
|
|
paymenttype: 42,
|
|
items: items,
|
|
);
|
|
|
|
final response = await http
|
|
.post(
|
|
Uri.parse(
|
|
'https://queue.workolik.com/live/api/v1/mob/orders/createorder'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode(order.toJson()),
|
|
)
|
|
.timeout(const Duration(seconds: 15));
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
debugPrint('Order placed successfully: ${response.body}');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Order placed successfully!')),
|
|
);
|
|
}
|
|
} else {
|
|
debugPrint(
|
|
'Order failed: ${response.statusCode} ${response.body}');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content:
|
|
Text('Order failed (${response.statusCode}).')),
|
|
);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Order error: $e');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Could not place order. Try again.')),
|
|
);
|
|
}
|
|
} finally {
|
|
if (mounted) setState(() => _isPlacingOrder = false);
|
|
}
|
|
}
|
|
|
|
String _fmtNow(DateTime dt) =>
|
|
'${_fmtDate(dt)} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
|
|
|
|
String _fmtDate(DateTime dt) =>
|
|
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
|
|
|
|
Widget _stepperBtn(
|
|
{required IconData icon, required VoidCallback onTap}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
|
child: Icon(icon, color: Colors.white, size: 16),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _productPlaceholder(String emoji) => Container(
|
|
width: 64,
|
|
height: 64,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
emoji.isNotEmpty ? emoji : '🛒',
|
|
style: const TextStyle(fontSize: 28),
|
|
),
|
|
),
|
|
);
|
|
|
|
String _fmt(double v) => v == v.truncateToDouble()
|
|
? v.toInt().toString()
|
|
: v.toStringAsFixed(2);
|
|
} |