voyage 0.1.0-canary.1 copy "voyage: ^0.1.0-canary.1" to clipboard
voyage: ^0.1.0-canary.1 copied to clipboard

A Flutter plugin for integrating Kruzr 360 trip tracking, driver analytics, and real-time vehicle data communication.

example/lib/main.dart

// change on line no 115 for licence key and 81, 82 for company name and account name
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:voyage/kruzr_360_communicator.dart';
import 'package:voyage/model/kruzr_360_init_config.dart';
import 'package:voyage/model/registered_driver.dart';
import 'package:voyage/model/single_trip_response.dart';
import 'package:voyage/model/trip_stats_response.dart';
import 'package:voyage/model/vehicle/nearby_device.dart';

void main() {
  runApp(const ZenApp());
}

String _extractError(Object e) {
  if (e is Map) {
    final msg = e['message'];
    final code = e['code'];
    if (msg != null) return code != null ? '[$code] $msg' : msg.toString();
  }
  return e.toString();
}

// ── Design tokens ────────────────────────────────────────────────
const _bg        = Color(0xFF0F1117);
const _surface   = Color(0xFF1A1D27);
const _surfaceHi = Color(0xFF222636);
const _accent    = Color(0xFF4F8EF7);
const _accentDim = Color(0xFF2A3F6F);
const _green     = Color(0xFF2ECC71);
const _red       = Color(0xFFE74C3C);
const _textPri   = Color(0xFFF0F4FF);
const _textSec   = Color(0xFF8A92A8);
const _divider   = Color(0xFF252A3A);

class ZenApp extends StatelessWidget {
  const ZenApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Kruzr Dev App',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: _bg,
        colorScheme: const ColorScheme.dark(
          primary: _accent,
          surface: _surface,
        ),
        textTheme: const TextTheme(
          bodyMedium: TextStyle(color: _textPri, fontSize: 14, height: 1.5),
          bodySmall:  TextStyle(color: _textSec, fontSize: 12),
        ),
        dividerColor: _divider,
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            backgroundColor: _accentDim,
            foregroundColor: _accent,
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
            padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
            textStyle: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, letterSpacing: 0.3),
            elevation: 0,
          ),
        ),
      ),
      home: const LoginScreen(),
    );
  }
}

