Flutter: fitur Tambah Saham (tab Saham + form kode/perusahaan/sektor)
- Tab baru 'Saham' di NavigationBar (daftar master saham dari saat.saham) - FAB konteks: di tab Saham buka form Tambah Saham, di tab lain catat transaksi - EditSahamScreen: form tambah saham baru (kode ticker, perusahaan, sektor) - StockService: method addSaham(), updateSaham(), hapusSaham() ke Odoo
This commit is contained in:
115
flutter/lib/edit_saham_screen.dart
Normal file
115
flutter/lib/edit_saham_screen.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../models/saham_transaksi.dart';
|
||||||
|
import '../services/stock_service.dart';
|
||||||
|
|
||||||
|
class EditSahamScreen extends StatefulWidget {
|
||||||
|
const EditSahamScreen({super.key, required this.service, this.saham});
|
||||||
|
final StockService service;
|
||||||
|
final Saham? saham; // null = tambah baru; ada = edit
|
||||||
|
@override
|
||||||
|
State<EditSahamScreen> createState() => _EditSahamScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditSahamScreenState extends State<EditSahamScreen> {
|
||||||
|
final _kode = TextEditingController();
|
||||||
|
final _perusahaan = TextEditingController();
|
||||||
|
final _sektor = TextEditingController();
|
||||||
|
bool _busy = false;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final s = widget.saham;
|
||||||
|
if (s != null) {
|
||||||
|
_kode.text = s.kode;
|
||||||
|
_perusahaan.text = s.perusahaan ?? '';
|
||||||
|
_sektor.text = s.sektor ?? '';
|
||||||
|
} else {
|
||||||
|
_sektor.text = 'Financial';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isEdit = widget.saham != null;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(isEdit ? 'Edit Saham' : 'Tambah Saham')),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _kode,
|
||||||
|
enabled: !isEdit, // kode tidak boleh diubah saat edit
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Kode Saham (Ticker)',
|
||||||
|
hintText: 'misal: BBRI, UNVR, TLKM',
|
||||||
|
prefixIcon: Icon(Icons.business)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _perusahaan,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nama Perusahaan / Emiten',
|
||||||
|
prefixIcon: Icon(Icons.apartment)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: _sektor,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Sektor', prefixIcon: Icon(Icons.category)),
|
||||||
|
),
|
||||||
|
if (_error != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8),
|
||||||
|
child: Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _busy ? null : _save,
|
||||||
|
icon: _busy
|
||||||
|
? const SizedBox(width: 18, height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: const Text('Simpan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
setState(() {
|
||||||
|
_busy = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final kode = _kode.text.trim().toUpperCase();
|
||||||
|
if (kode.isEmpty) throw Exception('Kode saham wajib diisi');
|
||||||
|
|
||||||
|
if (widget.saham == null) {
|
||||||
|
await widget.service.addSaham(
|
||||||
|
kode: kode,
|
||||||
|
perusahaan: _perusahaan.text.trim(),
|
||||||
|
sektor: _sektor.text.trim(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await widget.service.updateSaham(widget.saham!.id,
|
||||||
|
perusahaan: _perusahaan.text.trim(),
|
||||||
|
sektor: _sektor.text.trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_busy = false;
|
||||||
|
_error = e.toString().replaceFirst('Exception: ', '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import '../models/portfolio.dart';
|
|||||||
import '../models/saham_transaksi.dart';
|
import '../models/saham_transaksi.dart';
|
||||||
import '../services/stock_service.dart';
|
import '../services/stock_service.dart';
|
||||||
import 'add_transaksi_screen.dart';
|
import 'add_transaksi_screen.dart';
|
||||||
|
import 'edit_saham_screen.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatelessWidget {
|
||||||
const HomeScreen({
|
const HomeScreen({
|
||||||
@@ -24,7 +25,8 @@ class HomeScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = OdooClient(baseUrl: baseUrl, db: db, username: user, password: pass);
|
final client = OdooClient(
|
||||||
|
baseUrl: baseUrl, db: db, username: user, password: pass);
|
||||||
final service = StockService(client);
|
final service = StockService(client);
|
||||||
return _HomeShell(
|
return _HomeShell(
|
||||||
service: service,
|
service: service,
|
||||||
@@ -53,12 +55,23 @@ class _HomeShellState extends State<_HomeShell> {
|
|||||||
void _reload() => setState(() => _reloadTick++);
|
void _reload() => setState(() => _reloadTick++);
|
||||||
|
|
||||||
Future<void> _openAdd() async {
|
Future<void> _openAdd() async {
|
||||||
final added = await Navigator.of(context).push<bool>(
|
if (_tab == 1) {
|
||||||
MaterialPageRoute(
|
// Tab Saham -> buka tambah saham
|
||||||
builder: (_) => AddTransaksiScreen(service: widget.service),
|
final added = await Navigator.of(context).push<bool>(
|
||||||
),
|
MaterialPageRoute(
|
||||||
);
|
builder: (_) => EditSahamScreen(service: widget.service),
|
||||||
if (added == true) _reload();
|
),
|
||||||
|
);
|
||||||
|
if (added == true) _reload();
|
||||||
|
} else {
|
||||||
|
// Tab lain -> buka buat transaksi
|
||||||
|
final added = await Navigator.of(context).push<bool>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => AddTransaksiScreen(service: widget.service),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (added == true) _reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -67,20 +80,26 @@ class _HomeShellState extends State<_HomeShell> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Saat Invest'),
|
title: const Text('Saat Invest'),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: const Icon(Icons.refresh), tooltip: 'Muat ulang',
|
IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: 'Muat ulang',
|
||||||
onPressed: _reload),
|
onPressed: _reload),
|
||||||
IconButton(icon: const Icon(Icons.logout), tooltip: 'Keluar', onPressed: widget.onLogout),
|
IconButton(
|
||||||
|
icon: const Icon(Icons.logout),
|
||||||
|
tooltip: 'Keluar',
|
||||||
|
onPressed: widget.onLogout),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: _openAdd,
|
onPressed: _openAdd,
|
||||||
tooltip: 'Catat transaksi baru',
|
tooltip: _tab == 1 ? 'Tambah Saham' : 'Catat Transaksi',
|
||||||
child: const Icon(Icons.add),
|
child: Icon(_tab == 1 ? Icons.add_business : Icons.add),
|
||||||
),
|
),
|
||||||
body: IndexedStack(
|
body: IndexedStack(
|
||||||
index: _tab,
|
index: _tab,
|
||||||
children: [
|
children: [
|
||||||
_PortfolioTab(key: ValueKey('pf$_reloadTick'), service: widget.service),
|
_PortfolioTab(key: ValueKey('pf$_reloadTick'), service: widget.service),
|
||||||
|
_SahamTab(key: ValueKey('sh$_reloadTick'), service: widget.service),
|
||||||
_TransaksiTab(key: ValueKey('tx$_reloadTick'), service: widget.service),
|
_TransaksiTab(key: ValueKey('tx$_reloadTick'), service: widget.service),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -89,6 +108,7 @@ class _HomeShellState extends State<_HomeShell> {
|
|||||||
onDestinationSelected: (i) => setState(() => _tab = i),
|
onDestinationSelected: (i) => setState(() => _tab = i),
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationDestination(icon: Icon(Icons.pie_chart), label: 'Portofolio'),
|
NavigationDestination(icon: Icon(Icons.pie_chart), label: 'Portofolio'),
|
||||||
|
NavigationDestination(icon: Icon(Icons.trending_up), label: 'Saham'),
|
||||||
NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Transaksi'),
|
NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Transaksi'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -96,7 +116,7 @@ class _HomeShellState extends State<_HomeShell> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Portofolio / Equity tab ----------------
|
// ---------------- Portofolio tab ----------------
|
||||||
|
|
||||||
class _PortfolioTab extends StatefulWidget {
|
class _PortfolioTab extends StatefulWidget {
|
||||||
const _PortfolioTab({super.key, required this.service});
|
const _PortfolioTab({super.key, required this.service});
|
||||||
@@ -107,7 +127,6 @@ class _PortfolioTab extends StatefulWidget {
|
|||||||
|
|
||||||
class _PortfolioTabState extends State<_PortfolioTab> {
|
class _PortfolioTabState extends State<_PortfolioTab> {
|
||||||
late Future<List<Portfolio>> _fut;
|
late Future<List<Portfolio>> _fut;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -128,8 +147,7 @@ class _PortfolioTabState extends State<_PortfolioTab> {
|
|||||||
return _ErrorRetry(message: '${snap.error}', onRetry: _reload);
|
return _ErrorRetry(message: '${snap.error}', onRetry: _reload);
|
||||||
}
|
}
|
||||||
final list = snap.data ?? [];
|
final list = snap.data ?? [];
|
||||||
final totalNilai = list.fold<double>(
|
final totalNilai = list.fold<double>(0, (a, p) => a + p.nilaiSekarang);
|
||||||
0, (a, p) => a + p.nilaiSekarang);
|
|
||||||
final totalModal = list.fold<double>(0, (a, p) => a + p.avgCost * p.sisaLembar);
|
final totalModal = list.fold<double>(0, (a, p) => a + p.avgCost * p.sisaLembar);
|
||||||
final totalPl = list.fold<double>(0, (a, p) => a + p.unrealizedPl + p.realizedPl);
|
final totalPl = list.fold<double>(0, (a, p) => a + p.unrealizedPl + p.realizedPl);
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
@@ -144,21 +162,24 @@ class _PortfolioTabState extends State<_PortfolioTab> {
|
|||||||
textAlign: TextAlign.center),
|
textAlign: TextAlign.center),
|
||||||
),
|
),
|
||||||
...list.map((p) => Card(
|
...list.map((p) => Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(p.sahamTicker, style: const TextStyle(fontWeight: FontWeight.bold)),
|
title: Text(p.sahamTicker,
|
||||||
subtitle: Text('${p.sisaLembar} lbr · avg ${_fmt(p.avgCost)} · hrga ${_fmt(p.hargaSekarang)}'),
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
trailing: Column(
|
subtitle: Text(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
'${p.sisaLembar} lbr · avg ${_fmt(p.avgCost)} · hrga ${_fmt(p.hargaSekarang)}'),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
trailing: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
Text(_fmt(p.nilaiSekarang)),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Text(_rm(p.unrealizedPl), style: TextStyle(
|
children: [
|
||||||
color: p.unrealizedPl >= 0 ? Colors.green : Colors.red)),
|
Text(_fmt(p.nilaiSekarang)),
|
||||||
],
|
Text(_rm(p.unrealizedPl),
|
||||||
),
|
style: TextStyle(
|
||||||
),
|
color: p.unrealizedPl >= 0 ? Colors.green : Colors.red)),
|
||||||
)),
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -181,23 +202,85 @@ class _EquityCard extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.all(12),
|
margin: const EdgeInsets.all(12),
|
||||||
color: const Color(0xFF0B6E4F),
|
color: const Color(0xFF0B6E4F),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text('Total Equity', style: TextStyle(color: Colors.white70, fontSize: 14)),
|
Text('Total Equity',
|
||||||
Text('Rp ${_fmt(equity, force: true)}',
|
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)),
|
Text('Rp ${_fmt(equity, force: true)}',
|
||||||
const SizedBox(height: 8),
|
style: const TextStyle(
|
||||||
Text('Modal dikeluarkan: ${_fmt(modal)}',
|
color: Colors.white,
|
||||||
style: const TextStyle(color: Colors.white70)),
|
fontSize: 28,
|
||||||
Row(children: [
|
fontWeight: FontWeight.bold)),
|
||||||
Text('P/L: ', style: const TextStyle(color: Colors.white70)),
|
const SizedBox(height: 8),
|
||||||
Text(_rm(pl),
|
Text('Modal dikeluarkan: ${_fmt(modal)}',
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
style: const TextStyle(color: Colors.white70)),
|
||||||
const SizedBox(width: 8),
|
Row(children: [
|
||||||
Text('(${pct.toStringAsFixed(2)}%)', style: const TextStyle(color: Colors.white70)),
|
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<List<Saham>> _fut;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fut = widget.service.listSaham();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _reload() => setState(() => _fut = widget.service.listSaham());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<List<Saham>>(
|
||||||
|
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: -'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +296,6 @@ class _TransaksiTab extends StatefulWidget {
|
|||||||
|
|
||||||
class _TransaksiTabState extends State<_TransaksiTab> {
|
class _TransaksiTabState extends State<_TransaksiTab> {
|
||||||
late Future<List<Transaksi>> _fut;
|
late Future<List<Transaksi>> _fut;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -247,9 +329,10 @@ class _TransaksiTabState extends State<_TransaksiTab> {
|
|||||||
final buy = t.isBuy;
|
final buy = t.isBuy;
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
backgroundColor: buy ? Colors.green : Colors.red,
|
backgroundColor:
|
||||||
child: Text(buy ? 'B' : 'J', style: const TextStyle(color: Colors.white)),
|
buy ? Colors.green : Colors.red,
|
||||||
),
|
child: Text(buy ? 'B' : 'J',
|
||||||
|
style: const TextStyle(color: Colors.white))),
|
||||||
title: Text('${t.sahamTicker} · ${t.volume} lbr'),
|
title: Text('${t.sahamTicker} · ${t.volume} lbr'),
|
||||||
subtitle: Text('${t.tanggal} @ ${_fmt(t.harga)} · fee ${_fmt(t.fee)}'),
|
subtitle: Text('${t.tanggal} @ ${_fmt(t.harga)} · fee ${_fmt(t.fee)}'),
|
||||||
trailing: Text(_fmt(t.total)),
|
trailing: Text(_fmt(t.total)),
|
||||||
@@ -271,23 +354,24 @@ class _ErrorRetry extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Center(
|
Widget build(BuildContext context) => Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
const Icon(Icons.error_outline, color: Colors.red, size: 40),
|
const Icon(Icons.error_outline, color: Colors.red, size: 40),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(message, textAlign: TextAlign.center),
|
Text(message, textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
OutlinedButton(onPressed: onRetry, child: const Text('Coba lagi')),
|
OutlinedButton(onPressed: onRetry, child: const Text('Coba lagi')),
|
||||||
]),
|
])),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
|
||||||
String _fmt(double v, {bool force = false}) {
|
String _fmt(double v, {bool force = false}) {
|
||||||
final fmt = force
|
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) => '.')
|
||||||
: v.toStringAsFixed(0).replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.');
|
: v.toStringAsFixed(0).replaceAllMapped(
|
||||||
|
RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.');
|
||||||
return fmt;
|
return fmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,4 +381,3 @@ String _formatAbs(double v) {
|
|||||||
final a = v.abs().toStringAsFixed(0);
|
final a = v.abs().toStringAsFixed(0);
|
||||||
return a.replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.');
|
return a.replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,35 @@ class StockService {
|
|||||||
return (res as List).map((e) => Saham.fromOdoo(e as Map<String, dynamic>)).toList();
|
return (res as List).map((e) => Saham.fromOdoo(e as Map<String, dynamic>)).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tambah saham baru ke Odoo saat.saham.create
|
||||||
|
Future<int> addSaham({
|
||||||
|
required String kode,
|
||||||
|
String? perusahaan,
|
||||||
|
String? sektor,
|
||||||
|
}) async {
|
||||||
|
final vals = <String, dynamic>{
|
||||||
|
'name': kode,
|
||||||
|
if (perusahaan != null && perusahaan.isNotEmpty) 'perusahaan': perusahaan,
|
||||||
|
if (sektor != null && sektor.isNotEmpty) 'sector': sektor,
|
||||||
|
};
|
||||||
|
final res = await client.executeKwd('saat.saham', 'create', args: [vals]);
|
||||||
|
return (res as num).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update data saham (perusahaan, sektor)
|
||||||
|
Future<void> updateSaham(int id, {String? perusahaan, String? sektor}) async {
|
||||||
|
final vals = <String, dynamic>{
|
||||||
|
if (perusahaan != null) 'perusahaan': perusahaan,
|
||||||
|
if (sektor != null) 'sector': sektor,
|
||||||
|
};
|
||||||
|
await client.executeKwd('saat.saham', 'write', args: [[id], vals]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Hapus saham (optional) ---
|
||||||
|
Future<void> hapusSaham(int id) async {
|
||||||
|
await client.executeKwd('saat.saham', 'unlink', args: [[id]]);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Portofolio ----
|
// ---- Portofolio ----
|
||||||
Future<List<Portfolio>> listPortofolio() async {
|
Future<List<Portfolio>> listPortofolio() async {
|
||||||
final fields = [
|
final fields = [
|
||||||
|
|||||||
Reference in New Issue
Block a user