h-1.flutter.0/lib/screens/dashboard_screen.dart
2026-03-04 14:55:40 +09:00

334 lines
11 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import '../config/app_config.dart';
import '../models/invoice_models.dart';
import '../modules/feature_module.dart';
import '../modules/module_registry.dart';
import '../services/app_settings_repository.dart';
import '../services/customer_repository.dart';
import '../services/location_service.dart';
import '../widgets/slide_to_unlock.dart';
import 'customer_master_screen.dart';
import 'invoice_detail_page.dart';
import 'invoice_history_screen.dart';
import 'invoice_input_screen.dart';
import 'master_hub_page.dart';
import 'sales_orders_screen.dart';
import 'product_master_screen.dart';
import 'settings_screen.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
final _repo = AppSettingsRepository();
final _moduleRegistry = ModuleRegistry.instance;
bool _loading = true;
bool _statusEnabled = true;
String _statusText = '工事中';
List<DashboardMenuItem> _menu = [];
bool _historyUnlocked = false;
List<ModuleDashboardCard> _moduleCards = [];
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final statusEnabled = await _repo.getDashboardStatusEnabled();
final statusText = await _repo.getDashboardStatusText();
final rawMenu = await _repo.getDashboardMenu();
final moduleCards = _moduleRegistry.enabledCards.toList();
final menu = rawMenu.toList();
_ensureModuleCardsInjected(menu, moduleCards);
final unlocked = await _repo.getDashboardHistoryUnlocked();
setState(() {
_statusEnabled = statusEnabled;
_statusText = statusText;
_menu = menu;
_loading = false;
_historyUnlocked = unlocked;
_moduleCards = moduleCards;
});
}
Set<String> _enabledRouteSet() {
final routes = {...AppConfig.enabledRoutes};
for (final card in _moduleRegistry.enabledCards) {
routes.add(card.route);
}
return routes;
}
void _ensureModuleCardsInjected(List<DashboardMenuItem> menu, List<ModuleDashboardCard> cards) {
final existingRoutes = menu.map((m) => m.route).toSet();
for (final card in cards) {
if (!existingRoutes.contains(card.route)) {
menu.add(DashboardMenuItem(id: card.id, title: card.title, route: card.route, iconName: card.iconName));
}
}
}
ModuleDashboardCard? _moduleCardForRoute(String route) {
for (final card in _moduleCards) {
if (card.route == route) {
return card;
}
}
return null;
}
void _navigate(DashboardMenuItem item) async {
Widget? target;
final enabledRoutes = _enabledRouteSet();
if (!enabledRoutes.contains(item.route)) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('この機能は現在ご利用いただけません')));
return;
}
final moduleCard = _moduleCardForRoute(item.route);
if (moduleCard != null) {
if (moduleCard.requiresUnlock && !_historyUnlocked) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('ロックを解除してください')));
return;
}
await moduleCard.onTap(context);
return;
}
switch (item.route) {
case 'invoice_history':
if (!_historyUnlocked) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('ロックを解除してください')));
return;
}
target = const InvoiceHistoryScreen(initialUnlocked: true);
break;
case 'invoice_input':
target = InvoiceInputForm(
onInvoiceGenerated: (invoice, path) async {
final locationService = LocationService();
final pos = await locationService.getCurrentLocation();
if (pos != null) {
final customerRepo = CustomerRepository();
await customerRepo.addGpsHistory(invoice.customer.id, pos.latitude, pos.longitude);
}
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(builder: (_) => InvoiceDetailPage(invoice: invoice)),
);
},
initialDocumentType: DocumentType.invoice,
);
break;
case 'customer_master':
target = const CustomerMasterScreen();
break;
case 'product_master':
target = const ProductMasterScreen();
break;
case 'master_hub':
target = const MasterHubPage();
break;
case 'settings':
target = const SettingsScreen();
break;
case 'sales_operations':
target = const SalesOrdersScreen();
break;
default:
target = const InvoiceHistoryScreen();
break;
}
await Navigator.push(context, MaterialPageRoute(builder: (_) => target!));
if (item.route == 'settings') {
await _load();
}
}
Widget _tile(DashboardMenuItem item) {
return GestureDetector(
onTap: () => _navigate(item),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(color: Colors.black12, blurRadius: 6, offset: const Offset(0, 2)),
],
),
padding: const EdgeInsets.all(16),
child: Row(
children: [
_leading(item),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 4),
Text(_routeLabel(item.route), style: const TextStyle(color: Colors.grey)),
],
),
),
const Icon(Icons.chevron_right, color: Colors.grey),
],
),
),
);
}
Widget _leading(DashboardMenuItem item) {
if (item.customIconPath != null && File(item.customIconPath!).existsSync()) {
return CircleAvatar(backgroundImage: FileImage(File(item.customIconPath!)), radius: 22);
}
return CircleAvatar(
radius: 22,
backgroundColor: Colors.indigo.shade50,
foregroundColor: Colors.indigo.shade700,
child: Icon(_iconForName(item.iconName ?? 'list_alt')),
);
}
IconData _iconForName(String name) {
return kIconsMap[name] ?? Icons.apps;
}
String _routeLabel(String route) {
final moduleCard = _moduleCardForRoute(route);
if (moduleCard != null) {
return moduleCard.description;
}
switch (route) {
case 'invoice_history':
return 'A2:伝票一覧';
case 'invoice_input':
return 'A1:伝票入力';
case 'customer_master':
return 'C1:顧客マスター';
case 'product_master':
return 'P1:商品マスター';
case 'master_hub':
return 'M1:マスター管理';
case 'settings':
return 'S1:設定';
case 'sales_operations':
return 'B1:販売オペレーション';
default:
return route;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('D1:ダッシュボード'),
actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
await Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen()));
await _load();
},
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _load,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _historyUnlocked
? Row(
children: [
const Icon(Icons.lock_open, color: Colors.green),
const SizedBox(width: 8),
const Expanded(child: Text('A2ロック解除済')),
OutlinedButton.icon(
onPressed: () async {
setState(() => _historyUnlocked = false);
await _repo.setDashboardHistoryUnlocked(false);
},
icon: const Icon(Icons.lock),
label: const Text('再ロック'),
),
],
)
: SlideToUnlock(
isLocked: !_historyUnlocked,
lockedText: 'スライドでロック解除 (A2)',
unlockedText: 'A2解除済',
onUnlocked: () async {
setState(() => _historyUnlocked = true);
await _repo.setDashboardHistoryUnlocked(true);
},
),
),
if (_statusEnabled)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: Row(
children: [
const Icon(Icons.info_outline, color: Colors.orange),
const SizedBox(width: 12),
Expanded(child: Text(_statusText, style: const TextStyle(fontWeight: FontWeight.bold))),
],
),
),
..._menu.map((e) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _tile(e),
)),
if (_menu.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text('メニューが未設定です。設定画面から追加してください。'),
),
)
],
),
),
);
}
}
// fallback icon map for dashboard
const Map<String, IconData> kIconsMap = {
'list_alt': Icons.list_alt,
'edit_note': Icons.edit_note,
'history': Icons.history,
'settings': Icons.settings,
'invoice': Icons.receipt_long,
'customer': Icons.people,
'product': Icons.inventory_2,
'menu': Icons.menu,
'analytics': Icons.analytics,
'map': Icons.map,
'master': Icons.storage,
'qr': Icons.qr_code,
'camera': Icons.camera_alt,
'contact': Icons.contact_mail,
};