/* ═══════════════════════════════════════════════════════════
   LOGIN SCREEN
═══════════════════════════════════════════════════════════ */
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _name  = TextEditingController();
  final _email = TextEditingController();
  final _phone = TextEditingController();
  final _formKey = GlobalKey<FormState>();

  final _kruzr = Kruzr360Communicator(
    companyName: "Your Company Name",
    accountName: "Your Account Name",
  );

  bool   _loading = false;
  String _error   = '';

  Future<void> _requestPermissions() async {
    final whenInUseStatus = await [
      Permission.locationWhenInUse,
    ].request();

    Map<Permission, PermissionStatus> alwaysStatus = {};
    if (whenInUseStatus[Permission.locationWhenInUse]?.isGranted ?? false) {
      alwaysStatus = await [
        Permission.locationAlways,
      ].request();
    }
    final otherStatuses = await [
      Permission.activityRecognition,
      Permission.ignoreBatteryOptimizations,
      Permission.notification,
    ].request();

    final statuses = {
      ...whenInUseStatus,
      ...alwaysStatus,
      ...otherStatuses,
    };

    // Existing denied logic (unchanged)
    final denied = statuses.entries
        .where((e) =>
    e.key != Permission.ignoreBatteryOptimizations &&
        !e.value.isGranted)
        .map((e) => e.key.toString())
        .toList();

    if (denied.isNotEmpty) {
      debugPrint('⚠️ Permissions not granted: $denied');
    }
  }

  Future<void> _registerUser() async {
    if (!(_formKey.currentState?.validate() ?? false)) return;

    setState(() { _loading = true; _error = ''; });
    await _requestPermissions();

    try {
      final initConfig = Kruzr360InitConfig(
        licenseKey: "Your LicenceKey", // ← replace with your key
        appName: "Zen",
        notificationChannelId: "zen_trip_channel",
        shouldTripAutoStart: true,
        shouldTripAutoEnd: true,
        allowEventSyncRealTime: true,
      );

      await Kruzr360Communicator.initializeSDK(initConfig);

      final String userId = await _kruzr.registerUser(
        name:        _name.text.trim(),
        driverId:    _phone.text.trim(),
        email:       _email.text.trim(),
        countryCode: "+91",
        phoneNumber: _phone.text.trim(),
      );

      if (!mounted) return;

      if (userId.isNotEmpty) {
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(
            builder: (_) => HomeScreen(kruzr: _kruzr, initialUserName: _name.text.trim()),
          ),
        );
      } else {
        setState(() => _error = "Registration failed — no userId returned.");
      }
    } catch (e, st) {
      if (!mounted) return;
      final msg = _extractError(e);
      setState(() => _error = msg);
      debugPrint(st.toString());
    } finally {
      if (mounted) setState(() => _loading = false);
    }
  }

  @override
  void dispose() {
    _name.dispose(); _email.dispose(); _phone.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: Form(
              key: _formKey,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Logo / header
                  Container(
                    width: 64, height: 64,
                    decoration: BoxDecoration(
                      color: _accentDim,
                      borderRadius: BorderRadius.circular(18),
                    ),
                    child: const Icon(Icons.directions_car_rounded, color: _accent, size: 34),
                  ),
                  const SizedBox(height: 24),
                  const Text('Kruzr Dev',
                    style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: _textPri, letterSpacing: -0.5),
                  ),
                  const SizedBox(height: 6),
                  const Text('Plugin test harness',
                    style: TextStyle(fontSize: 14, color: _textSec),
                  ),
                  const SizedBox(height: 36),

                  // Form card
                  Container(
                    padding: const EdgeInsets.all(20),
                    decoration: BoxDecoration(
                      color: _surface,
                      borderRadius: BorderRadius.circular(16),
                      border: Border.all(color: _divider),
                    ),
                    child: Column(
                      children: [
                        _InputField(controller: _name,  label: 'Full Name',     icon: Icons.person_outline,
                            validator: (v) => (v == null || v.isEmpty) ? 'Required' : null),
                        const SizedBox(height: 14),
                        _InputField(controller: _email, label: 'Email',          icon: Icons.mail_outline,
                            inputType: TextInputType.emailAddress,
                            validator: (v) => (v == null || !v.contains('@')) ? 'Enter valid email' : null),
                        const SizedBox(height: 14),
                        _InputField(controller: _phone, label: 'Phone (driverId)', icon: Icons.phone_outlined,
                            inputType: TextInputType.phone,
                            validator: (v) => (v == null || v.length < 5) ? 'Enter valid phone' : null),
                      ],
                    ),
                  ),

                  const SizedBox(height: 20),

                  if (_error.isNotEmpty)
                    Container(
                      padding: const EdgeInsets.all(12),
                      margin: const EdgeInsets.only(bottom: 12),
                      decoration: BoxDecoration(
                        color: _red.withValues(alpha: 0.12),
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(color: _red.withValues(alpha: 0.3)),
                      ),
                      child: Row(
                        children: [
                          const Icon(Icons.error_outline, color: _red, size: 16),
                          const SizedBox(width: 8),
                          Expanded(child: Text(_error, style: const TextStyle(color: _red, fontSize: 13))),
                        ],
                      ),
                    ),

                  SizedBox(
                    height: 50,
                    child: ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        backgroundColor: _accent,
                        foregroundColor: Colors.white,
                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
                      ),
                      onPressed: _loading ? null : _registerUser,
                      child: _loading
                          ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
                          : const Text('Register & Continue', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _InputField extends StatelessWidget {
  final TextEditingController controller;
  final String label;
  final IconData icon;
  final TextInputType? inputType;
  final String? Function(String?)? validator;
  const _InputField({required this.controller, required this.label, required this.icon, this.inputType, this.validator});

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      keyboardType: inputType,
      validator: validator,
      style: const TextStyle(color: _textPri, fontSize: 14),
      decoration: InputDecoration(
        labelText: label,
        labelStyle: const TextStyle(color: _textSec, fontSize: 13),
        prefixIcon: Icon(icon, color: _textSec, size: 18),
        filled: true,
        fillColor: _surfaceHi,
        border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _divider)),
        enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _divider)),
        focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _accent, width: 1.5)),
        errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _red)),
        contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
      ),
    );
  }
}

/* ═══════════════════════════════════════════════════════════
   HOME SCREEN
═══════════════════════════════════════════════════════════ */
class HomeScreen extends StatefulWidget {
  final Kruzr360Communicator kruzr;
  final String initialUserName;
  const HomeScreen({super.key, required this.kruzr, required this.initialUserName});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  RegisteredDriver?                   _driver;
  List<SingleTripResponse>            _trips       = [];
  Map<String, TripStatsResponse>      _statsMap    = {};
  final Map<String, dynamic>          _insightsMap = {};
  String?                             _selectedTripId;  // auto-set from loaded trips
  bool                                _loading     = true;
  String?                             _error;

