import 'package:flutter/material.dart'; import '../api/odoo_client.dart'; import '../models/portfolio.dart'; import '../models/saham_transaksi.dart'; import '../services/stock_service.dart'; import 'add_transaksi_screen.dart'; import 'edit_saham_screen.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({ super.key, required this.baseUrl, required this.db, required this.user, required this.pass, required this.onLogout, }); final String baseUrl; final String db; final String user; final String pass; final VoidCallback onLogout; @override Widget build(BuildContext context) { final client = OdooClient( baseUrl: baseUrl, db: db, username: user, password: pass); final service = StockService(client); return _HomeShell( service: service, client: client, title: 'Portofolio', onLogout: onLogout, ); } } class _HomeShell extends StatefulWidget { const _HomeShell({required this.service, required this.client, required this.title, required this.onLogout}); final StockService service; final OdooClient client; final String title; final VoidCallback onLogout; @override State<_HomeShell> createState() => _HomeShellState(); } class _HomeShellState extends State<_HomeShell> { int _tab = 0; int _reloadTick = 0; void _reload() => setState(() => _reloadTick++); Future _openAdd() async { if (_tab == 1) { // Tab Saham -> buka tambah saham final added = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => EditSahamScreen(service: widget.service), ), ); if (added == true) _reload(); } else { // Tab lain -> buka buat transaksi final added = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => AddTransaksiScreen(service: widget.service), ), ); if (added == true) _reload(); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Saat Invest'), actions: [ IconButton( icon: const Icon(Icons.refresh), tooltip: 'Muat ulang', onPressed: _reload), IconButton( icon: const Icon(Icons.logout), tooltip: 'Keluar', onPressed: widget.onLogout), ], ), floatingActionButton: FloatingActionButton( onPressed: _openAdd, tooltip: _tab == 1 ? 'Tambah Saham' : 'Catat Transaksi', child: Icon(_tab == 1 ? Icons.add_business : Icons.add), ), body: IndexedStack( index: _tab, children: [ _PortfolioTab(key: ValueKey('pf$_reloadTick'), service: widget.service), _SahamTab(key: ValueKey('sh$_reloadTick'), service: widget.service), _TransaksiTab(key: ValueKey('tx$_reloadTick'), service: widget.service), ], ), bottomNavigationBar: NavigationBar( selectedIndex: _tab, onDestinationSelected: (i) => setState(() => _tab = i), destinations: const [ NavigationDestination(icon: Icon(Icons.pie_chart), label: 'Portofolio'), NavigationDestination(icon: Icon(Icons.trending_up), label: 'Saham'), NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Transaksi'), ], ), ); } } // ---------------- Portofolio tab ---------------- class _PortfolioTab extends StatefulWidget { const _PortfolioTab({super.key, required this.service}); final StockService service; @override State<_PortfolioTab> createState() => _PortfolioTabState(); } class _PortfolioTabState extends State<_PortfolioTab> { late Future> _fut; @override void initState() { super.initState(); _fut = widget.service.listPortofolio(); } void _reload() => setState(() => _fut = widget.service.listPortofolio()); @override Widget build(BuildContext context) { return FutureBuilder>( future: _fut, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return _ErrorRetry(message: '${snap.error}', onRetry: _reload); } final list = snap.data ?? []; final totalNilai = list.fold(0, (a, p) => a + p.nilaiSekarang); final totalModal = list.fold(0, (a, p) => a + p.avgCost * p.sisaLembar); final totalPl = list.fold(0, (a, p) => a + p.unrealizedPl + p.realizedPl); return RefreshIndicator( onRefresh: () async => _reload(), child: ListView( children: [ _EquityCard(equity: totalNilai, modal: totalModal, pl: totalPl), if (list.isEmpty) const Padding( padding: EdgeInsets.all(32), child: Text('Belum ada posisi.\nCatat transaksi beli dulu di tab Transaksi.', textAlign: TextAlign.center), ), ...list.map((p) => Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ListTile( title: Text(p.sahamTicker, style: const TextStyle(fontWeight: FontWeight.bold)), subtitle: Text( '${p.sisaLembar} lbr · avg ${_fmt(p.avgCost)} · hrga ${_fmt(p.hargaSekarang)}'), trailing: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_fmt(p.nilaiSekarang)), Text(_rm(p.unrealizedPl), style: TextStyle( color: p.unrealizedPl >= 0 ? Colors.green : Colors.red)), ], ), ), )), const SizedBox(height: 16), ], ), ); }, ); } } class _EquityCard extends StatelessWidget { const _EquityCard({required this.equity, required this.modal, required this.pl}); final double equity; final double modal; final double pl; @override Widget build(BuildContext context) { final pct = modal > 0 ? (pl / modal) * 100 : 0.0; return Card( margin: const EdgeInsets.all(12), color: const Color(0xFF0B6E4F), child: Padding( padding: const EdgeInsets.all(20), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Total Equity', style: TextStyle(color: Colors.white70, fontSize: 14)), Text('Rp ${_fmt(equity, force: true)}', style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text('Modal dikeluarkan: ${_fmt(modal)}', style: const TextStyle(color: Colors.white70)), Row(children: [ Text('P/L: ', style: const TextStyle(color: Colors.white70)), Text(_rm(pl), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600)), const SizedBox(width: 8), Text('(${pct.toStringAsFixed(2)}%)', style: const TextStyle(color: Colors.white70)), ]), ])), ); } } // ---------------- Saham tab ---------------- class _SahamTab extends StatefulWidget { const _SahamTab({super.key, required this.service}); final StockService service; @override State<_SahamTab> createState() => _SahamTabState(); } class _SahamTabState extends State<_SahamTab> { late Future> _fut; @override void initState() { super.initState(); _fut = widget.service.listSaham(); } void _reload() => setState(() => _fut = widget.service.listSaham()); @override Widget build(BuildContext context) { return FutureBuilder>( future: _fut, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return _ErrorRetry(message: '${snap.error}', onRetry: _reload); } final list = snap.data ?? []; return RefreshIndicator( onRefresh: () async => _reload(), child: ListView.builder( itemCount: list.length, itemBuilder: (c, i) { final s = list[i]; return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ListTile( leading: CircleAvatar( backgroundColor: const Color(0xFF0B6E4F), child: Text(s.kode.substring(0, 1).toUpperCase(), style: const TextStyle(color: Colors.white)), ), title: Text('${s.kode} — ${s.perusahaan ?? '-'}'), subtitle: Text(s.sektor ?? 'Sektor: -'), ), ); }, ), ); }, ); } } // ---------------- Transaksi tab ---------------- class _TransaksiTab extends StatefulWidget { const _TransaksiTab({super.key, required this.service}); final StockService service; @override State<_TransaksiTab> createState() => _TransaksiTabState(); } class _TransaksiTabState extends State<_TransaksiTab> { late Future> _fut; @override void initState() { super.initState(); _fut = widget.service.listTransaksi(); } void _reload() => setState(() => _fut = widget.service.listTransaksi()); Future _cancelTransaksi(Transaksi t) async { final confirm = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Batalkan Transaksi'), content: Text( 'Apakah Anda yakin ingin membatalkan transaksi ${t.sahamTicker} (${t.volume} lembar)?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Batal'), ), FilledButton( style: FilledButton.styleFrom(backgroundColor: Colors.red), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Ya, Batalkan'), ), ], ), ); if (confirm == true) { try { await widget.service.cancelTransaksi(t.id); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Transaksi berhasil dibatalkan')), ); _reload(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Gagal membatalkan transaksi: $e')), ); } } } @override Widget build(BuildContext context) { return FutureBuilder>( future: _fut, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return _ErrorRetry(message: '${snap.error}', onRetry: _reload); } final list = snap.data ?? []; return Column(children: [ Expanded( child: list.isEmpty ? const Center(child: Text('Belum ada transaksi.')) : RefreshIndicator( onRefresh: () async => _reload(), child: ListView.builder( itemCount: list.length, itemBuilder: (c, i) { final t = list[i]; final buy = t.isBuy; final isCancelled = t.isCancelled; return ListTile( leading: CircleAvatar( backgroundColor: isCancelled ? Colors.grey : (buy ? Colors.green : Colors.red), child: Text(buy ? 'B' : 'J', style: const TextStyle(color: Colors.white))), title: Row( children: [ Text( '${t.sahamTicker} · ${t.volume} lbr', style: TextStyle( decoration: isCancelled ? TextDecoration.lineThrough : null, color: isCancelled ? Colors.grey : null, ), ), if (isCancelled) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.red.shade100, borderRadius: BorderRadius.circular(4), ), child: Text( 'Dibatalkan', style: TextStyle( fontSize: 10, color: Colors.red.shade900, fontWeight: FontWeight.bold, ), ), ), ], ], ), subtitle: Text( '${t.tanggal} @ ${_fmt(t.harga)} · fee ${_fmt(t.fee)}', style: TextStyle( decoration: isCancelled ? TextDecoration.lineThrough : null, color: isCancelled ? Colors.grey : null, ), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text( _fmt(t.total), style: TextStyle( decoration: isCancelled ? TextDecoration.lineThrough : null, color: isCancelled ? Colors.grey : null, ), ), if (!isCancelled) IconButton( icon: const Icon(Icons.cancel_outlined, color: Colors.redAccent, size: 20), tooltip: 'Batalkan transaksi', onPressed: () => _cancelTransaksi(t), ), ], ), ); }, ), ), ), ]); }, ); } } class _ErrorRetry extends StatelessWidget { const _ErrorRetry({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: Colors.red, size: 40), const SizedBox(height: 8), Text(message, textAlign: TextAlign.center), const SizedBox(height: 12), OutlinedButton(onPressed: onRetry, child: const Text('Coba lagi')), ])), ); } // helpers String _fmt(double v, {bool force = false}) { final fmt = force ? v.toStringAsFixed(0).replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.') : v.toStringAsFixed(0).replaceAllMapped( RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.'); return fmt; } String _rm(double v) => '${v >= 0 ? '+' : ''}Rp ${_formatAbs(v)}'; String _formatAbs(double v) { final a = v.abs().toStringAsFixed(0); return a.replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.'); }