feat: Gestion complète des containers et refactorisation du matériel
Ajout de la gestion des containers (création, édition, suppression, affichage des détails).
Introduction d'un système de génération de QR codes unifié et d'un mode de sélection multiple.
**Features:**
- **Gestion des Containers :**
- Nouvelle page de gestion des containers (`container_management_page.dart`) avec recherche et filtres.
- Formulaire de création/édition de containers (`container_form_page.dart`) avec génération d'ID automatique.
- Page de détails d'un container (`container_detail_page.dart`) affichant son contenu et ses caractéristiques.
- Ajout des routes et du provider (`ContainerProvider`) nécessaires.
- **Modèle de Données :**
- Ajout du `ContainerModel` pour représenter les boîtes, flight cases, etc.
- Le modèle `EquipmentModel` a été enrichi avec des caractéristiques physiques (poids, dimensions).
- **QR Codes :**
- Nouveau service unifié (`UnifiedPDFGeneratorService`) pour générer des PDFs de QR codes pour n'importe quelle entité.
- Services `PDFGeneratorService` et `ContainerPDFGeneratorService` transformés en wrappers pour maintenir la compatibilité.
- Amélioration de la performance de la génération de QR codes en masse.
- **Interface Utilisateur (UI/UX) :**
- Nouvelle page de détails pour le matériel (`equipment_detail_page.dart`).
- Ajout d'un `SelectionModeMixin` pour gérer la sélection multiple dans les pages de gestion.
- Dialogues réutilisables pour l'affichage de QR codes (`QRCodeDialog`) et la sélection de format d'impression (`QRCodeFormatSelectorDialog`).
- Ajout d'un bouton "Gérer les boîtes" sur la page de gestion du matériel.
**Refactorisation :**
- L' `IdGenerator` a été déplacé dans le répertoire `utils` et étendu pour gérer les containers.
- Mise à jour de nombreuses dépendances `pubspec.yaml` vers des versions plus récentes.
- Séparation de la logique d'affichage des containers et du matériel dans des widgets dédiés (`ContainerHeaderCard`, `EquipmentParentContainers`, etc.).
This commit is contained in:
881
em2rp/lib/views/equipment_detail_page.dart
Normal file
881
em2rp/lib/views/equipment_detail_page.dart
Normal file
@@ -0,0 +1,881 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/maintenance_model.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/services/equipment_service.dart';
|
||||
import 'package:em2rp/services/qr_code_service.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/utils/permission_gate.dart';
|
||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||
import 'package:em2rp/views/equipment_form_page.dart';
|
||||
import 'package:em2rp/views/widgets/equipment/equipment_parent_containers.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
|
||||
class EquipmentDetailPage extends StatefulWidget {
|
||||
final EquipmentModel equipment;
|
||||
|
||||
const EquipmentDetailPage({super.key, required this.equipment});
|
||||
|
||||
@override
|
||||
State<EquipmentDetailPage> createState() => _EquipmentDetailPageState();
|
||||
}
|
||||
|
||||
class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
||||
final EquipmentService _equipmentService = EquipmentService();
|
||||
List<MaintenanceModel> _maintenances = [];
|
||||
bool _isLoadingMaintenances = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMaintenances();
|
||||
}
|
||||
|
||||
Future<void> _loadMaintenances() async {
|
||||
try {
|
||||
final maintenances = await _equipmentService.getMaintenancesForEquipment(widget.equipment.id);
|
||||
setState(() {
|
||||
_maintenances = maintenances;
|
||||
_isLoadingMaintenances = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingMaintenances = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 800;
|
||||
final userProvider = Provider.of<LocalUserProvider>(context);
|
||||
final hasManagePermission = userProvider.hasPermission('manage_equipment');
|
||||
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: widget.equipment.id,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code),
|
||||
tooltip: 'Générer QR Code',
|
||||
onPressed: _showQRCode,
|
||||
),
|
||||
if (hasManagePermission)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: 'Modifier',
|
||||
onPressed: _editEquipment,
|
||||
),
|
||||
if (hasManagePermission)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
tooltip: 'Supprimer',
|
||||
onPressed: _deleteEquipment,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(isMobile ? 16 : 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 24),
|
||||
_buildMainInfoSection(),
|
||||
const SizedBox(height: 24),
|
||||
if (hasManagePermission) ...[
|
||||
_buildPriceSection(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
if (widget.equipment.category == EquipmentCategory.consumable ||
|
||||
widget.equipment.category == EquipmentCategory.cable) ...[
|
||||
_buildQuantitySection(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
if (widget.equipment.parentBoxIds.isNotEmpty) ...[
|
||||
EquipmentParentContainers(
|
||||
parentBoxIds: widget.equipment.parentBoxIds,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
_buildDatesSection(),
|
||||
const SizedBox(height: 24),
|
||||
if (widget.equipment.notes != null && widget.equipment.notes!.isNotEmpty) ...[
|
||||
_buildNotesSection(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
_buildMaintenanceHistorySection(hasManagePermission),
|
||||
const SizedBox(height: 24),
|
||||
_buildAssociatedEventsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.rouge, AppColors.rouge.withValues(alpha: 0.8)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.rouge.withValues(alpha: 0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.white,
|
||||
radius: 30,
|
||||
child: Icon(
|
||||
_getCategoryIcon(widget.equipment.category),
|
||||
color: AppColors.rouge,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.equipment.id,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim().isNotEmpty
|
||||
? '${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim()
|
||||
: 'Marque/Modèle non défini',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.equipment.category != EquipmentCategory.consumable &&
|
||||
widget.equipment.category != EquipmentCategory.cable)
|
||||
_buildStatusBadge(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge() {
|
||||
final statusInfo = _getStatusInfo(widget.equipment.status);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: statusInfo.$2,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
statusInfo.$1,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: statusInfo.$2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainInfoSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Informations principales',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow('Catégorie', _getCategoryName(widget.equipment.category)),
|
||||
if (widget.equipment.brand != null && widget.equipment.brand!.isNotEmpty)
|
||||
_buildInfoRow('Marque', widget.equipment.brand!),
|
||||
if (widget.equipment.model != null && widget.equipment.model!.isNotEmpty)
|
||||
_buildInfoRow('Modèle', widget.equipment.model!),
|
||||
if (widget.equipment.category != EquipmentCategory.consumable &&
|
||||
widget.equipment.category != EquipmentCategory.cable)
|
||||
_buildInfoRow('Statut', _getStatusInfo(widget.equipment.status).$1),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPriceSection() {
|
||||
final hasPrices = widget.equipment.purchasePrice != null || widget.equipment.rentalPrice != null;
|
||||
|
||||
if (!hasPrices) return const SizedBox.shrink();
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.euro, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Prix',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
if (widget.equipment.purchasePrice != null)
|
||||
_buildInfoRow(
|
||||
'Prix d\'achat',
|
||||
'${widget.equipment.purchasePrice!.toStringAsFixed(2)} €',
|
||||
),
|
||||
if (widget.equipment.rentalPrice != null)
|
||||
_buildInfoRow(
|
||||
'Prix de location',
|
||||
'${widget.equipment.rentalPrice!.toStringAsFixed(2)} €/jour',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantitySection() {
|
||||
final availableQty = widget.equipment.availableQuantity ?? 0;
|
||||
final totalQty = widget.equipment.totalQuantity ?? 0;
|
||||
final criticalThreshold = widget.equipment.criticalThreshold ?? 0;
|
||||
final isCritical = criticalThreshold > 0 && availableQty <= criticalThreshold;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: isCritical ? Colors.red.shade50 : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isCritical ? Icons.warning : Icons.inventory,
|
||||
color: isCritical ? Colors.red : AppColors.rouge,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Quantités',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isCritical ? Colors.red : null,
|
||||
),
|
||||
),
|
||||
if (isCritical) ...[
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'STOCK CRITIQUE',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildInfoRow(
|
||||
'Quantité disponible',
|
||||
availableQty.toString(),
|
||||
valueColor: isCritical ? Colors.red : null,
|
||||
valueWeight: isCritical ? FontWeight.bold : null,
|
||||
),
|
||||
_buildInfoRow('Quantité totale', totalQty.toString()),
|
||||
if (criticalThreshold > 0)
|
||||
_buildInfoRow('Seuil critique', criticalThreshold.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildDatesSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Dates',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
if (widget.equipment.purchaseDate != null)
|
||||
_buildInfoRow(
|
||||
'Date d\'achat',
|
||||
DateFormat('dd/MM/yyyy').format(widget.equipment.purchaseDate!),
|
||||
),
|
||||
if (widget.equipment.lastMaintenanceDate != null)
|
||||
_buildInfoRow(
|
||||
'Dernière maintenance',
|
||||
DateFormat('dd/MM/yyyy').format(widget.equipment.lastMaintenanceDate!),
|
||||
),
|
||||
if (widget.equipment.nextMaintenanceDate != null)
|
||||
_buildInfoRow(
|
||||
'Prochaine maintenance',
|
||||
DateFormat('dd/MM/yyyy').format(widget.equipment.nextMaintenanceDate!),
|
||||
valueColor: widget.equipment.nextMaintenanceDate!.isBefore(DateTime.now())
|
||||
? Colors.red
|
||||
: null,
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Créé le',
|
||||
DateFormat('dd/MM/yyyy à HH:mm').format(widget.equipment.createdAt),
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Modifié le',
|
||||
DateFormat('dd/MM/yyyy à HH:mm').format(widget.equipment.updatedAt),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotesSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.notes, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Notes',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
widget.equipment.notes!,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaintenanceHistorySection(bool hasManagePermission) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.build, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Historique des maintenances',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
if (_isLoadingMaintenances)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else if (_maintenances.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Aucune maintenance enregistrée',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _maintenances.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final maintenance = _maintenances[index];
|
||||
return _buildMaintenanceItem(maintenance, hasManagePermission);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMaintenanceItem(MaintenanceModel maintenance, bool showCost) {
|
||||
final isCompleted = maintenance.completedDate != null;
|
||||
final typeInfo = _getMaintenanceTypeInfo(maintenance.type);
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: isCompleted ? Colors.green.withValues(alpha: 0.2) : Colors.orange.withValues(alpha: 0.2),
|
||||
child: Icon(
|
||||
isCompleted ? Icons.check_circle : Icons.schedule,
|
||||
color: isCompleted ? Colors.green : Colors.orange,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
maintenance.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(typeInfo.$2, size: 16, color: Colors.grey[600]),
|
||||
const SizedBox(width: 4),
|
||||
Text(typeInfo.$1, style: TextStyle(color: Colors.grey[600], fontSize: 12)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isCompleted
|
||||
? 'Effectuée le ${DateFormat('dd/MM/yyyy').format(maintenance.completedDate!)}'
|
||||
: 'Planifiée le ${DateFormat('dd/MM/yyyy').format(maintenance.scheduledDate)}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||
),
|
||||
if (showCost && maintenance.cost != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Coût: ${maintenance.cost!.toStringAsFixed(2)} €',
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssociatedEventsSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.event, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Événements associés',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Fonctionnalité à implémenter',
|
||||
style: TextStyle(color: Colors.grey, fontStyle: FontStyle.italic),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(
|
||||
String label,
|
||||
String value, {
|
||||
Color? valueColor,
|
||||
FontWeight? valueWeight,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: valueColor,
|
||||
fontWeight: valueWeight ?? FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showQRCode() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.qr_code, color: AppColors.rouge, size: 32),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'QR Code - ${widget.equipment.id}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: widget.equipment.id,
|
||||
version: QrVersions.auto,
|
||||
size: 300,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.equipment.id,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim(),
|
||||
style: TextStyle(color: Colors.grey[700]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _exportQRCode(),
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('Télécharger PNG'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.rouge,
|
||||
minimumSize: const Size(0, 48),
|
||||
),
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
label: const Text(
|
||||
'Fermer',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportQRCode() async {
|
||||
try {
|
||||
final qrImage = await QRCodeService.generateQRCode(
|
||||
widget.equipment.id,
|
||||
size: 1024,
|
||||
useCache: false,
|
||||
);
|
||||
|
||||
await Printing.sharePdf(
|
||||
bytes: qrImage,
|
||||
filename: 'QRCode_${widget.equipment.id}.png',
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('QR Code exporté avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur lors de l\'export: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _editEquipment() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EquipmentFormPage(equipment: widget.equipment),
|
||||
),
|
||||
).then((_) {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
}
|
||||
|
||||
void _deleteEquipment() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text(
|
||||
'Voulez-vous vraiment supprimer "${widget.equipment.id}" ?\n\nCette action est irréversible.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
await context
|
||||
.read<EquipmentProvider>()
|
||||
.deleteEquipment(widget.equipment.id);
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Équipement supprimé avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getCategoryIcon(EquipmentCategory category) {
|
||||
switch (category) {
|
||||
case EquipmentCategory.lighting:
|
||||
return Icons.light_mode;
|
||||
case EquipmentCategory.sound:
|
||||
return Icons.volume_up;
|
||||
case EquipmentCategory.video:
|
||||
return Icons.videocam;
|
||||
case EquipmentCategory.effect:
|
||||
return Icons.auto_awesome;
|
||||
case EquipmentCategory.structure:
|
||||
return Icons.construction;
|
||||
case EquipmentCategory.consumable:
|
||||
return Icons.inventory_2;
|
||||
case EquipmentCategory.cable:
|
||||
return Icons.cable;
|
||||
case EquipmentCategory.other:
|
||||
return Icons.more_horiz;
|
||||
}
|
||||
}
|
||||
|
||||
String _getCategoryName(EquipmentCategory category) {
|
||||
switch (category) {
|
||||
case EquipmentCategory.lighting:
|
||||
return 'Lumière';
|
||||
case EquipmentCategory.sound:
|
||||
return 'Son';
|
||||
case EquipmentCategory.video:
|
||||
return 'Vidéo';
|
||||
case EquipmentCategory.effect:
|
||||
return 'Effets';
|
||||
case EquipmentCategory.structure:
|
||||
return 'Structure';
|
||||
case EquipmentCategory.consumable:
|
||||
return 'Consommable';
|
||||
case EquipmentCategory.cable:
|
||||
return 'Câble';
|
||||
case EquipmentCategory.other:
|
||||
return 'Autre';
|
||||
}
|
||||
}
|
||||
|
||||
(String, Color) _getStatusInfo(EquipmentStatus status) {
|
||||
switch (status) {
|
||||
case EquipmentStatus.available:
|
||||
return ('Disponible', Colors.green);
|
||||
case EquipmentStatus.inUse:
|
||||
return ('En prestation', Colors.blue);
|
||||
case EquipmentStatus.rented:
|
||||
return ('Loué', Colors.orange);
|
||||
case EquipmentStatus.lost:
|
||||
return ('Perdu', Colors.red);
|
||||
case EquipmentStatus.outOfService:
|
||||
return ('HS', Colors.red[900]!);
|
||||
case EquipmentStatus.maintenance:
|
||||
return ('Maintenance', Colors.amber);
|
||||
}
|
||||
}
|
||||
|
||||
(String, IconData) _getMaintenanceTypeInfo(MaintenanceType type) {
|
||||
switch (type) {
|
||||
case MaintenanceType.preventive:
|
||||
return ('Préventive', Icons.schedule);
|
||||
case MaintenanceType.corrective:
|
||||
return ('Corrective', Icons.build);
|
||||
case MaintenanceType.inspection:
|
||||
return ('Inspection', Icons.search);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user