  // ── Trip start/stop state ────────────────────────────────
  bool _tripRunning = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _loadAll());
  }

  // ── Data loading ─────────────────────────────────────────
  Future<void> _loadAll() async {
    setState(() { _loading = true; _error = null; });
    try {
      // 1. User details
      final driverRaw = await _safeCall(() => widget.kruzr.getUserDetails());
      _driver = _parse<RegisteredDriver>(driverRaw, RegisteredDriver.fromJson);

      // 2. Trip list — use new name
      final tripsRaw = await _safeCall(() => widget.kruzr.getTripList(0, 10));
      final trips = _parseList<SingleTripResponse>(tripsRaw, SingleTripResponse.fromJson);

      if (trips != null && trips.isNotEmpty) {
        _trips = trips.take(4).toList();

        // Auto-select first trip if none selected yet
        _selectedTripId ??= _trips.first.appTripId;

        // 3. Stats for each trip — use new name
        _statsMap = {};
        for (final trip in _trips) {
          if (trip.appTripId.isEmpty) continue;
          final statsRaw = await _safeCall(() => widget.kruzr.getTripStats(trip.appTripId));
          final stats = _parse<TripStatsResponse>(statsRaw, TripStatsResponse.fromJson);
          if (stats != null) _statsMap[trip.appTripId] = stats;
        }

        // 4. Insights for COMPLETED trips only — use new name
        await Future.wait(_trips.map((trip) async {
          final id = trip.appTripId;
          if (id.isEmpty) return;
          if (trip.tripScoringStatus?.value != "COMPLETED") return;
          try {
            final insights = await widget.kruzr.fetchTripInsights(id);
            _insightsMap[id] = insights;
          } catch (e) {
            debugPrint("Insights failed for $id: $e");
          }
        }));
      }

      if (!mounted) return;
      setState(() => _loading = false);
    } catch (e, st) {
      debugPrint("_loadAll error: $e\n$st");
      if (!mounted) return;
      setState(() { _error = e.toString(); _loading = false; });
    }
  }

  Future<dynamic> _safeCall(Future<dynamic> Function() fn) async {
    try { return await fn(); }
    catch (e) { debugPrint("plugin call failed: ${_extractError(e)}"); return null; }
  }

  T? _parse<T>(dynamic raw, T Function(Map<String, dynamic>) fromJson) {
    if (raw == null) return null;
    try {
      if (raw is T) return raw;
      if (raw is String) return fromJson(jsonDecode(raw) as Map<String, dynamic>);
      if (raw is Map) return fromJson(Map<String, dynamic>.from(raw));
    } catch (e) { debugPrint("_parse<$T> failed: $e"); }
    return null;
  }

  List<T>? _parseList<T>(dynamic raw, T Function(Map<String, dynamic>) fromJson) {
    if (raw == null) return null;
    try {
      List items;
      if (raw is List)   items = raw;
      else if (raw is String) items = jsonDecode(raw) as List;
      else return null;
      return items.map((e) {
        if (e is T) return e;
        if (e is Map) return fromJson(Map<String, dynamic>.from(e));
        return fromJson(Map<String, dynamic>.from(jsonDecode(e as String)));
      }).toList();
    } catch (e) { debugPrint("_parseList<$T> failed: $e"); }
    return null;
  }

  // ── Formatting helpers ───────────────────────────────────
  String _fmt(num? v, {int fixed = 1}) => v == null ? '–' : v.toDouble().toStringAsFixed(fixed);

  String _fmtDuration(num? s) {
    if (s == null) return '–';
    final d = Duration(seconds: s.toInt());
    String p(int n) => n.toString().padLeft(2, '0');
    return '${p(d.inHours)}:${p(d.inMinutes.remainder(60))}:${p(d.inSeconds.remainder(60))}';
  }

  // ── Dev test runner ──────────────────────────────────────
  Future<void> _test(String label, Future<dynamic> Function() fn) async {
    try {
      final res = await fn();
      debugPrint("🧪 [$label] => $res");
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text("✓ $label"),
        duration: const Duration(milliseconds: 800),
        behavior: SnackBarBehavior.floating,
        backgroundColor: _green.withValues(alpha: 0.9),
      ));
    } catch (e, st) {
      debugPrint("❌ [$label] $e\n$st");
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text("❌ $label: ${_extractError(e)}"),
        behavior: SnackBarBehavior.floating,
        backgroundColor: _red.withValues(alpha: 0.9),
      ));
    }
  }

  // ── Trip controls ────────────────────────────────────────
  Future<void> _startTrip() async {
    final whenInUse = await Permission.locationWhenInUse.request();

    if (!whenInUse.isGranted) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text("Location permission required"),
          behavior: SnackBarBehavior.floating,
        ),
      );
      return;
    }
    final always = await Permission.locationAlways.request();
    final activity = await Permission.activityRecognition.request();
    final statuses = {
      Permission.locationWhenInUse: whenInUse,
      Permission.locationAlways: always,
      Permission.activityRecognition: activity,
    };

    final allGranted = statuses.values.every((s) => s.isGranted);

    if (!allGranted) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text("Location & Activity recognition permissions required"),
          behavior: SnackBarBehavior.floating,
        ),
      );
      return;
    }

    try {
      final started = await widget.kruzr.startTrip();
      debugPrint("startTrip => $started");

      if (started == true) {
        setState(() => _tripRunning = true);
      }

      await Future.delayed(const Duration(seconds: 1));
      _loadAll();
    } catch (e) {
      debugPrint("startTrip error: $e");
    }
  }

  Future<void> _stopTrip() async {
    try {
      final stopped = await widget.kruzr.stopTrip();
      debugPrint("stopTrip => $stopped");
      if (stopped == true) setState(() => _tripRunning = false);
      await Future.delayed(const Duration(milliseconds: 800));
      _loadAll();
    } catch (e) {
      debugPrint("stopTrip error: $e");
    }
  }

  // ── Build ─────────────────────────────────────────────────
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: _bg,
      appBar: AppBar(
        backgroundColor: _surface,
        elevation: 0,
        title: Row(children: [
          const Icon(Icons.directions_car_rounded, color: _accent, size: 20),
          const SizedBox(width: 8),
          Text(
            _driver?.name ?? widget.initialUserName,
            style: const TextStyle(color: _textPri, fontSize: 16, fontWeight: FontWeight.w600),
          ),
        ]),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh_rounded, color: _textSec),
            onPressed: _loading ? null : _loadAll,
            tooltip: 'Refresh',
          ),
        ],
      ),
      body: _loading
          ? const Center(child: CircularProgressIndicator(color: _accent, strokeWidth: 2))
          : _error != null
          ? _ErrorView(message: _error!, onRetry: _loadAll)
          : RefreshIndicator(
        color: _accent,
        onRefresh: _loadAll,
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _StatsCard(driver: _driver, fmt: _fmt, fmtDuration: _fmtDuration),
            const SizedBox(height: 16),
            _TripControls(
              tripRunning: _tripRunning,
              onStart: _startTrip,
              onStop: _stopTrip,
            ),
            const SizedBox(height: 16),
            _TripList(
              trips: _trips,
              statsMap: _statsMap,
              insightsMap: _insightsMap,
              selectedTripId: _selectedTripId,
              onSelect: (id) => setState(() => _selectedTripId = id),
              fmt: _fmt,
              fmtDuration: _fmtDuration,
            ),
            if (_selectedTripId != null) ...[
              const SizedBox(height: 8),
              _SelectedTripBadge(tripId: _selectedTripId!),
            ],
            const SizedBox(height: 24),
            _DevPanel(
              kruzr: widget.kruzr,
              selectedTripId: _selectedTripId,
              test: _test,
            ),
            const SizedBox(height: 40),
          ],
        ),
      ),
    );
  }
}

