cashumit/lib/screens/config_screen.dart

729 lines
27 KiB
Dart

// lib/screens/config_screen.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cashumit/services/firefly_api_service.dart';
import 'package:cashumit/services/account_cache_service.dart';
import 'package:cashumit/services/account_mirror_service.dart';
import 'package:cashumit/models/report_signature_config.dart';
class ConfigScreen extends StatefulWidget {
const ConfigScreen({super.key});
@override
State<ConfigScreen> createState() => _ConfigScreenState();
}
class _ConfigScreenState extends State<ConfigScreen> {
final _formKey = GlobalKey<FormState>();
final _urlController = TextEditingController();
final _tokenController = TextEditingController();
// Controller Konfigurasi Laporan Keuangan (4 Pihak Penandatangan Grid 2x2)
final _reportTitleController = TextEditingController();
final _cityController = TextEditingController();
// Pihak 1: Pelaksana UPJ (Kiri Atas)
final _sig1RoleController = TextEditingController();
final _sig1NameController = TextEditingController();
final _sig1NipController = TextEditingController();
// Pihak 2: Pembuat Laporan (Kanan Atas)
final _sig2RoleController = TextEditingController();
final _sig2NameController = TextEditingController();
final _sig2NipController = TextEditingController();
// Pihak 3: Koordinator UPJ (Kiri Bawah)
final _sig3RoleController = TextEditingController();
final _sig3NameController = TextEditingController();
final _sig3NipController = TextEditingController();
// Pihak 4: KPA BLUD SMKN 4 Malang (Kanan Bawah)
final _sig4RoleController = TextEditingController();
final _sig4NameController = TextEditingController();
final _sig4NipController = TextEditingController();
bool _isTestingConnection = false;
bool _isTestingAuth = false;
// Pengaturan cetak gambar di struk thermal
bool _printLogo = true;
bool _printQris = true;
static const String _prefsKeyUrl = 'firefly_url';
static const String _prefsKeyToken = 'firefly_token';
@override
void initState() {
super.initState();
_loadConfig();
}
@override
void dispose() {
_urlController.dispose();
_tokenController.dispose();
_reportTitleController.dispose();
_cityController.dispose();
_sig1RoleController.dispose();
_sig1NameController.dispose();
_sig1NipController.dispose();
_sig2RoleController.dispose();
_sig2NameController.dispose();
_sig2NipController.dispose();
_sig3RoleController.dispose();
_sig3NameController.dispose();
_sig3NipController.dispose();
_sig4RoleController.dispose();
_sig4NameController.dispose();
_sig4NipController.dispose();
super.dispose();
}
/// Memuat konfigurasi yang tersimpan dari shared preferences.
Future<void> _loadConfig() async {
final prefs = await SharedPreferences.getInstance();
final savedUrl = prefs.getString(_prefsKeyUrl) ?? '';
final savedToken = prefs.getString(_prefsKeyToken) ?? '';
final sigConfig = await ReportSignatureConfig.load();
setState(() {
_urlController.text = savedUrl;
_tokenController.text = savedToken;
_reportTitleController.text = sigConfig.reportTitle;
_cityController.text = sigConfig.city;
_printLogo = prefs.getBool('print_logo_enabled') ?? true;
_printQris = prefs.getBool('print_qris_enabled') ?? true;
_sig1RoleController.text = sigConfig.signatory1Role;
_sig1NameController.text = sigConfig.signatory1Name;
_sig1NipController.text = sigConfig.signatory1Nip;
_sig2RoleController.text = sigConfig.signatory2Role;
_sig2NameController.text = sigConfig.signatory2Name;
_sig2NipController.text = sigConfig.signatory2Nip;
_sig3RoleController.text = sigConfig.signatory3Role;
_sig3NameController.text = sigConfig.signatory3Name;
_sig3NipController.text = sigConfig.signatory3Nip;
_sig4RoleController.text = sigConfig.signatory4Role;
_sig4NameController.text = sigConfig.signatory4Name;
_sig4NipController.text = sigConfig.signatory4Nip;
});
}
/// Menyimpan konfigurasi ke shared preferences.
Future<void> _saveConfig() async {
if (_formKey.currentState!.validate()) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsKeyUrl, _urlController.text);
await prefs.setString(_prefsKeyToken, _tokenController.text);
await prefs.setBool('print_logo_enabled', _printLogo);
await prefs.setBool('print_qris_enabled', _printQris);
final sigConfig = ReportSignatureConfig(
reportTitle: _reportTitleController.text.trim().isEmpty
? 'LAPORAN KEUANGAN UPJ MEKATRONIKA'
: _reportTitleController.text.trim(),
city: _cityController.text.trim().isEmpty
? 'Malang'
: _cityController.text.trim(),
signatory1Role: _sig1RoleController.text.trim().isEmpty
? 'Mengetahui,\nPelaksana UPJ Mekatronika'
: _sig1RoleController.text.trim(),
signatory1Name: _sig1NameController.text.trim().isEmpty
? 'Anggoro Dwi Nur Rohman, S.Pd, M.T'
: _sig1NameController.text.trim(),
signatory1Nip: _sig1NipController.text.trim(),
signatory2Role: _sig2RoleController.text.trim().isEmpty
? 'Dibuat oleh,'
: _sig2RoleController.text.trim(),
signatory2Name: _sig2NameController.text.trim().isEmpty
? 'Rama Dwi Nofanto Setiawan'
: _sig2NameController.text.trim(),
signatory2Nip: _sig2NipController.text.trim(),
signatory3Role: _sig3RoleController.text.trim().isEmpty
? 'Koordinator UPJ'
: _sig3RoleController.text.trim(),
signatory3Name: _sig3NameController.text.trim().isEmpty
? 'Puguh Hartono, SPd'
: _sig3NameController.text.trim(),
signatory3Nip: _sig3NipController.text.trim(),
signatory4Role: _sig4RoleController.text.trim().isEmpty
? 'KPA BLUD\nSMKN 4 Malang'
: _sig4RoleController.text.trim(),
signatory4Name: _sig4NameController.text.trim().isEmpty
? 'Drs. Gunawan Dwiyono, S.ST, M.Pd'
: _sig4NameController.text.trim(),
signatory4Nip: _sig4NipController.text.trim(),
);
await sigConfig.save();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Konfigurasi tersimpan!')),
);
Navigator.pop(context, true);
}
}
}
/// Menguji koneksi ke Firefly III.
Future<void> _testConnection() async {
if (_urlController.text.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Silakan masukkan URL Firefly III terlebih dahulu')),
);
}
return;
}
setState(() {
_isTestingConnection = true;
});
try {
final isConnected =
await FireflyApiService.testConnection(baseUrl: _urlController.text);
if (mounted) {
if (isConnected) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Koneksi berhasil!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Koneksi gagal. Periksa URL dan koneksi internet Anda.')),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Terjadi kesalahan saat menguji koneksi')),
);
}
} finally {
if (mounted) {
setState(() {
_isTestingConnection = false;
});
}
}
}
/// Menguji autentikasi dengan token.
Future<void> _testAuthentication() async {
if (_urlController.text.isEmpty || _tokenController.text.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Silakan masukkan URL dan token Firefly III terlebih dahulu')),
);
}
return;
}
setState(() {
_isTestingAuth = true;
});
try {
final isAuthenticated = await FireflyApiService.testAuthentication(
baseUrl: _urlController.text,
accessToken: _tokenController.text,
);
if (mounted) {
if (isAuthenticated) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Autentikasi berhasil!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Autentikasi gagal. Periksa token Anda.')),
);
}
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Terjadi kesalahan saat menguji autentikasi')),
);
}
} finally {
if (mounted) {
setState(() {
_isTestingAuth = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Konfigurasi'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Firefly III Configuration Section
const Text(
'Konfigurasi Firefly III',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
'Masukkan detail koneksi ke instance Firefly III Anda:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
// TextField untuk URL
TextFormField(
controller: _urlController,
decoration: const InputDecoration(
labelText: 'Firefly III URL',
hintText: 'https://firefly.yourdomain.com',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'URL tidak boleh kosong';
}
if (!value.startsWith('http')) {
return 'URL harus dimulai dengan http:// atau https://';
}
return null;
},
),
const SizedBox(height: 20),
// TextField untuk Token
TextFormField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Personal Access Token',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Token tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 20),
// Tombol Simpan
ElevatedButton.icon(
onPressed: _saveConfig,
icon: const Icon(Icons.save),
label: const Text('Simpan Konfigurasi'),
),
const SizedBox(height: 10),
// Tombol Test Connection
ElevatedButton.icon(
onPressed: _isTestingConnection ? null : _testConnection,
icon: _isTestingConnection
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.link),
label: Text(_isTestingConnection
? 'Menguji Koneksi...'
: 'Test Koneksi'),
),
const SizedBox(height: 10),
// Tombol Test Authentication
ElevatedButton.icon(
onPressed: _isTestingAuth ? null : _testAuthentication,
icon: _isTestingAuth
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.verified_user),
label: Text(_isTestingAuth
? 'Menguji Autentikasi...'
: 'Test Autentikasi'),
),
const SizedBox(height: 30),
// ==================== PENGATURAN CETAK STRUK ====================
const Divider(thickness: 1.5),
const SizedBox(height: 10),
const Text(
'Pengaturan Cetak Struk',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Pilih gambar yang ikut dicetak pada struk printer thermal. Logo dan QRIS diambil dari Konfigurasi Info Toko.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Cetak logo toko'),
value: _printLogo,
onChanged: (value) {
setState(() {
_printLogo = value;
});
},
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Cetak QRIS'),
value: _printQris,
onChanged: (value) {
setState(() {
_printQris = value;
});
},
),
const SizedBox(height: 30),
// Offline Account Configuration Section
const Text(
'Pengaturan Akun Offline',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
'Atur sinkronisasi data akun ketika server Firefly III terhubung untuk digunakan saat offline.',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 20),
// Tampilkan informasi waktu sync terakhir
FutureBuilder<DateTime?>(
future: AccountMirrorService.getLastSyncTime(),
builder: (context, snapshot) {
String syncInfo = 'Waktu sync terakhir: ';
if (snapshot.hasData && snapshot.data != null) {
final syncTime = snapshot.data!;
syncInfo += AccountMirrorService.formatSyncTime(syncTime);
} else {
syncInfo += 'Belum pernah disinkronisasi';
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
syncInfo,
style:
const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 10),
// Tombol untuk sync manual
ElevatedButton.icon(
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
final url = prefs.getString('firefly_url');
final token = prefs.getString('firefly_token');
if (url != null &&
token != null &&
url.isNotEmpty &&
token.isNotEmpty) {
try {
final revenue =
await FireflyApiService.fetchAccounts(
baseUrl: url,
accessToken: token,
type: 'revenue',
);
final asset =
await FireflyApiService.fetchAccounts(
baseUrl: url,
accessToken: token,
type: 'asset',
);
await AccountMirrorService.mirrorAccounts([
...revenue,
...asset,
]);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Sinkronisasi akun berhasil!')),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Gagal menyinkronisasi: $e')),
);
}
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Silakan atur URL dan token Firefly III terlebih dahulu')),
);
}
}
},
icon: const Icon(Icons.sync),
label: const Text('Sinkronisasi Manual'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
),
),
const SizedBox(height: 10),
// Tombol untuk menghapus cache akun
ElevatedButton.icon(
onPressed: () async {
await AccountCacheService.clearCache();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Cache akun dihapus')),
);
setState(() {});
}
},
icon: const Icon(Icons.delete),
label: const Text('Hapus Cache Akun'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
),
],
);
},
),
const SizedBox(height: 30),
// ==================== PENGATURAN LAPORAN KEUANGAN ====================
const Divider(thickness: 1.5),
const SizedBox(height: 10),
const Text(
'Pengaturan Dokumen Laporan Keuangan',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Kustomisasi judul dokumen, kota, dan data penandatangan resmi untuk laporan PDF A4.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 16),
TextFormField(
controller: _reportTitleController,
decoration: const InputDecoration(
labelText: 'Judul Laporan',
hintText: 'LAPORAN KEUANGAN UPJ MEKATRONIKA',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextFormField(
controller: _cityController,
decoration: const InputDecoration(
labelText: 'Kota Penerbitan Laporan',
hintText: 'Malang',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
// Penandatangan 1 (Pelaksana UPJ - Kiri Atas)
const Text(
'1. Pelaksana UPJ (Kiri Atas):',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig1RoleController,
decoration: const InputDecoration(
labelText: 'Jabatan / Posisi Penandatangan 1',
hintText: 'Mengetahui,\nPelaksana UPJ Mekatronika',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 10),
TextFormField(
controller: _sig1NameController,
decoration: const InputDecoration(
labelText: 'Nama Lengkap & Gelar',
hintText: 'Anggoro Dwi Nur Rohman, S.Pd, M.T',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig1NipController,
decoration: const InputDecoration(
labelText: 'NIP / NNIP Penandatangan 1',
hintText: 'NNIP. 20533816260193012106054',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Penandatangan 2 (Pembuat Laporan - Kanan Atas)
const Text(
'2. Pembuat Laporan (Kanan Atas):',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig2RoleController,
decoration: const InputDecoration(
labelText: 'Keterangan Pembuat',
hintText: 'Dibuat oleh,',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig2NameController,
decoration: const InputDecoration(
labelText: 'Nama Lengkap & Gelar',
hintText: 'Rama Dwi Nofanto Setiawan',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig2NipController,
decoration: const InputDecoration(
labelText: 'NIP / ID (Opsional)',
hintText: 'Kosongkan jika tidak ada',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Penandatangan 3 (Koordinator UPJ - Kiri Bawah)
const Text(
'3. Koordinator UPJ (Kiri Bawah):',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig3RoleController,
decoration: const InputDecoration(
labelText: 'Jabatan Penandatangan 3',
hintText: 'Koordinator UPJ',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig3NameController,
decoration: const InputDecoration(
labelText: 'Nama Lengkap & Gelar',
hintText: 'Puguh Hartono, SPd',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig3NipController,
decoration: const InputDecoration(
labelText: 'NIP Penandatangan 3',
hintText: '197003152003121009',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Penandatangan 4 (KPA BLUD - Kanan Bawah)
const Text(
'4. KPA BLUD SMKN 4 Malang (Kanan Bawah):',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig4RoleController,
decoration: const InputDecoration(
labelText: 'Jabatan Penandatangan 4',
hintText: 'KPA BLUD\nSMKN 4 Malang',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 10),
TextFormField(
controller: _sig4NameController,
decoration: const InputDecoration(
labelText: 'Nama Lengkap & Gelar',
hintText: 'Drs. Gunawan Dwiyono, S.ST, M.Pd',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
TextFormField(
controller: _sig4NipController,
decoration: const InputDecoration(
labelText: 'NIP Penandatangan 4',
hintText: '19670930 199512 1 002',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Tombol Simpan Semua
ElevatedButton.icon(
onPressed: _saveConfig,
icon: const Icon(Icons.save),
label: const Text('Simpan Pengaturan Laporan'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF14532D),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
const SizedBox(height: 20),
],
),
),
),
);
}
}