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:
179
flutter/lib/main.dart
Normal file
179
flutter/lib/main.dart
Normal file
@@ -0,0 +1,179 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../api/odoo_client.dart';
|
||||
import '../services/session.dart';
|
||||
import 'home_screen.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const SaatInvestApp());
|
||||
}
|
||||
|
||||
class SaatInvestApp extends StatelessWidget {
|
||||
const SaatInvestApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Saat Invest',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(colorSchemeSeed: const Color(0xFF0B6E4F), useMaterial3: true),
|
||||
home: const RootScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Jika session tersimpan -> Home, jika tidak -> Login.
|
||||
class RootScreen extends StatefulWidget {
|
||||
const RootScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RootScreen> createState() => _RootScreenState();
|
||||
}
|
||||
|
||||
class _RootScreenState extends State<RootScreen> {
|
||||
late Future<({String base, String db, String user, String pass})?> _check;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_check = Session.load();
|
||||
}
|
||||
|
||||
void _onLoginDone({required String base, required String db, required String user, required String pass}) {
|
||||
setState(() {
|
||||
_check = Future.value(
|
||||
(base: base, db: db, user: user, pass: pass),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
await Session.clear();
|
||||
setState(() => _check = Session.load());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _check,
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
final s = snap.data;
|
||||
if (s == null) {
|
||||
return LoginScreen(onSuccess: _onLoginDone);
|
||||
}
|
||||
return HomeScreen(
|
||||
baseUrl: s.base,
|
||||
db: s.db,
|
||||
user: s.user,
|
||||
pass: s.pass,
|
||||
onLogout: _logout,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key, required this.onSuccess});
|
||||
|
||||
final void Function({required String base, required String db, required String user, required String pass}) onSuccess;
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final _url = TextEditingController(); // mis http://<IP>:18069
|
||||
final _db = TextEditingController(text: 'odoo');
|
||||
final _user = TextEditingController(text: 'admin');
|
||||
final _pass = TextEditingController();
|
||||
bool _busy = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFF0B6E4F),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Icon(Icons.trending_up, color: Color(0xFF0B6E4F), size: 40),
|
||||
const SizedBox(height: 8),
|
||||
Text('Saat Invest', textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall),
|
||||
Text('Catat Jual-Beli Saham via Odoo',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
const SizedBox(height: 20),
|
||||
TextField(controller: _url,
|
||||
decoration: const InputDecoration(labelText: 'URL Server Odoo',
|
||||
hintText: 'http://192.168.1.5:18069',
|
||||
prefixIcon: Icon(Icons.dns))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(controller: _db,
|
||||
decoration: const InputDecoration(labelText: 'Database', prefixIcon: Icon(Icons.storage))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(controller: _user,
|
||||
decoration: const InputDecoration(labelText: 'Username', prefixIcon: Icon(Icons.person))),
|
||||
const SizedBox(height: 12),
|
||||
TextField(controller: _pass, obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password', prefixIcon: Icon(Icons.lock))),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: _busy ? null : _login,
|
||||
icon: _busy
|
||||
? const SizedBox(width: 18, height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))
|
||||
: const Icon(Icons.login),
|
||||
label: const Text('Masuk'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
setState(() { _busy = true; _error = null; });
|
||||
String base = _url.text.trim();
|
||||
if (base.isNotEmpty && !base.startsWith('http')) base = 'http://$base';
|
||||
try {
|
||||
final c = OdooClient(
|
||||
baseUrl: base, db: _db.text.trim(), username: _user.text.trim(),
|
||||
password: _pass.text,
|
||||
);
|
||||
await c.authenticate(); // hit; lempar bila gagal
|
||||
await Session.save(base, _db.text.trim(), _user.text.trim(), _pass.text);
|
||||
if (!mounted) return;
|
||||
widget.onSuccess(base: base, db: _db.text.trim(), user: _user.text.trim(), pass: _pass.text);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_busy = false;
|
||||
_error = e.toString().replaceFirst('Exception: ', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user