/* ═══════════════════════════════════════════════════════════
   COMPONENT WIDGETS
═══════════════════════════════════════════════════════════ */

// ── Stats card ───────────────────────────────────────────────────
class _StatsCard extends StatelessWidget {
  final RegisteredDriver? driver;
  final String Function(num?, {int fixed}) fmt;
  final String Function(num?) fmtDuration;
  const _StatsCard({required this.driver, required this.fmt, required this.fmtDuration});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        gradient: const LinearGradient(
          colors: [Color(0xFF1A2A4A), Color(0xFF1A1D27)],
          begin: Alignment.topLeft, end: Alignment.bottomRight,
        ),
        borderRadius: BorderRadius.circular(16),
        border: Border.all(color: _accentDim),
      ),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Row(children: [
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            decoration: BoxDecoration(color: _accentDim, borderRadius: BorderRadius.circular(6)),
            child: const Text('Driver Stats', style: TextStyle(color: _accent, fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 0.5)),
          ),
        ]),
        const SizedBox(height: 16),
        Row(children: [
          _StatItem(label: 'Trips',    value: '${driver?.totalTripCount ?? 0}', icon: Icons.route_rounded),
          const SizedBox(width: 16),
          _StatItem(label: 'Distance', value: '${fmt(driver?.totalDistanceTravelled)} km', icon: Icons.straighten_rounded),
          const SizedBox(width: 16),
          _StatItem(label: 'Duration', value: fmtDuration(driver?.totalTripDurationInSeconds), icon: Icons.timer_outlined),
        ]),
        const SizedBox(height: 14),
        Container(
          padding: const EdgeInsets.all(12),
          decoration: BoxDecoration(color: _surfaceHi, borderRadius: BorderRadius.circular(10)),
          child: Row(children: [
            const Icon(Icons.star_rounded, color: Color(0xFFF4C430), size: 18),
            const SizedBox(width: 8),
            Text('Avg Score', style: const TextStyle(color: _textSec, fontSize: 13)),
            const Spacer(),
            Text(fmt(driver?.averageTripScore, fixed: 2),
                style: const TextStyle(color: _textPri, fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: -0.5)),
          ]),
        ),
      ]),
    );
  }
}

class _StatItem extends StatelessWidget {
  final String label, value;
  final IconData icon;
  const _StatItem({required this.label, required this.value, required this.icon});

