Files
saat-app/flutter/lib/main.dart
2026-09-06 04:31:16 +07:00

219 lines
6.8 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 {
// try {
// String base = await Session.readBase();
// String? db = await Session.readDb();
// String? pass = await Session.readPass();
// String? user = await Session.readUser();
//
// int uid = await Session.readUid() ?? 0;
//
// final c = OdooClient(
// baseUrl: base, db: db!, username: user!,
// password: pass!,
// );
//
// c.logout(uid, db, pass);
// } catch(e) {
// // leave it empty
// }
await Session.clear();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const RootScreen()),
(route) => false,
);
}
@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://127.0.0.1:8069',
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,
);
int _uid = await c.authenticate(); // hit; lempar bila gagal
if (_uid > 0) {
await Session.saveUid(_uid);
}
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: ', '');
});
}
}
}