441 lines
16 KiB
Dart
441 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../core/constants/app_constants.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../data/models/weapon.dart';
|
|
import '../../data/models/maintenance.dart';
|
|
import '../../data/repositories/session_repository.dart';
|
|
|
|
class WeaponDetailScreen extends StatefulWidget {
|
|
final Weapon weapon;
|
|
const WeaponDetailScreen({super.key, required this.weapon});
|
|
|
|
@override
|
|
State<WeaponDetailScreen> createState() => _WeaponDetailScreenState();
|
|
}
|
|
|
|
class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|
late Weapon _weapon;
|
|
List<MaintenanceEntry> _maintenance = [];
|
|
int _totalRounds = 0;
|
|
int _sessionCount = 0;
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_weapon = widget.weapon;
|
|
_loadData();
|
|
}
|
|
|
|
Future<void> _loadData() async {
|
|
final repository = context.read<SessionRepository>();
|
|
final history = await repository.getMaintenanceHistory(_weapon.id);
|
|
final rounds = await repository.getRoundsFiredForWeapon(_weapon.id);
|
|
final sessions = await repository.getSessionCountForWeapon(_weapon.id);
|
|
if (mounted) {
|
|
setState(() {
|
|
_maintenance = history;
|
|
_totalRounds = rounds;
|
|
_sessionCount = sessions;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(_weapon.customName ?? _weapon.name),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.edit),
|
|
onPressed: () => _showEditWeaponDialog(context),
|
|
),
|
|
],
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildStatsSection(),
|
|
const SizedBox(height: 24),
|
|
_buildInfoCard(),
|
|
const SizedBox(height: 24),
|
|
_buildOptionsCard(),
|
|
const SizedBox(height: 24),
|
|
_buildHistorySection(),
|
|
],
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton.extended(
|
|
onPressed: () => _showAddMaintenanceDialog(context),
|
|
label: const Text('Entretien'),
|
|
icon: const Icon(Icons.build),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatsSection() {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
'Total Tirs',
|
|
_totalRounds.toString(),
|
|
Icons.ads_click,
|
|
AppTheme.primaryColor,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: _buildStatCard(
|
|
'Sessions',
|
|
_sessionCount.toString(),
|
|
Icons.history,
|
|
Colors.orange,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
Icon(icon, color: color),
|
|
const SizedBox(height: 8),
|
|
Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
|
Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoCard() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Informations techniques', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
|
const Divider(),
|
|
_buildInfoRow('Modèle', _weapon.name),
|
|
_buildInfoRow('Type', _weapon.type.displayName),
|
|
_buildInfoRow('Calibre', _weapon.caliber),
|
|
_buildInfoRow('Chargeurs', '${_weapon.magazineCount} x ${_weapon.magazineCapacity} coups'),
|
|
if (_weapon.notes != null && _weapon.notes!.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
const Text('Notes:', style: TextStyle(fontWeight: FontWeight.w500)),
|
|
Text(_weapon.notes!, style: const TextStyle(color: Colors.grey)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOptionsCard() {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Options & Personnalisation', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
|
const Divider(),
|
|
_buildInfoRow('Optique / Lunette', _weapon.optic ?? 'Mire fer'),
|
|
_buildInfoRow('Modérateur / Silencieux', _weapon.silencer ?? 'Aucun'),
|
|
_buildInfoRow('Détente', _weapon.trigger ?? 'Standard'),
|
|
if (_weapon.customName != null)
|
|
_buildInfoRow('Surnom', _weapon.customName!),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label, style: const TextStyle(color: Colors.grey)),
|
|
Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHistorySection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Historique d\'entretien', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
|
const SizedBox(height: 8),
|
|
if (_maintenance.isEmpty)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 24.0),
|
|
child: Center(child: Text('Aucun entretien enregistré', style: TextStyle(color: Colors.grey))),
|
|
)
|
|
else
|
|
ListView.builder(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: _maintenance.length,
|
|
itemBuilder: (context, index) {
|
|
final entry = _maintenance[index];
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
child: ListTile(
|
|
leading: CircleAvatar(
|
|
backgroundColor: _getMaintenanceColor(entry.type).withValues(alpha: 0.1),
|
|
child: Icon(_getMaintenanceIcon(entry.type), color: _getMaintenanceColor(entry.type), size: 20),
|
|
),
|
|
title: Text(entry.type.displayName),
|
|
subtitle: Text(entry.description),
|
|
trailing: Text(DateFormat('dd/MM/yy').format(entry.date), style: const TextStyle(fontSize: 12)),
|
|
onLongPress: () => _confirmDeleteMaintenance(context, entry),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Color _getMaintenanceColor(MaintenanceType type) {
|
|
switch (type) {
|
|
case MaintenanceType.cleaning: return Colors.blue;
|
|
case MaintenanceType.repair: return Colors.red;
|
|
case MaintenanceType.inspection: return Colors.green;
|
|
case MaintenanceType.upgrade: return Colors.purple;
|
|
case MaintenanceType.other: return Colors.grey;
|
|
}
|
|
}
|
|
|
|
IconData _getMaintenanceIcon(MaintenanceType type) {
|
|
switch (type) {
|
|
case MaintenanceType.cleaning: return Icons.opacity;
|
|
case MaintenanceType.repair: return Icons.build;
|
|
case MaintenanceType.inspection: return Icons.visibility;
|
|
case MaintenanceType.upgrade: return Icons.trending_up;
|
|
case MaintenanceType.other: return Icons.more_horiz;
|
|
}
|
|
}
|
|
|
|
void _showEditWeaponDialog(BuildContext context) async {
|
|
final nameController = TextEditingController(text: _weapon.name);
|
|
final caliberController = TextEditingController(text: _weapon.caliber);
|
|
final magCountController = TextEditingController(text: _weapon.magazineCount.toString());
|
|
final magCapController = TextEditingController(text: _weapon.magazineCapacity.toString());
|
|
final notesController = TextEditingController(text: _weapon.notes);
|
|
final opticController = TextEditingController(text: _weapon.optic);
|
|
final silencerController = TextEditingController(text: _weapon.silencer);
|
|
final triggerController = TextEditingController(text: _weapon.trigger);
|
|
final customNameController = TextEditingController(text: _weapon.customName);
|
|
WeaponType selectedType = _weapon.type;
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => StatefulBuilder(
|
|
builder: (context, setState) => AlertDialog(
|
|
title: const Text('Modifier l\'arme'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
controller: nameController,
|
|
decoration: const InputDecoration(labelText: 'Modèle', hintText: 'ex: Glock 17'),
|
|
),
|
|
TextField(
|
|
controller: customNameController,
|
|
decoration: const InputDecoration(labelText: 'Surnom / Custom Name', hintText: 'ex: Mon Glock de Compète'),
|
|
),
|
|
DropdownButtonFormField<WeaponType>(
|
|
value: selectedType,
|
|
decoration: const InputDecoration(labelText: 'Type'),
|
|
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
|
onChanged: (v) => setState(() => selectedType = v!),
|
|
),
|
|
TextField(
|
|
controller: caliberController,
|
|
decoration: const InputDecoration(labelText: 'Calibre'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Équipement & Options', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
TextField(
|
|
controller: opticController,
|
|
decoration: const InputDecoration(labelText: 'Optique / Lunette', hintText: 'ex: Holosun 507C'),
|
|
),
|
|
TextField(
|
|
controller: silencerController,
|
|
decoration: const InputDecoration(labelText: 'Modérateur / Silencieux'),
|
|
),
|
|
TextField(
|
|
controller: triggerController,
|
|
decoration: const InputDecoration(labelText: 'Détente / Trigger'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Logistique', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: magCountController,
|
|
decoration: const InputDecoration(labelText: 'Chargeurs'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: magCapController,
|
|
decoration: const InputDecoration(labelText: 'Capacité'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
TextField(
|
|
controller: notesController,
|
|
decoration: const InputDecoration(labelText: 'Notes'),
|
|
maxLines: 2,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
|
|
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Enregistrer')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
if (result == true) {
|
|
final updatedWeapon = _weapon.copyWith(
|
|
name: nameController.text,
|
|
type: selectedType,
|
|
caliber: caliberController.text,
|
|
magazineCount: int.tryParse(magCountController.text) ?? 1,
|
|
magazineCapacity: int.tryParse(magCapController.text) ?? 10,
|
|
notes: notesController.text,
|
|
optic: opticController.text.isEmpty ? null : opticController.text,
|
|
silencer: silencerController.text.isEmpty ? null : silencerController.text,
|
|
trigger: triggerController.text.isEmpty ? null : triggerController.text,
|
|
customName: customNameController.text.isEmpty ? null : customNameController.text,
|
|
);
|
|
|
|
final repository = context.read<SessionRepository>();
|
|
await repository.updateWeapon(updatedWeapon);
|
|
setState(() {
|
|
_weapon = updatedWeapon;
|
|
});
|
|
}
|
|
}
|
|
|
|
void _showAddMaintenanceDialog(BuildContext context) async {
|
|
final descController = TextEditingController();
|
|
MaintenanceType selectedType = MaintenanceType.cleaning;
|
|
DateTime selectedDate = DateTime.now();
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => StatefulBuilder(
|
|
builder: (context, setState) => AlertDialog(
|
|
title: const Text('Ajouter un entretien'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
DropdownButtonFormField<MaintenanceType>(
|
|
value: selectedType,
|
|
decoration: const InputDecoration(labelText: 'Type d\'intervention'),
|
|
items: MaintenanceType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
|
onChanged: (v) => setState(() => selectedType = v!),
|
|
),
|
|
TextField(
|
|
controller: descController,
|
|
decoration: const InputDecoration(labelText: 'Description', hintText: 'ex: Nettoyage complet après séance'),
|
|
maxLines: 2,
|
|
),
|
|
const SizedBox(height: 8),
|
|
InkWell(
|
|
onTap: () async {
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: selectedDate,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime.now(),
|
|
);
|
|
if (picked != null) {
|
|
setState(() => selectedDate = picked);
|
|
}
|
|
},
|
|
child: InputDecorator(
|
|
decoration: const InputDecoration(
|
|
labelText: 'Date',
|
|
suffixIcon: Icon(Icons.calendar_today, size: 18),
|
|
),
|
|
child: Text(DateFormat('dd/MM/yyyy').format(selectedDate)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
|
|
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Ajouter')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
if (result == true && descController.text.isNotEmpty) {
|
|
final repository = context.read<SessionRepository>();
|
|
await repository.addMaintenanceEntry(
|
|
weaponId: _weapon.id,
|
|
type: selectedType,
|
|
description: descController.text,
|
|
roundsSinceLast: _totalRounds,
|
|
date: selectedDate,
|
|
);
|
|
_loadData();
|
|
}
|
|
}
|
|
|
|
void _confirmDeleteMaintenance(BuildContext context, MaintenanceEntry entry) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Supprimer'),
|
|
content: const Text('Supprimer cette entrée de l\'historique ?'),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Supprimer', style: TextStyle(color: AppTheme.errorColor)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true) {
|
|
final repository = context.read<SessionRepository>();
|
|
await repository.deleteMaintenanceEntry(entry.id);
|
|
_loadData();
|
|
}
|
|
}
|
|
}
|