  @override
  Widget build(BuildContext context) {
    return Expanded(child: Container(
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(color: _surfaceHi, borderRadius: BorderRadius.circular(10)),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Icon(icon, color: _accent, size: 16),
        const SizedBox(height: 6),
        Text(value, style: const TextStyle(color: _textPri, fontSize: 15, fontWeight: FontWeight.w700)),
        const SizedBox(height: 2),
        Text(label, style: const TextStyle(color: _textSec, fontSize: 11)),
      ]),
    ));
  }
}

// ── Trip controls ────────────────────────────────────────────────
class _TripControls extends StatelessWidget {
  final bool tripRunning;
  final VoidCallback onStart, onStop;
  const _TripControls({required this.tripRunning, required this.onStart, required this.onStop});

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      Expanded(child: _PrimaryBtn(
        label: 'Start Trip',
        icon: Icons.play_arrow_rounded,
        color: _green,
        onTap: tripRunning ? null : onStart,
      )),
      const SizedBox(width: 12),
      Expanded(child: _PrimaryBtn(
        label: 'Stop Trip',
        icon: Icons.stop_rounded,
        color: _red,
        onTap: !tripRunning ? null : onStop,
      )),
    ]);
  }
}

class _PrimaryBtn extends StatelessWidget {
  final String label;
  final IconData icon;
  final Color color;
  final VoidCallback? onTap;
  const _PrimaryBtn({required this.label, required this.icon, required this.color, this.onTap});

  @override
  Widget build(BuildContext context) {
    final active = onTap != null;
    return GestureDetector(
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 150),
        padding: const EdgeInsets.symmetric(vertical: 14),
        decoration: BoxDecoration(
          color: active ? color.withValues(alpha: 0.15) : _surfaceHi,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: active ? color.withValues(alpha: 0.5) : _divider),
        ),
        child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          Icon(icon, color: active ? color : _textSec, size: 18),
          const SizedBox(width: 8),
          Text(label, style: TextStyle(color: active ? color : _textSec, fontWeight: FontWeight.w600, fontSize: 14)),
        ]),
      ),
    );
  }
}

// ── Trip list ────────────────────────────────────────────────────
class _TripList extends StatelessWidget {
  final List<SingleTripResponse> trips;
  final Map<String, TripStatsResponse> statsMap;
  final Map<String, dynamic> insightsMap;
  final String? selectedTripId;
  final void Function(String) onSelect;
  final String Function(num?, {int fixed}) fmt;
  final String Function(num?) fmtDuration;
  const _TripList({required this.trips, required this.statsMap, required this.insightsMap,
    required this.selectedTripId, required this.onSelect, required this.fmt, required this.fmtDuration});

  @override
  Widget build(BuildContext context) {
    if (trips.isEmpty) {
      return Container(
        padding: const EdgeInsets.all(20),
        decoration: BoxDecoration(color: _surface, borderRadius: BorderRadius.circular(14), border: Border.all(color: _divider)),
        child: const Center(child: Text('No recent trips found', style: TextStyle(color: _textSec))),
      );
    }

    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      Padding(
        padding: const EdgeInsets.only(bottom: 10),
        child: Row(children: [
          const Text('Recent Trips', style: TextStyle(color: _textPri, fontSize: 16, fontWeight: FontWeight.w700)),
          const Spacer(),
          Text('Tap to select', style: TextStyle(color: _textSec, fontSize: 12)),
        ]),
      ),
      ...trips.asMap().entries.map((e) => Padding(
        padding: const EdgeInsets.only(bottom: 8),
        child: _TripCard(
          trip: e.value,
          index: e.key,
          stats: statsMap[e.value.appTripId],
          hasInsights: insightsMap.containsKey(e.value.appTripId),
          isSelected: e.value.appTripId == selectedTripId,
          onTap: () => onSelect(e.value.appTripId),
          fmt: fmt,
          fmtDuration: fmtDuration,
        ),
      )),
    ]);
  }
}

class _TripCard extends StatelessWidget {
  final SingleTripResponse trip;
  final int index;
  final TripStatsResponse? stats;
  final bool hasInsights, isSelected;
  final VoidCallback onTap;
  final String Function(num?, {int fixed}) fmt;
  final String Function(num?) fmtDuration;
  const _TripCard({required this.trip, required this.index, this.stats, required this.hasInsights,
    required this.isSelected, required this.onTap, required this.fmt, required this.fmtDuration});

