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 _anim; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), )..repeat(); _anim = Tween(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 createState() => _SearchPageState(); } class _SearchPageState extends State { final TextEditingController searchController = TextEditingController(); bool isLoading = false; Map? responseData; String? errorMessage; String? clarificationQuestion; // ← NEW: holds question when needs_clarification File? selectedImage; List clarificationOptions = []; Future 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) { 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 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? ?? {}; 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? responseData; final void Function(Map storeData)? onStoreSelected; const CompareStoresSection({ super.key, required this.responseData, this.onStoreSelected, }); @override State createState() => _CompareStoresSectionState(); } class _CompareStoresSectionState extends State { int _selectedIndex = 0; List get _stores => (widget.responseData?['stores'] as List?) ?? []; String _discountLabel( Map store, Map 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) { return const SizedBox.shrink(); } final storeData = raw; final store = (storeData['store'] as Map?) ?? {}; 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 matchedProducts; const _SelectedStoreProducts({ super.key, required this.matchedProducts, }); @override State<_SelectedStoreProducts> createState() => _SelectedStoreProductsState(); } class _SelectedStoreProductsState extends State<_SelectedStoreProducts> { final Map _cartQuantities = {}; bool _isPlacingOrder = false; @override void initState() { super.initState(); _initCart(); } void _initCart() { for (final item in widget.matchedProducts) { if (item is! Map) continue; final product = (item['product'] as Map?) ?? {}; 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) { return const SizedBox.shrink(); } final product = (item['product'] as Map?) ?? {}; 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) continue; final product = (item['product'] as Map?) ?? {}; 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 _placeOrder() async { if (_cartQuantities.isEmpty || _isPlacingOrder) return; setState(() => _isPlacingOrder = true); try { final firstItem = widget.matchedProducts.first as Map; final firstProduct = (firstItem['product'] as Map?) ?? {}; final tenantid = firstProduct['tenantid'] as int? ?? 0; final categoryid = firstProduct['categoryid'] as int? ?? 0; final subcategoryid = firstProduct['subcategoryid'] as int? ?? 0; final items = []; double total = 0; for (final item in widget.matchedProducts) { if (item is! Map) continue; final product = (item['product'] as Map?) ?? {}; 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); }