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:
2026-09-06 19:52:07 +07:00
parent 286b207f27
commit 1fc095c63c
3 changed files with 290 additions and 63 deletions

View 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: ', '');
});
}
}
}