Files
saat-app/flutter/lib/main.dart
Alex Ardhian a41656d3c5 Flutter: tampilkan URL server tersimpan saat layar login berikutnya
- Session.clear() kini hapus sesi (user/pass) tapi sisakan URL terakhir
- + Session.readLastUrl()
- LoginScreen mengisi field URL dari URL terakhir saat dibuka
- sesudah logout login lagi URL sudah terisi, tak perlu ketik ulang
2026-09-02 18:14:32 +07:00

193 lines
6.2 KiB
Dart

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
void initState() {
super.initState();
_loadUrl();
}
Future<void> _loadUrl() async {
final url = await Session.readLastUrl();
if (url != null && mounted) {
setState(() => _url.text = url);
}
}
@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://169.58.149.164: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: ', '');
});
}
}
}