barcode_scanner/lib/pages/operation/reception/reception_details_page.dart
your-name 18f74daae4 feat: Implement explicit synchronization and validation
Refactors the reception process to provide clearer control over data synchronization and validation.

- Replaces the `isDraft` property with `synchronized` in `StockPickingRecordEntity` to accurately represent the local data's sync status with the backend.
- Introduces a new API endpoint and corresponding logic for `validateStockPicking`, allowing finalization of receptions.
- Splits the "Save" action into "Synchronize data" and "Validate reception" on the details page. "Validate" now implicitly performs a synchronization first.
- Updates UI elements (cards, quick actions) to reflect the new synchronization state and expose the new actions.
- Corrects a typo in the `updateAllMoveLineOnStockPicking` API method name.
- Improves local data update logic for scanned items, marking them as unsynchronized.
2025-07-31 02:34:50 +03:00

218 lines
9.7 KiB
Dart

import 'package:e_scan/backend/objectbox/entities/stock_picking/stock_picking_record_entity.dart';
import 'package:e_scan/components/components.dart';
import 'package:e_scan/pages/operation/reception/reception_details_page_model.dart';
import 'package:e_scan/router/go_secure_router_builder.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:e_scan/utils/utils.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/material.dart';
class ReceptionDetailsPage extends ConsumerStatefulWidget {
const ReceptionDetailsPage({super.key, required this.receptionId});
final int receptionId;
@override
ConsumerState<ReceptionDetailsPage> createState() =>
_ReceptionDetailsPageState();
}
class _ReceptionDetailsPageState extends ConsumerState<ReceptionDetailsPage> {
@override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
ref
.read(receptionDetailsPageModelProvider.notifier)
.getReceptionById(id: widget.receptionId);
});
}
@override
Widget build(BuildContext context) {
final state = ref.watch(receptionDetailsPageModelProvider);
final reception = state.reception;
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
appBar: MainAppbarComponent(
leading: BackButton(color: AppTheme.of(context).white),
title: "Réceptions",
subTitle: "Opérations d'Entrepôt",
),
body: state.loading
? Center(child: LoadingProgressComponent())
: RefreshIndicator(
color: AppTheme.of(context).white,
backgroundColor: AppTheme.of(context).primary,
onRefresh: () async {
await ref
.read(receptionDetailsPageModelProvider.notifier)
.getReceptionById(id: widget.receptionId);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
children: [
const SizedBox(height: 16),
if (reception?.isDone == false) ...[
QuickActionComponent(
onTapValidateReception: reception?.synchronized == false
? () {
ref
.read(
receptionDetailsPageModelProvider
.notifier,
)
.validate(
receptionId: widget.receptionId,
onSuccess: () {
Toast.showSuccess(
'Réception validée avec succès.',
);
},
onError: () {
Toast.showError(
'Connexion impossible. Les données seront synchronisées plus tard.',
);
},
);
}
: null,
validateReceptionLoading: state.validateLoading,
syncReceptionLoading: state.saveLoading,
onTapSyncReception: reception?.synchronized == false
? () {
ref
.read(
receptionDetailsPageModelProvider
.notifier,
)
.save(
receptionId: widget.receptionId,
onSuccess: () {
Toast.showSuccess(
'Les données sont synchronisées.',
);
},
onError: () {
Toast.showError(
'Connexion impossible. Les données seront synchronisées plus tard.',
);
},
);
}
: null,
onTapScan: () {
final id = reception?.id;
if (id == null) return;
ReceptionScanRoute(receptionId: id)
.push(context)
.then(
(v) => ref
.read(
receptionDetailsPageModelProvider
.notifier,
)
.getReceptionById(id: widget.receptionId),
);
},
),
const SizedBox(height: 16),
],
Text(
'Détails réception',
style: AppTheme.of(
context,
).bodyMedium.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
StockPickingCard(
synchronized: reception?.synchronized == true,
isDone: reception?.isDone == true,
margin: EdgeInsets.symmetric(horizontal: 5),
reference: reception?.name ?? '',
from: reception?.locationId.target?.completeName,
to: reception?.locationDestId.target?.completeName,
contact: reception?.partnerId.target?.displayName,
origin: reception?.origin,
status: reception?.state,
),
SizedBox(height: 20),
Text(
'Produits',
style: AppTheme.of(
context,
).bodyMedium.copyWith(fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
...reception?.moveIdsWithoutPackage
.map(
(move) => Card(
color: AppTheme.of(context).primaryBackground,
child: ListTile(
title: Text(
move.productId.target?.displayName ?? '',
style: TextStyle(color: Colors.black),
),
subtitle: Wrap(
spacing: 5,
children: [
Chip(
backgroundColor: AppTheme.of(
context,
).secondaryBackground,
label: Text(
"Qté demandée: ${move.productUomQty}",
),
),
Chip(
backgroundColor: AppTheme.of(
context,
).secondaryBackground,
label: Text(
"Qté reçue: ${move.quantity}",
),
),
Chip(
backgroundColor: AppTheme.of(
context,
).secondaryBackground,
label: Text(
"Ecart: ${move.ecart}",
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
)
.toList() ??
[],
],
),
),
),
floatingActionButton: reception?.isDone == false
? FloatingActionButton(
backgroundColor: AppTheme.of(context).primary,
onPressed: () {
final id = reception?.id;
if (id == null) return;
ReceptionScanRoute(receptionId: id)
.push(context)
.then(
(v) => ref
.read(receptionDetailsPageModelProvider.notifier)
.getReceptionById(id: widget.receptionId),
);
},
child: Icon(Icons.qr_code, color: AppTheme.of(context).white),
)
: null,
);
}
}