perbarui login fix nama aplikasi serta icon aplikasi
|
|
@ -2,7 +2,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="finalproject"
|
android:label="Tobaku Sulastri"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true">
|
android:usesCleartextTraffic="true">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 945 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 700 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -15,6 +15,11 @@ subprojects {
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
subprojects {
|
||||||
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
|
options.compilerArgs.add("-Xlint:-options")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("clean") {
|
tasks.register<Delete>("clean") {
|
||||||
delete(rootProject.layout.buildDirectory)
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
org.gradle.java.home=C:/Program Files/Android/Android Studio1/jbr
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.javaCompile.suppressSourceTargetDeprecationWarning=true
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 311 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 768 B |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 739 B |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 952 B |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 915 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
|
@ -5,7 +5,7 @@
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Finalproject</string>
|
<string>Tobaku Sulastri</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>finalproject</string>
|
<string>Tobaku Sulastri</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ class MyApp extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Prediksi Stok Bahan Kue',
|
title: 'Tobaku Sulastri',
|
||||||
theme: AppTheme.lightTheme(),
|
theme: AppTheme.lightTheme(),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
navigatorObservers: [routeObserver],
|
navigatorObservers: [routeObserver],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:finalproject/services/auth_service.dart';
|
||||||
|
import 'package:finalproject/services/ml_service.dart';
|
||||||
|
|
||||||
class LoginController extends ChangeNotifier {
|
class LoginController extends ChangeNotifier {
|
||||||
final usernameController = TextEditingController(text: 'admin@sulastri.com');
|
final usernameController = TextEditingController();
|
||||||
final passwordController = TextEditingController(text: 'password');
|
final passwordController = TextEditingController();
|
||||||
|
|
||||||
bool isPasswordVisible = false;
|
bool isPasswordVisible = false;
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
String? errorMessage;
|
||||||
|
|
||||||
void togglePasswordVisibility() {
|
void togglePasswordVisibility() {
|
||||||
isPasswordVisible = !isPasswordVisible;
|
isPasswordVisible = !isPasswordVisible;
|
||||||
|
|
@ -24,13 +27,27 @@ class LoginController extends ChangeNotifier {
|
||||||
if (error != null) return false;
|
if (error != null) return false;
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
final result = await MLService.login(
|
||||||
|
usernameOrEmail: usernameController.text.trim(),
|
||||||
|
password: passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
final success = result['status'] == 'success';
|
||||||
|
if (success) {
|
||||||
|
final data = result['data'];
|
||||||
|
await AuthService.saveLoginSession(
|
||||||
|
name: data is Map ? data['name']?.toString() : null,
|
||||||
|
email: data is Map ? data['email']?.toString() : null,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errorMessage = result['message']?.toString() ?? 'Login gagal';
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,27 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
Navigator.of(context).pushReplacementNamed('/dashboard');
|
Navigator.of(context).pushReplacementNamed('/dashboard');
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(_controller.errorMessage ?? 'Login gagal'),
|
||||||
|
backgroundColor: AppColors.statusError,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onForgotPasswordPressed() {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text(
|
||||||
|
'Fitur lupa password dengan OTP akan dikembangkan pada tahap berikutnya.',
|
||||||
|
),
|
||||||
|
backgroundColor: AppColors.primaryBrown,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
|
|
@ -193,7 +211,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
_controller.isLoading ? null : () {},
|
_controller.isLoading
|
||||||
|
? null
|
||||||
|
: _onForgotPasswordPressed,
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
|
@ -247,35 +267,13 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.bgLight,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: AppColors.grey300),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Demo: Klik "Masuk" untuk melanjutkan',
|
|
||||||
style: AppTextStyles.bodySmall.copyWith(
|
|
||||||
color: AppColors.statusError,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
Text(
|
Text(
|
||||||
'© 2025 Toko Bahan Kue Sulastri',
|
'(c) 2025 Tobaku Sulastri',
|
||||||
style: AppTextStyles.bodySmall.copyWith(
|
style: AppTextStyles.bodySmall.copyWith(
|
||||||
color: AppColors.textTertiary,
|
color: AppColors.textTertiary,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:finalproject/theme/colors.dart';
|
import 'package:finalproject/theme/colors.dart';
|
||||||
import 'package:finalproject/theme/text_styles.dart';
|
import 'package:finalproject/theme/text_styles.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:finalproject/services/auth_service.dart';
|
||||||
|
|
||||||
import 'settings_controller.dart';
|
import 'settings_controller.dart';
|
||||||
|
|
||||||
|
|
@ -51,8 +52,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
await AuthService.logout();
|
||||||
|
if (!context.mounted) return;
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
context,
|
context,
|
||||||
).pushNamedAndRemoveUntil('/login', (route) => false);
|
).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||||
|
|
@ -341,7 +344,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'© 2025 Toko Bahan Kue Sulastri',
|
'© 2025 Tobaku Sulastri',
|
||||||
style: AppTextStyles.bodySmall.copyWith(
|
style: AppTextStyles.bodySmall.copyWith(
|
||||||
color: AppColors.textTertiary,
|
color: AppColors.textTertiary,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:finalproject/services/auth_service.dart';
|
||||||
import 'package:finalproject/theme/colors.dart';
|
import 'package:finalproject/theme/colors.dart';
|
||||||
|
|
||||||
class SplashScreen extends StatefulWidget {
|
class SplashScreen extends StatefulWidget {
|
||||||
const SplashScreen({Key? key}) : super(key: key);
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SplashScreen> createState() => _SplashScreenState();
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
|
|
@ -63,10 +64,12 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
// Tunggu sebentar sebelum navigate
|
// Tunggu sebentar sebelum navigate
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
|
||||||
if (mounted) {
|
final isLoggedIn = await AuthService.isLoggedIn();
|
||||||
// Navigate ke Login
|
if (!mounted) return;
|
||||||
Navigator.of(context).pushReplacementNamed('/login');
|
|
||||||
}
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pushReplacementNamed(isLoggedIn ? '/dashboard' : '/login');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _statusMessage = 'Error: ${e.toString()}');
|
setState(() => _statusMessage = 'Error: ${e.toString()}');
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
@ -148,16 +151,17 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color: Colors.black.withValues(alpha: 0.15),
|
||||||
blurRadius: 25,
|
blurRadius: 25,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.all(18),
|
||||||
'🍰',
|
child: Image.asset(
|
||||||
style: TextStyle(fontSize: 60),
|
'assets/branding/app_icon.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -179,7 +183,7 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
children: [
|
children: [
|
||||||
// Main title
|
// Main title
|
||||||
Text(
|
Text(
|
||||||
'Sulastri',
|
'Tobaku Sulastri',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
@ -301,6 +305,16 @@ class _SplashScreenState extends State<SplashScreen>
|
||||||
),
|
),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
_statusMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: AppColors.textTertiary,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Version text
|
// Version text
|
||||||
Text(
|
Text(
|
||||||
'Version 1.0.0',
|
'Version 1.0.0',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
static const String _isLoggedInKey = 'is_logged_in';
|
||||||
|
static const String _userNameKey = 'user_name';
|
||||||
|
static const String _userEmailKey = 'user_email';
|
||||||
|
|
||||||
|
static Future<void> saveLoginSession({String? name, String? email}) async {
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
await preferences.setBool(_isLoggedInKey, true);
|
||||||
|
|
||||||
|
if (name != null && name.isNotEmpty) {
|
||||||
|
await preferences.setString(_userNameKey, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email != null && email.isNotEmpty) {
|
||||||
|
await preferences.setString(_userEmailKey, email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> isLoggedIn() async {
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
return preferences.getBool(_isLoggedInKey) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> logout() async {
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
await preferences.remove(_isLoggedInKey);
|
||||||
|
await preferences.remove(_userNameKey);
|
||||||
|
await preferences.remove(_userEmailKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,10 +5,42 @@ class MLService {
|
||||||
// API URL - Change based on environment
|
// API URL - Change based on environment
|
||||||
// Untuk emulator Android: 10.0.2.2
|
// Untuk emulator Android: 10.0.2.2
|
||||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
||||||
static const String baseUrl = 'http://192.168.1.72:5000';
|
static const String baseUrl = 'http://192.168.18.30:5000';
|
||||||
|
|
||||||
static const int timeoutSeconds = 30;
|
static const int timeoutSeconds = 30;
|
||||||
|
|
||||||
|
/// Login using account data from MySQL login table.
|
||||||
|
static Future<Map<String, dynamic>> login({
|
||||||
|
required String usernameOrEmail,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await http
|
||||||
|
.post(
|
||||||
|
Uri.parse('$baseUrl/api/login'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'email': usernameOrEmail,
|
||||||
|
'username': usernameOrEmail,
|
||||||
|
'password': password,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.timeout(Duration(seconds: timeoutSeconds));
|
||||||
|
|
||||||
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'status': 'error', 'message': body['message'] ?? 'Login gagal'};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Tidak dapat terhubung ke server: $e',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert grams to kilograms with rounding rules:
|
/// Convert grams to kilograms with rounding rules:
|
||||||
/// - <= 500g -> 0.5kg
|
/// - <= 500g -> 0.5kg
|
||||||
/// - 500g < g <= 1000g -> 1kg
|
/// - 500g < g <= 1000g -> 1kg
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ static void my_application_activate(GApplication* application) {
|
||||||
if (use_header_bar) {
|
if (use_header_bar) {
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
gtk_header_bar_set_title(header_bar, "finalproject");
|
gtk_header_bar_set_title(header_bar, "Tobaku Sulastri");
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
} else {
|
} else {
|
||||||
gtk_window_set_title(window, "finalproject");
|
gtk_window_set_title(window, "Tobaku Sulastri");
|
||||||
}
|
}
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ import Foundation
|
||||||
|
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 851 B |
|
|
@ -5,10 +5,10 @@
|
||||||
// 'flutter create' template.
|
// 'flutter create' template.
|
||||||
|
|
||||||
// The application's name. By default this is also the title of the Flutter window.
|
// The application's name. By default this is also the title of the Flutter window.
|
||||||
PRODUCT_NAME = finalproject
|
PRODUCT_NAME = Tobaku Sulastri
|
||||||
|
|
||||||
// The application's bundle identifier
|
// The application's bundle identifier
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.finalproject
|
PRODUCT_BUNDLE_IDENTIFIER = com.example.finalproject
|
||||||
|
|
||||||
// The copyright displayed in application information
|
// The copyright displayed in application information
|
||||||
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
|
PRODUCT_COPYRIGHT = Copyright (c) 2026 Tobaku Sulastri. All rights reserved.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
from mysql.connector import Error
|
from mysql.connector import Error
|
||||||
|
|
@ -97,6 +98,14 @@ def build_report_response(success: bool, message: str, data: list | None = None,
|
||||||
'data': data or []
|
'data': data or []
|
||||||
}), status_code
|
}), status_code
|
||||||
|
|
||||||
|
def verify_login_password(input_password: str, stored_password: str) -> bool:
|
||||||
|
"""Accept plain text passwords and SHA-256 hashes for simple local auth."""
|
||||||
|
if input_password == stored_password:
|
||||||
|
return True
|
||||||
|
|
||||||
|
hashed_input = hashlib.sha256(input_password.encode('utf-8')).hexdigest()
|
||||||
|
return hashed_input == stored_password
|
||||||
|
|
||||||
def ensure_prediction_needs_column(connection, table_name: str = 'predictions') -> str | None:
|
def ensure_prediction_needs_column(connection, table_name: str = 'predictions') -> str | None:
|
||||||
needs_col = get_existing_column(
|
needs_col = get_existing_column(
|
||||||
connection,
|
connection,
|
||||||
|
|
@ -477,6 +486,64 @@ def get_metadata():
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
"""Login using email/username and password stored in the login table."""
|
||||||
|
try:
|
||||||
|
data = request.json or {}
|
||||||
|
username_or_email = (data.get('email') or data.get('username') or '').strip()
|
||||||
|
password = data.get('password') or ''
|
||||||
|
|
||||||
|
if not username_or_email or not password:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Email/username dan password wajib diisi'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
connection = get_db_connection()
|
||||||
|
if connection is None:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Tidak dapat terhubung ke database'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, name, email, username, password
|
||||||
|
FROM login
|
||||||
|
WHERE email = %s OR username = %s
|
||||||
|
LIMIT 1
|
||||||
|
""", (username_or_email, username_or_email))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
if user is None or not verify_login_password(password, user['password']):
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Email/username atau password salah'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
user.pop('password', None)
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Login berhasil',
|
||||||
|
'data': user
|
||||||
|
}), 200
|
||||||
|
except Error as e:
|
||||||
|
logger.error(f"Login database error: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Tabel login belum tersedia atau database bermasalah'
|
||||||
|
}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Login error: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': 'Terjadi kesalahan saat login'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/prediksi', methods=['POST'])
|
@app.route('/prediksi', methods=['POST'])
|
||||||
def prediksi():
|
def prediksi():
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,19 @@ def create_tables():
|
||||||
""")
|
""")
|
||||||
logger.info("✅ Predictions table created successfully")
|
logger.info("✅ Predictions table created successfully")
|
||||||
|
|
||||||
|
# Login table
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS login (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
logger.info("Login table created successfully")
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
cursor.close()
|
cursor.close()
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
@ -136,6 +149,29 @@ def insert_default_products():
|
||||||
print(f"❌ Error inserting products: {err}")
|
print(f"❌ Error inserting products: {err}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def insert_default_login():
|
||||||
|
"""Insert default login account"""
|
||||||
|
try:
|
||||||
|
connection = mysql.connector.connect(**DB_CONFIG)
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO login (name, email, username, password)
|
||||||
|
VALUES (%s, %s, %s, %s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
username = VALUES(username)
|
||||||
|
""", ('Ibu Sulastri', 'admin@sulastri.com', 'admin', 'password'))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
logger.info("Default login account ready")
|
||||||
|
cursor.close()
|
||||||
|
connection.close()
|
||||||
|
return True
|
||||||
|
except Error as err:
|
||||||
|
print(f"Error inserting login account: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
def verify_connection():
|
def verify_connection():
|
||||||
"""Verify database connection"""
|
"""Verify database connection"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -171,6 +207,9 @@ def main():
|
||||||
if not insert_default_products():
|
if not insert_default_products():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not insert_default_login():
|
||||||
|
return
|
||||||
|
|
||||||
verify_connection()
|
verify_connection()
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,16 @@ CREATE TABLE IF NOT EXISTS predictions (
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Create Login Table
|
||||||
|
CREATE TABLE IF NOT EXISTS login (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- Create Recipes Table
|
-- Create Recipes Table
|
||||||
CREATE TABLE IF NOT EXISTS recipes (
|
CREATE TABLE IF NOT EXISTS recipes (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|
@ -88,6 +98,13 @@ INSERT INTO products (name, category, product_type, unit, price, current_stock)
|
||||||
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10),
|
('Keju Parut 250gr', 'Keju', 'Bahan', 'kg', 28000, 10),
|
||||||
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35);
|
('Baking Powder', 'Bahan Tambahan', 'Bahan', 'kg', 8000, 35);
|
||||||
|
|
||||||
|
-- Insert Default Login Account
|
||||||
|
INSERT INTO login (name, email, username, password) VALUES
|
||||||
|
('Ibu Sulastri', 'admin@sulastri.com', 'admin', 'password')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
username = VALUES(username);
|
||||||
|
|
||||||
-- Insert Sample Recipes
|
-- Insert Sample Recipes
|
||||||
INSERT INTO recipes (recipe_name, description) VALUES
|
INSERT INTO recipes (recipe_name, description) VALUES
|
||||||
('Donat', 'Resep donat lezat'),
|
('Donat', 'Resep donat lezat'),
|
||||||
|
|
|
||||||
56
pubspec.lock
|
|
@ -392,6 +392,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.2"
|
version: "5.0.2"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.3"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.13"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.4"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ dependencies:
|
||||||
# HTTP Client untuk API
|
# HTTP Client untuk API
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
|
|
||||||
|
# Local storage untuk menyimpan status login
|
||||||
|
shared_preferences: ^2.2.3
|
||||||
|
|
||||||
# Intl untuk date formatting
|
# Intl untuk date formatting
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
|
||||||
|
|
@ -73,6 +76,9 @@ flutter:
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/branding/app_icon.png
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
# assets:
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 6.3 KiB |
|
|
@ -23,13 +23,13 @@
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="finalproject">
|
<meta name="apple-mobile-web-app-title" content="Tobaku Sulastri">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>finalproject</title>
|
<title>Tobaku Sulastri</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "finalproject",
|
"name": "Tobaku Sulastri",
|
||||||
"short_name": "finalproject",
|
"short_name": "Tobaku",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#F8F1E8",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#8B5A3C",
|
||||||
"description": "A new Flutter project.",
|
"description": "Aplikasi prediksi permintaan dan stok bahan kue Tobaku Sulastri.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
"icons": [
|
"icons": [
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,12 @@ BEGIN
|
||||||
BLOCK "040904e4"
|
BLOCK "040904e4"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "com.example" "\0"
|
VALUE "CompanyName", "com.example" "\0"
|
||||||
VALUE "FileDescription", "finalproject" "\0"
|
VALUE "FileDescription", "Tobaku Sulastri" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
VALUE "InternalName", "finalproject" "\0"
|
VALUE "InternalName", "Tobaku Sulastri" "\0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "finalproject.exe" "\0"
|
VALUE "OriginalFilename", "finalproject.exe" "\0"
|
||||||
VALUE "ProductName", "finalproject" "\0"
|
VALUE "ProductName", "Tobaku Sulastri" "\0"
|
||||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"finalproject", origin, size)) {
|
if (!window.Create(L"Tobaku Sulastri", origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 22 KiB |