  @override
  Widget build(BuildContext context) {
    final status = trip.tripScoringStatus?.value ?? '–';
    final statusColor = status == 'COMPLETED' ? _green : _textSec;

    return GestureDetector(
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 150),
        padding: const EdgeInsets.all(14),
        decoration: BoxDecoration(
          color: isSelected ? const Color(0xFF1A2A4A) : _surface,
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: isSelected ? _accent : _divider, width: isSelected ? 1.5 : 1),
        ),
        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
          Row(children: [
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
              decoration: BoxDecoration(color: _surfaceHi, borderRadius: BorderRadius.circular(6)),
              child: Text('Trip ${index + 1}', style: const TextStyle(color: _textSec, fontSize: 11, fontWeight: FontWeight.w600)),
            ),
            const Spacer(),
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
              decoration: BoxDecoration(color: statusColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6)),
              child: Text(status, style: TextStyle(color: statusColor, fontSize: 10, fontWeight: FontWeight.w600)),
            ),
            if (isSelected) ...[
              const SizedBox(width: 6),
              const Icon(Icons.check_circle_rounded, color: _accent, size: 16),
            ],
          ]),
          const SizedBox(height: 10),
          Row(children: [
            _TripStat(label: 'Score',    value: fmt(trip.overallTripScore, fixed: 1), icon: Icons.star_rounded),
            _TripStat(label: 'Distance', value: '${fmt(trip.totalDistance)} km',      icon: Icons.straighten_rounded),
            _TripStat(label: 'Duration', value: '${trip.getTripDuration() ?? '–'} min', icon: Icons.timer_outlined),
          ]),
          if (stats != null || hasInsights) ...[
            const SizedBox(height: 8),
            Row(children: [
              if (stats != null) _Chip(label: 'Stats ✓', color: _green),
              if (hasInsights)  ...[const SizedBox(width: 6), _Chip(label: 'Insights ✓', color: _accent)],
            ]),
          ],
          const SizedBox(height: 6),
          Text(trip.appTripId, style: const TextStyle(color: _textSec, fontSize: 10), overflow: TextOverflow.ellipsis),
        ]),
      ),
    );
  }
}

class _TripStat extends StatelessWidget {
  final String label, value;
  final IconData icon;
  const _TripStat({required this.label, required this.value, required this.icon});

  @override
  Widget build(BuildContext context) => Expanded(
    child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      Row(children: [Icon(icon, size: 12, color: _textSec), const SizedBox(width: 4), Text(label, style: const TextStyle(color: _textSec, fontSize: 10))]),
      const SizedBox(height: 2),
      Text(value, style: const TextStyle(color: _textPri, fontSize: 13, fontWeight: FontWeight.w600)),
    ]),
  );
}

class _Chip extends StatelessWidget {
  final String label; final Color color;
  const _Chip({required this.label, required this.color});

  @override
  Widget build(BuildContext context) => Container(
    padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
    decoration: BoxDecoration(color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(5)),
    child: Text(label, style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.w600)),
  );
}

// ── Selected trip badge ──────────────────────────────────────────
class _SelectedTripBadge extends StatelessWidget {
  final String tripId;
  const _SelectedTripBadge({required this.tripId});

  @override
  Widget build(BuildContext context) => Container(
    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
    decoration: BoxDecoration(
      color: _accent.withValues(alpha: 0.08),
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: _accent.withValues(alpha: 0.25)),
    ),
    child: Row(children: [
      const Icon(Icons.info_outline_rounded, color: _accent, size: 14),
      const SizedBox(width: 8),
      const Text('Selected: ', style: TextStyle(color: _textSec, fontSize: 12)),
      Expanded(child: Text(tripId, style: const TextStyle(color: _accent, fontSize: 12, fontWeight: FontWeight.w600), overflow: TextOverflow.ellipsis)),
    ]),
  );
}

// ── Error view ───────────────────────────────────────────────────
class _ErrorView extends StatelessWidget {
  final String message; final VoidCallback onRetry;
  const _ErrorView({required this.message, required this.onRetry});

  @override
  Widget build(BuildContext context) => Center(
    child: Padding(
      padding: const EdgeInsets.all(32),
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        const Icon(Icons.wifi_off_rounded, color: _red, size: 48),
        const SizedBox(height: 16),
        Text('Something went wrong', style: const TextStyle(color: _textPri, fontSize: 16, fontWeight: FontWeight.w600)),
        const SizedBox(height: 8),
        Text(message, style: const TextStyle(color: _textSec, fontSize: 12), textAlign: TextAlign.center),
        const SizedBox(height: 20),
        ElevatedButton.icon(onPressed: onRetry, icon: const Icon(Icons.refresh_rounded, size: 16), label: const Text('Retry')),
      ]),
    ),
  );
}

/* ═══════════════════════════════════════════════════════════
   DEV PANEL — grouped collapsible sections
═══════════════════════════════════════════════════════════ */
class _DevPanel extends StatelessWidget {
  final Kruzr360Communicator kruzr;
  final String? selectedTripId;
  final Future<void> Function(String, Future<dynamic> Function()) test;
  const _DevPanel({required this.kruzr, required this.selectedTripId, required this.test});

