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).
127 lines
5.2 KiB
Dart
127 lines
5.2 KiB
Dart
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: ', ''); });
|
|
}
|
|
}
|
|
}
|