- 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)
193 lines
6.5 KiB
Dart
193 lines
6.5 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/step_indicator.dart';
|
||
import '../../data/models/models.dart';
|
||
import '../../data/services/order_service.dart';
|
||
|
||
class ClarificationScreen extends StatefulWidget {
|
||
final int orderId;
|
||
const ClarificationScreen({super.key, required this.orderId});
|
||
|
||
@override
|
||
State<ClarificationScreen> createState() => _ClarificationScreenState();
|
||
}
|
||
|
||
class _ClarificationScreenState extends State<ClarificationScreen> {
|
||
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; });
|
||
}
|
||
|
||
List<OrderItem> get _clarifyItems {
|
||
final result = <OrderItem>[];
|
||
for (final seg in (_order?.segments ?? [])) {
|
||
result.addAll(seg.items.where((i) => i.type == ItemType.clarify && !i.isCompleted));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
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())
|
||
: Column(
|
||
children: [
|
||
const StepIndicator(currentStep: BatchStep.clarification),
|
||
Expanded(
|
||
child: _clarifyItems.isEmpty
|
||
? _buildNoClarify()
|
||
: _buildClarifyList(),
|
||
),
|
||
SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: ElevatedButton(
|
||
onPressed: () => context.push('/orders/${widget.orderId}/slots'),
|
||
child: const Text('Далі: Ячейки'),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildNoClarify() {
|
||
return const Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.check_circle, size: 64, color: AppColors.success),
|
||
SizedBox(height: 16),
|
||
Text('Немає товарів для уточнення', style: TextStyle(fontSize: 16, color: AppColors.textSecondary)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildClarifyList() {
|
||
return ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.warning.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: AppColors.warning.withOpacity(0.3)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.warning_amber, color: AppColors.warning),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text('Зателефонуйте клієнту для уточнення ${_clarifyItems.length} товарів',
|
||
style: const TextStyle(color: AppColors.warning)),
|
||
),
|
||
TextButton.icon(
|
||
onPressed: _callCustomer,
|
||
icon: const Icon(Icons.call),
|
||
label: const Text('Дзвонити'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
..._clarifyItems.map((item) => _ClarifyItemCard(
|
||
item: item,
|
||
service: _service,
|
||
onUpdated: () => setState(() {}),
|
||
)),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ClarifyItemCard extends StatelessWidget {
|
||
final OrderItem item;
|
||
final OrderService service;
|
||
final VoidCallback onUpdated;
|
||
|
||
const _ClarifyItemCard({required this.item, required this.service, required this.onUpdated});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Card(
|
||
color: AppColors.itemClarify,
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(item.name, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||
Text('${item.planQty} шт • ${item.price} грн',
|
||
style: const TextStyle(fontSize: 13, color: AppColors.textSecondary)),
|
||
const SizedBox(height: 8),
|
||
if (item.substitutes.isNotEmpty) ...[
|
||
const Text('Можливі заміни:', style: TextStyle(fontSize: 12, color: AppColors.textSecondary)),
|
||
const SizedBox(height: 4),
|
||
...item.substitutes.map((sub) => InkWell(
|
||
onTap: () async {
|
||
await service.replaceItem(item.id, sub.sku, item.planQty);
|
||
item.status = ItemStatus.replaced;
|
||
onUpdated();
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.swap_horiz, size: 16, color: AppColors.primary),
|
||
const SizedBox(width: 4),
|
||
Text(sub.name, style: const TextStyle(fontSize: 13, color: AppColors.primary)),
|
||
],
|
||
),
|
||
),
|
||
)),
|
||
],
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: OutlinedButton(
|
||
onPressed: () async {
|
||
await service.markItemNotFound(item.id);
|
||
item.status = ItemStatus.notFound;
|
||
onUpdated();
|
||
},
|
||
style: OutlinedButton.styleFrom(foregroundColor: AppColors.error),
|
||
child: const Text('Не замінювати'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|