  @override
  Widget build(BuildContext context) {
    final hasTripId = selectedTripId != null;

    return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
      const Padding(
        padding: EdgeInsets.only(bottom: 12),
        child: Row(children: [
          Icon(Icons.bug_report_rounded, color: _accent, size: 16),
          SizedBox(width: 8),
          Text('Developer Test Panel', style: TextStyle(color: _textPri, fontSize: 16, fontWeight: FontWeight.w700)),
        ]),
      ),

      // User
      _DevSection(title: 'User & Ranking', icon: Icons.person_rounded, items: [
        _DevItem('getUserDetails',        () => kruzr.getUserDetails()),
        _DevItem('getUserStreak',         () => kruzr.getUserStreak()),
        _DevItem('getUserRank',           () => kruzr.getUserRank()),
        _DevItem('getLeaderboard',        () => kruzr.getLeaderboard(0, 10)),
        _DevItem('getLeaderboardTop10',   () => kruzr.getLeaderboardTop10()),
        _DevItem('getAchievements',       () => kruzr.getAchievements()),
      ], test: test),

      // Trip
      _DevSection(title: 'Trip Data', icon: Icons.route_rounded, items: [
        _DevItem('getCurrentTrip',        () => kruzr.getOngoingTrip()),
        _DevItem('getTripList',           () => kruzr.getTripList(0, 10)),
        _DevItem('getTripDetails',        hasTripId ? () => kruzr.getTripDetails(selectedTripId!) : null, requiresTripId: true),
        _DevItem('getTripStats',          hasTripId ? () => kruzr.getTripStats(selectedTripId!) : null, requiresTripId: true),
        _DevItem('getTripInterventions',  hasTripId ? () => kruzr.getTripInterventions(selectedTripId!) : null, requiresTripId: true),
        _DevItem('getTripRoute',          hasTripId ? () => kruzr.getTripRoute(selectedTripId!) : null, requiresTripId: true),
        _DevItem('fetchTripInsights',     hasTripId ? () => kruzr.fetchTripInsights(selectedTripId!) : null, requiresTripId: true),
        _DevItem('shareTrip',             hasTripId ? () => kruzr.shareTrip(selectedTripId!, "+91", "9999999999") : null, requiresTripId: true),
        _DevItem('getTripShareUrl',       hasTripId ? () => kruzr.getTripShareUrl(selectedTripId!) : null, requiresTripId: true),
      ], test: test),

      // Sync
      _DevSection(title: 'Sync & Settings', icon: Icons.sync_rounded, items: [
        _DevItem('syncTripData',                 () => kruzr.syncTripData()),
        _DevItem('refreshSyncStatus',            () => kruzr.refreshSyncStatus()),
        _DevItem('getPendingSyncCount',          () => kruzr.getPendingSyncCount()),
        _DevItem('getPendingSyncSize',           () => kruzr.getPendingSyncSize()),
        _DevItem('isWifiOnlySyncEnabled',        () => kruzr.isWifiOnlySyncEnabled()),
        _DevItem('setWifiOnlySync ON',           () => kruzr.setWifiOnlySync(true)),
        _DevItem('setWifiOnlySync OFF',          () => kruzr.setWifiOnlySync(false)),
        _DevItem('isTripAutoStartEnabled',       () => kruzr.isTripAutoStartEnabled()),
        _DevItem('setTripAutoStart ON',          () => kruzr.setTripAutoStart(true)),
        _DevItem('setTripAutoStart OFF',         () => kruzr.setTripAutoStart(false)),
        _DevItem('isTripAutoEndEnabled',         () => kruzr.isTripAutoEndEnabled()),
        _DevItem('setTripAutoEnd ON',            () => kruzr.setTripAutoEnd(true)),
        _DevItem('setTripAutoEnd OFF',           () => kruzr.setTripAutoEnd(false)),
        _DevItem('isRealTimeEventSyncEnabled',   () => kruzr.isRealTimeEventSyncEnabled()),
        _DevItem('setRealTimeEventSync ON',      () => kruzr.setRealTimeEventSync(true)),
        _DevItem('setRealTimeEventSync OFF',     () => kruzr.setRealTimeEventSync(false)),
        _DevItem('getAutoTripStartIfVehicle',    () => kruzr.getAutoTripStartOnlyIfVehicleConnected()),
        _DevItem('setAutoTripStartIfVehicle ON', () => kruzr.setAutoTripStartOnlyIfVehicleConnected(true)),
      ], test: test),

      // Bluetooth
      _DevSection(title: 'Bluetooth', icon: Icons.bluetooth_rounded, items: [
        _DevItem('startBluetoothScan',    () => kruzr.startBluetoothScan()),
        _DevItem('stopBluetoothScan',     () => kruzr.stopBluetoothScan()),
        _DevItem('getPairedDevices',      () => kruzr.getPairedDevices()),
        _DevItem('getConnectedDevices',   () => kruzr.getConnectedDevices()),
      ], test: test),

      // Vehicle
      _DevSection(title: 'Vehicle', icon: Icons.directions_car_rounded, items: [
        _DevItem('saveVehicle', () async {
          kruzr.saveVehicle(Vehicle(id: 'DEV_TEST_MAC', name: 'Test Car', vehicleId: 'VH_123'));
          return 'OK';
        }),
        _DevItem('getSavedVehicles',      () => kruzr.getSavedVehicles()),
        _DevItem('updateVehicle', () async {
          kruzr.updateVehicle(Vehicle(id: 'DEV_TEST_MAC', name: 'Edited Car', vehicleId: null));
          return 'OK';
        }),
        _DevItem('removeVehicle', () async {
          kruzr.removeVehicle(Vehicle(id: 'DEV_TEST_MAC', name: 'Test Car', vehicleId: 'VH_123'));
          return 'OK';
        }),
        _DevItem('removeAllVehicles', () async {
          kruzr.removeAllVehicles();
          return 'OK';
        }),
      ], test: test),

      // Rewards
      _DevSection(title: 'Rewards', icon: Icons.emoji_events_rounded, items: [
        _DevItem('getAvailableRewards',       () => kruzr.getAvailableRewards()),
        _DevItem('getEarnedRewards',           () => kruzr.getEarnedRewards()),
        _DevItem('getUserTierRequirements',    () => kruzr.getUserTierRequirements()),
      ], test: test),

      // Permissions
      // _DevSection(title: 'Permissions', icon: Icons.lock_outline_rounded, items: [
      //   // _DevItem('checkPermission (location)',  () => kruzr.checkPermission(KruzrPermission.location)),
      //   // _DevItem('requestPermission (location)',() => kruzr.requestPermission(KruzrPermission.location)),
      //   // _DevItem('ensurePermission (location)', () => kruzr.ensurePermission(KruzrPermission.location)),
      //   // _DevItem('openSettings',                () async { kruzr.openSettings(); return 'OK'; }),
      // ], test: test),
    ]);
  }
}

