Gabung app Android Flutter 'Saat Invest' ke repo saat-app (folder flutter/)
Satu repo untuk backend Odoo (addon saat_invest) + app Android. - flutter/ berisi project Flutter (lib/, android/, pubspec, test) - Login Odoo JSON-RPC, tab Portofolio+Equity, tab Transaksi, catat jual/beli otomatis sync - Build .gitignore nested dipakai sehingga artefak build/.dart_tool tak ter-track Sejarah: gabung flat (satu sejarah backend+app).
This commit is contained in:
126
flutter/lib/add_transaksi_screen.dart
Normal file
126
flutter/lib/add_transaksi_screen.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../models/saham_transaksi.dart';
|
||||
import '../services/stock_service.dart';
|
||||
|
||||
class AddTransaksiScreen extends StatefulWidget {
|
||||
const AddTransaksiScreen({super.key, required this.service});
|
||||
final StockService service;
|
||||
|
||||
@override
|
||||
State<AddTransaksiScreen> createState() => _AddTransaksiScreenState();
|
||||
}
|
||||
|
||||
class _AddTransaksiScreenState extends State<AddTransaksiScreen> {
|
||||
final _tanggal = TextEditingController(
|
||||
text: DateFormat('yyyy-MM-dd').format(DateTime.now()));
|
||||
final _volume = TextEditingController();
|
||||
final _harga = TextEditingController();
|
||||
final _fee = TextEditingController(text: '0');
|
||||
|
||||
late Future<List<Saham>> _sahamFut;
|
||||
int? _sahamId;
|
||||
bool _isBuy = true;
|
||||
bool _busy = false;
|
||||
String? _err;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sahamFut = widget.service.listSaham();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Catat Transaksi')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
children: [
|
||||
SegmentedButton<bool>(
|
||||
segments: const [
|
||||
ButtonSegment(value: true, label: Text('Beli'), icon: Icon(Icons.add)),
|
||||
ButtonSegment(value: false, label: Text('Jual'), icon: Icon(Icons.remove)),
|
||||
],
|
||||
selected: {_isBuy},
|
||||
onSelectionChanged: (s) => setState(() => _isBuy = s.first),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(controller: _tanggal,
|
||||
decoration: const InputDecoration(labelText: 'Tanggal (YYYY-MM-DD)',
|
||||
prefixIcon: Icon(Icons.calendar_today))),
|
||||
const SizedBox(height: 12),
|
||||
FutureBuilder<List<Saham>>(
|
||||
future: _sahamFut,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const ListTile(
|
||||
leading: SizedBox(width:18,height:18,child:CircularProgressIndicator(strokeWidth:2)),
|
||||
title: Text('Memuat saham...'));
|
||||
}
|
||||
if (snap.hasError) {
|
||||
return Text('Error saham: ${snap.error}', style: const TextStyle(color: Colors.red));
|
||||
}
|
||||
final saham = snap.data ?? [];
|
||||
return DropdownButtonFormField<int>(
|
||||
initialValue: _sahamId,
|
||||
decoration: const InputDecoration(labelText: 'Pilih Saham',
|
||||
prefixIcon: Icon(Icons.business)),
|
||||
items: [
|
||||
for (final s in saham)
|
||||
DropdownMenuItem(value: s.id, child: Text('${s.kode} — ${s.perusahaan ?? ''}')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _sahamId = v),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(controller: _volume, keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: 'Jumlah Lembar', prefixIcon: Icon(Icons.numbers))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(controller: _harga, keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Harga per Lembar (Rp)', prefixIcon: Icon(Icons.attach_money))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(controller: _fee, keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(labelText: 'Biaya / Fee (Rp)', prefixIcon: Icon(Icons.payments))),
|
||||
if (_err != null)
|
||||
Padding(padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(_err!, 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; _err = null; });
|
||||
try {
|
||||
final date = _tanggal.text.trim();
|
||||
final vol = int.tryParse(_volume.text.trim()) ?? 0;
|
||||
final harga = double.tryParse(_harga.text.replaceAll('.', '').replaceAll(',', '.')) ?? 0;
|
||||
final feeC = double.tryParse(_fee.text.trim().isEmpty ? '0' : _fee.text.trim()) ?? 0;
|
||||
if (_sahamId == null) throw Exception('Pilih saham dulu');
|
||||
if (vol <= 0) throw Exception('Volume harus > 0');
|
||||
if (harga <= 0) throw Exception('Harga harus > 0');
|
||||
if (!date.contains('-')) throw Exception('Format tanggal salah');
|
||||
|
||||
await widget.service.addTransaksi(
|
||||
sahamId: _sahamId!, tipe: _isBuy ? 'buy' : 'sell',
|
||||
volume: vol, harga: harga, fee: feeC, tanggal: date);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() { _busy = false; _err = e.toString().replaceFirst('Exception: ', ''); });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user