part of '../../app/app.dart'; class ExpertiseScreen extends StatefulWidget { const ExpertiseScreen({ super.key, required this.token, required this.baseUrl, required this.id, }); final String token; final String baseUrl; final int id; @override State createState() => _ExpertiseScreenState(); } class _ExpertiseScreenState extends State { late final ApiClient _api; late Future> _future; final Map _textControllers = {}; final Map _selectValues = {}; final Map> _multiValues = {}; final Map _staffValues = {}; final Map _wizardSteps = {}; String _currentDlp = ''; bool _criticalValue = false; bool _saving = false; @override void initState() { super.initState(); _api = ApiClient(baseUrl: widget.baseUrl, token: widget.token); _future = _load(); } @override void dispose() { for (final controller in _textControllers.values) { controller.dispose(); } super.dispose(); } Future> _load() { return _api.get('api/mobile/examinations/${widget.id}/expertise'); } void _reload() { setState(() { _future = _load(); }); } void _prepareFields(Map data) { final dlp = data['dlp']?.toString() ?? ''; if (_currentDlp == dlp && _textControllers.isNotEmpty) { return; } _currentDlp = dlp; for (final controller in _textControllers.values) { controller.dispose(); } _textControllers.clear(); _selectValues.clear(); _multiValues.clear(); _staffValues.clear(); final components = asMap(data['components']); final item = asMap(data['item']); final user = asMap(data['user']); _staffValues['analis'] = (item['analis'] ?? user['id'] ?? '0').toString(); _staffValues['ppds3'] = (item['ppds3'] ?? user['id'] ?? '0').toString(); _staffValues['dokter'] = (item['dokter_id'] ?? user['id'] ?? '0') .toString(); _criticalValue = data['is_critical'] == true; for (final rawField in asList(data['fields'])) { final field = asMap(rawField); final name = field['name']?.toString() ?? ''; final type = field['type']?.toString() ?? 'text'; final value = components[name]; if (type == 'select') { _selectValues[name] = value?.toString() ?? ''; } else if (type == 'multiselect') { _multiValues[name] = value is List ? value.map((item) => item.toString()).toSet() : {}; } else { _textControllers[name] = TextEditingController( text: value?.toString() ?? '', ); } } for (final name in kAntibioticMediaRowFields) { _textControllers[name] = TextEditingController( text: components[name]?.toString() ?? '', ); } for (final name in kToolAntibioticRowFields) { _textControllers[name] = TextEditingController( text: components[name]?.toString() ?? '', ); } } Future _setTemplate(String dlp) async { setState(() => _saving = true); try { await _api.post( 'api/mobile/examinations/${widget.id}/expertise/template', {'dlp': dlp}, ); if (!mounted) return; _showMessage('Template $dlp dipilih.'); setState(() { _future = _load(); }); } on ApiException catch (error) { _showMessage(error.message); } catch (_) { _showMessage('Template tidak bisa dipilih.'); } finally { if (mounted) { setState(() => _saving = false); } } } Future _save(String action) async { if (action == 'statuspewarnaanlsg') { _wizardSteps[_currentDlp] = 1; } setState(() => _saving = true); final fields = {}; for (final entry in _textControllers.entries) { fields[entry.key] = entry.value.text.trim(); } for (final entry in _selectValues.entries) { fields[entry.key] = entry.value; } for (final entry in _multiValues.entries) { fields[entry.key] = entry.value.toList(); } Map data; try { data = await _api.post('api/mobile/examinations/${widget.id}/expertise', { 'dlp': _currentDlp, 'action': action, 'fields': fields, 'keterangan': fields['keterangan']?.toString() ?? '', 'analis': _staffValues['analis'] ?? '0', 'ppds3': _staffValues['ppds3'] ?? '0', 'dokter': _staffValues['dokter'] ?? '0', 'nilai_kritis': _criticalValue ? '1' : '0', }); } on ApiException catch (error) { _showMessage(error.message); return; } catch (_) { _showMessage('Expertise tidak bisa disimpan.'); return; } finally { if (mounted) { setState(() => _saving = false); } } if (!mounted) return; _showMessage(data['message']?.toString() ?? 'Expertise tersimpan.'); setState(() { _future = _load(); }); } int _wizardStep(String dlp) => _wizardSteps[dlp] ?? 0; void _setWizardStep(String dlp, int step) { _wizardSteps[dlp] = step; } void _showMessage(String message) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const LoadingScreen(); } if (snapshot.hasError) { return ErrorView( message: _message(snapshot.error), onRetry: _reload, ); } final data = snapshot.data!; final item = asMap(data['item']); final user = asMap(data['user']); _prepareFields(data); final isSupervisor = _isSupervisor(user['previlage']?.toString()); final summaryCard = _ExpertiseSummaryCard( item: item, dlp: _currentDlp, ); final expertiseContent = _currentDlp.isEmpty ? TemplatePicker( templates: asList(data['templates']), saving: _saving, onUse: _setTemplate, ) : _currentDlp == 'CCI' ? CciExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), optionSets: asMap(data['option_sets']), textControllers: _textControllers, selectValues: _selectValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : _currentDlp == 'Kultur' ? KulturExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), optionSets: asMap(data['option_sets']), textControllers: _textControllers, selectValues: _selectValues, multiValues: _multiValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : _currentDlp == 'Pewarna Langsung' ? PewarnaanLangsungExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), optionSets: asMap(data['option_sets']), textControllers: _textControllers, selectValues: _selectValues, multiValues: _multiValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : _currentDlp == 'Viral Load' ? ViralLoadExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), optionSets: asMap(data['option_sets']), textControllers: _textControllers, selectValues: _selectValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : _currentDlp == 'IgM IgG Leptospira' ? LeptospiraExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), textControllers: _textControllers, selectValues: _selectValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : _currentDlp == 'PCR COVID' ? CovidExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), textControllers: _textControllers, selectValues: _selectValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : _currentDlp == 'TBC' ? TbcExpertiseWizard( item: item, user: user, staffOptions: asMap(data['staff_options']), textControllers: _textControllers, selectValues: _selectValues, multiValues: _multiValues, staffValues: _staffValues, criticalValue: _criticalValue, isSupervisor: isSupervisor, saving: _saving, onCriticalChanged: (value) => setState(() => _criticalValue = value), onChanged: () => setState(() {}), onSave: _save, initialStep: _wizardStep(_currentDlp), onStepChanged: (step) => _setWizardStep(_currentDlp, step), ) : SingleChildScrollView( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PatientStaffPanel( item: item, user: user, staffOptions: asMap(data['staff_options']), staffValues: _staffValues, onChanged: () => setState(() {}), ), const SizedBox(height: 12), ExpertiseForm( dlp: _currentDlp, fields: asList(data['fields']), textControllers: _textControllers, selectValues: _selectValues, multiValues: _multiValues, onChanged: () => setState(() {}), ), const SizedBox(height: 12), ExpertiseActions( isSupervisor: isSupervisor, saving: _saving, onSave: _save, ), ], ), ); if (_currentDlp.isEmpty) { return ListView( padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), children: [ SafeArea( bottom: false, child: _ExpertiseHeader( title: 'Expertise', subtitle: 'Pilih Template', saving: _saving, onRefresh: _reload, ), ), const SizedBox(height: 22), summaryCard, const SizedBox(height: 14), expertiseContent, ], ); } return Padding( padding: const EdgeInsets.fromLTRB(18, 18, 18, 0), child: Column( children: [ SafeArea( bottom: false, child: _ExpertiseHeader( title: 'Expertise', subtitle: _currentDlp, saving: _saving, onRefresh: _reload, ), ), const SizedBox(height: 22), summaryCard, const SizedBox(height: 14), Expanded(child: expertiseContent), ], ), ); }, ), ); } } class _ExpertiseHeader extends StatelessWidget { const _ExpertiseHeader({ required this.title, required this.subtitle, required this.saving, required this.onRefresh, }); final String title; final String subtitle; final bool saving; final VoidCallback onRefresh; @override Widget build(BuildContext context) { return Row( children: [ IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), color: const Color(0xFF080D3D), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: const Color(0xFF080D3D), fontWeight: FontWeight.w900, ), ), const SizedBox(height: 4), Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: const Color(0xFF47517A), fontWeight: FontWeight.w700, ), ), ], ), ), IconButton( onPressed: saving ? null : onRefresh, icon: const Icon(Icons.refresh_rounded), color: const Color(0xFF063B60), tooltip: 'Refresh', ), ], ); } } class _ExpertiseSummaryCard extends StatelessWidget { const _ExpertiseSummaryCard({required this.item, required this.dlp}); final Map item; final String dlp; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, border: Border.all(color: const Color(0xFFE1E8F5)), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: const Color(0xFF2563EB).withValues(alpha: 0.05), blurRadius: 18, offset: const Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item['nmpasien']?.toString() ?? '-', maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleLarge?.copyWith( color: const Color(0xFF080D3D), fontWeight: FontWeight.w900, ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ BadgeLabel( text: item['nofoto']?.toString() ?? '-', color: const Color(0xFF0B7BFF), ), BadgeLabel( text: 'RM ${item['noregister'] ?? '-'}', color: const Color(0xFF667095), ), StatusPill(status: item['status']?.toString() ?? 'NEW'), if (dlp.isNotEmpty) BadgeLabel(text: dlp, color: const Color(0xFF0F766E)), ], ), const SizedBox(height: 12), Text( item['reques']?.toString() ?? '-', style: const TextStyle( color: Color(0xFF47517A), fontWeight: FontWeight.w800, height: 1.35, ), ), ], ), ); } }