class _DevItem {
  final String label;
  final Future<dynamic> Function()? fn;
  final bool requiresTripId;
  const _DevItem(this.label, this.fn, {this.requiresTripId = false});
}

class _DevSection extends StatefulWidget {
  final String title;
  final IconData icon;
  final List<_DevItem> items;
  final Future<void> Function(String, Future<dynamic> Function()) test;
  const _DevSection({required this.title, required this.icon, required this.items, required this.test});

  @override
  State<_DevSection> createState() => _DevSectionState();
}

class _DevSectionState extends State<_DevSection> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 10),
      decoration: BoxDecoration(
        color: _surface,
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: _expanded ? _accentDim : _divider),
      ),
      child: Column(children: [
        // Header
        InkWell(
          onTap: () => setState(() => _expanded = !_expanded),
          borderRadius: BorderRadius.circular(12),
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
            child: Row(children: [
              Icon(widget.icon, color: _accent, size: 16),
              const SizedBox(width: 10),
              Text(widget.title, style: const TextStyle(color: _textPri, fontSize: 14, fontWeight: FontWeight.w600)),
              const SizedBox(width: 8),
              Container(
                padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
                decoration: BoxDecoration(color: _accentDim, borderRadius: BorderRadius.circular(10)),
                child: Text('${widget.items.length}', style: const TextStyle(color: _accent, fontSize: 11, fontWeight: FontWeight.w700)),
              ),
              const Spacer(),
              AnimatedRotation(
                turns: _expanded ? 0.5 : 0,
                duration: const Duration(milliseconds: 200),
                child: const Icon(Icons.keyboard_arrow_down_rounded, color: _textSec, size: 18),
              ),
            ]),
          ),
        ),
        // Items
        if (_expanded) ...[
          const Divider(height: 1, color: _divider),
          Padding(
            padding: const EdgeInsets.all(10),
            child: Wrap(spacing: 8, runSpacing: 8, children: widget.items.map((item) {
              final enabled = item.fn != null;
              return GestureDetector(
                onTap: enabled ? () => widget.test(item.label, item.fn!) : null,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                  decoration: BoxDecoration(
                    color: enabled ? _surfaceHi : _surfaceHi.withValues(alpha: 0.5),
                    borderRadius: BorderRadius.circular(8),
                    border: Border.all(color: item.requiresTripId ? _accentDim : _divider),
                  ),
                  child: Row(mainAxisSize: MainAxisSize.min, children: [
                    if (item.requiresTripId) ...[
                      const Icon(Icons.link_rounded, size: 11, color: _accent),
                      const SizedBox(width: 4),
                    ],
                    Text(
                      item.label,
                      style: TextStyle(
                        color: enabled ? _textPri : _textSec,
                        fontSize: 12,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                  ]),
                ),
              );
            }).toList()),
          ),
        ],
      ]),
    );
  }
}
0
likes
0
points
1.55k
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for integrating Kruzr 360 trip tracking, driver analytics, and real-time vehicle data communication.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, geojson_vi, permission_handler, plugin_platform_interface

More

Packages that depend on voyage

Packages that implement voyage