feat: implement base architecture and core repositories for weapon tracking and target analysis functionality
This commit is contained in:
414
lib/features/garage/weapon_detail_screen.dart
Normal file
414
lib/features/garage/weapon_detail_screen.dart
Normal file
@@ -0,0 +1,414 @@
|
||||
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;
|
||||
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);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_maintenance = history;
|
||||
_totalRounds = rounds;
|
||||
_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',
|
||||
'N/A',
|
||||
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;
|
||||
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
);
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user