velmart-picker/lib/features/order_detail/order_detail_screen.dart
Fibe Agent 5ba2691eac feat: initial Velmart Picker Flutter app
- All 5 batch steps: Picking, Sorting, Clarification, Slots, Handoff
- go_router navigation with step indicator
- graphql_flutter client wired up (endpoint via env var)
- Mock data layer swappable with real GraphQL service
- Item types: normal, cold, frozen, alcohol, clarify
- Storage slot assignment (cell/freezer/fridge)
2026-04-23 21:43:07 +00:00

208 lines
6.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/order_progress_bar.dart';
import '../../core/widgets/step_indicator.dart';
import '../../data/models/models.dart';
import '../../data/services/order_service.dart';
class OrderDetailScreen extends StatefulWidget {
final int orderId;
const OrderDetailScreen({super.key, required this.orderId});
@override
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
final _service = MockOrderService();
Order? _order;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final order = await _service.getOrderDetail(widget.orderId);
if (mounted) setState(() { _order = order; _loading = false; });
}
void _callCustomer() async {
if (_order == null) return;
final uri = Uri(scheme: 'tel', path: _order!.customer.phone);
if (await canLaunchUrl(uri)) launchUrl(uri);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_order?.orderNumber ?? 'Замовлення'),
leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => context.pop()),
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _order == null
? const Center(child: Text('Замовлення не знайдено'))
: _buildBody(),
);
}
Widget _buildBody() {
final order = _order!;
return Column(
children: [
const StepIndicator(currentStep: BatchStep.picking),
// Customer info bar
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(order.customer.name,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
if (order.customer.comment != null)
Text(order.customer.comment!,
style: const TextStyle(fontSize: 12, color: AppColors.warning),
maxLines: 2,
overflow: TextOverflow.ellipsis),
],
),
),
IconButton(
icon: const Icon(Icons.call, color: AppColors.primary),
onPressed: _callCustomer,
tooltip: 'Зателефонувати',
),
],
),
),
const Divider(height: 1),
// Overall progress
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: OrderProgressBar(picked: order.pickedItems, total: order.totalItems),
),
const Divider(height: 1),
// Segments list
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: order.segments.length,
itemBuilder: (context, i) {
final segment = order.segments[i];
return _SegmentCard(
segment: segment,
onTap: () => context.push('/orders/${order.id}/picking/${segment.id}'),
);
},
),
),
// Bottom action
SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: order.isFullyPicked
? () => context.push('/orders/${order.id}/sorting')
: null,
child: const Text('Далі: Сортування'),
),
),
),
],
);
}
}
class _SegmentCard extends StatelessWidget {
final OrderSegment segment;
final VoidCallback onTap;
const _SegmentCard({required this.segment, required this.onTap});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(segment.name,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
Text('Зона ${segment.zone}',
style: const TextStyle(fontSize: 12, color: AppColors.textSecondary)),
],
),
),
if (segment.isCompleted)
const Icon(Icons.check_circle, color: AppColors.success)
else
const Icon(Icons.chevron_right, color: AppColors.textSecondary),
],
),
const SizedBox(height: 10),
OrderProgressBar(
picked: segment.pickedCount,
total: segment.totalCount,
showLabel: true,
),
const SizedBox(height: 8),
Wrap(
spacing: 6,
children: _buildTypeTags(segment),
),
],
),
),
),
);
}
List<Widget> _buildTypeTags(OrderSegment segment) {
final types = segment.items.map((i) => i.type).toSet();
return types.map((type) {
switch (type) {
case ItemType.cold:
return _tag('Холодне', AppColors.info);
case ItemType.frozen:
return _tag('Морозилка', Colors.indigo);
case ItemType.alcohol:
return _tag('Алкоголь', AppColors.warning);
case ItemType.clarify:
return _tag('Уточнення', AppColors.accent);
case ItemType.normal:
return const SizedBox.shrink();
}
}).toList();
}
Widget _tag(String label, Color color) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.4)),
),
child: Text(label, style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.w600)),
);
}