219 lines
6.8 KiB
Dart
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: ', '');
|
|
});
|
|
}
|
|
}
|
|
}
|