chore: stabilize app startup, auth flow, and cross-platform UI

This commit is contained in:
Wizznu 2026-03-27 23:11:12 +07:00
parent d3cc18a6bc
commit f51a0ef11f
59 changed files with 1324 additions and 1338 deletions

@ -0,0 +1 @@
Subproject commit f7354e32a95b4459b5482b57f10ea1c228b7629a

View File

@ -8,7 +8,7 @@ plugins {
android { android {
namespace = "com.example.telurku" namespace = "com.example.telurku"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
@ -24,7 +24,7 @@ android {
applicationId = "com.example.telurku" applicationId = "com.example.telurku"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = 23
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 67 KiB

BIN
assets/icons/telurku.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

View File

@ -1,122 +1 @@
{ {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -47,6 +47,8 @@ class DefaultFirebaseOptions {
messagingSenderId: '22111260761', messagingSenderId: '22111260761',
projectId: 'telurku-fa78c', projectId: 'telurku-fa78c',
storageBucket: 'telurku-fa78c.firebasestorage.app', storageBucket: 'telurku-fa78c.firebasestorage.app',
databaseURL:
'https://telurku-fa78c-default-rtdb.asia-southeast1.firebasedatabase.app',
); );
static const FirebaseOptions ios = FirebaseOptions( static const FirebaseOptions ios = FirebaseOptions(

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'providers/auth_provider.dart'; import 'providers/auth_provider.dart';
@ -16,11 +18,18 @@ import 'screens/home_page.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Realtime DB persistence tidak didukung di semua platform (terutama web).
if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS)) {
FirebaseDatabase.instance.setPersistenceEnabled(true);
}
// Initialize background scheduler untuk panen otomatis // Initialize background scheduler untuk panen otomatis
// TODO: Enable setelah scheduler stabil // TODO: Enable setelah scheduler stabil
// await PanenScheduler.initialize(); // await PanenScheduler.initialize();
runApp(const MyApp()); runApp(const MyApp());
} }
@ -52,7 +61,7 @@ class MyApp extends StatelessWidget {
'/landing': (context) => const LandingPage(), '/landing': (context) => const LandingPage(),
'/login': (context) => const LoginPage(), '/login': (context) => const LoginPage(),
'/signup': (context) => const SignupPage(), '/signup': (context) => const SignupPage(),
'/home': (context) => const HomePage(), '/home': (context) => const AuthWrapper(),
}, },
), ),
); );
@ -73,7 +82,11 @@ class AuthWrapper extends StatelessWidget {
} }
if (authProvider.isLoggedIn) { if (authProvider.isLoggedIn) {
return const HomePage(); final userId = authProvider.user?.uid;
if (userId == null) {
return const LandingPage();
}
return HomeBootstrap(userId: userId);
} }
return const LandingPage(); return const LandingPage();
@ -81,3 +94,77 @@ class AuthWrapper extends StatelessWidget {
); );
} }
} }
class HomeBootstrap extends StatefulWidget {
final String userId;
const HomeBootstrap({super.key, required this.userId});
@override
State<HomeBootstrap> createState() => _HomeBootstrapState();
}
class _HomeBootstrapState extends State<HomeBootstrap> {
late Future<void> _initFuture;
@override
void initState() {
super.initState();
_initFuture = _initializeData();
}
Future<void> _initializeData() async {
final penjadwalanProvider = context.read<PenjadwalanProvider>();
final kandangProvider = context.read<KandangProvider>();
final panenProvider = context.read<PanenProvider>();
await penjadwalanProvider.initializeWithUser(widget.userId);
await kandangProvider.initializeWithUser(widget.userId);
await panenProvider.loadTodaySnapshots();
await panenProvider.restorePanenHistoryFromFirebase();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _initFuture,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Gagal memuat data awal. Coba buka ulang aplikasi.',
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
setState(() {
_initFuture = _initializeData();
});
},
child: const Text('Coba Lagi'),
),
],
),
),
),
);
}
return const HomePage();
},
);
}
}

View File

@ -42,6 +42,11 @@ class AuthProvider extends ChangeNotifier {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
return false; return false;
} catch (_) {
_errorMessage = 'Terjadi kesalahan tak terduga. Coba lagi.';
_isLoading = false;
notifyListeners();
return false;
} }
} }
@ -65,6 +70,11 @@ class AuthProvider extends ChangeNotifier {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
return false; return false;
} catch (_) {
_errorMessage = 'Terjadi kesalahan tak terduga. Coba lagi.';
_isLoading = false;
notifyListeners();
return false;
} }
} }
@ -100,6 +110,11 @@ class AuthProvider extends ChangeNotifier {
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
return false; return false;
} catch (_) {
_errorMessage = 'Terjadi kesalahan tak terduga. Coba lagi.';
_isLoading = false;
notifyListeners();
return false;
} }
} }

View File

@ -6,13 +6,14 @@ import '../models/kandang_model.dart';
class KandangProvider extends ChangeNotifier { class KandangProvider extends ChangeNotifier {
final FirebaseDatabase _database = FirebaseDatabase.instance; final FirebaseDatabase _database = FirebaseDatabase.instance;
late DatabaseReference _kandangRef; late final DatabaseReference _kandangRef = _database.ref(_sharedKandangPath);
static const String _sharedKandangPath = 'kontrol/kandang'; static const String _sharedKandangPath = 'kontrol/kandang';
List<Kandang> _kandangs = []; List<Kandang> _kandangs = [];
bool _isLoading = true; bool _isLoading = true;
StreamSubscription? _subscription; StreamSubscription? _subscription;
StreamSubscription? _sensorSubscription; StreamSubscription? _sensorSubscription;
String? _activeUserId;
// Sensor Real-time Listeners // Sensor Real-time Listeners
int _infra1Value = 0; int _infra1Value = 0;
@ -25,7 +26,10 @@ class KandangProvider extends ChangeNotifier {
/// Initialize provider dengan user ID dan load data dari Firebase /// Initialize provider dengan user ID dan load data dari Firebase
Future<void> initializeWithUser(String userId) async { Future<void> initializeWithUser(String userId) async {
_kandangRef = _database.ref(_sharedKandangPath); if (_activeUserId == userId && _subscription != null) {
return;
}
_activeUserId = userId;
try { try {
// Pastikan ada data awal agar user baru tidak selalu input dari nol. // Pastikan ada data awal agar user baru tidak selalu input dari nol.
@ -91,14 +95,21 @@ class KandangProvider extends ChangeNotifier {
Future<void> _ensureInitialKandangData(String userId) async { Future<void> _ensureInitialKandangData(String userId) async {
final sharedSnapshot = await _kandangRef.get(); final sharedSnapshot = await _kandangRef.get();
if (sharedSnapshot.exists) return; if (sharedSnapshot.exists && sharedSnapshot.value is Map) {
final current = Map<dynamic, dynamic>.from(sharedSnapshot.value as Map);
if (current.isNotEmpty) return;
}
try { try {
final legacyRef = _database.ref('users/$userId/kandang'); final legacyRef = _database.ref('users/$userId/kandang');
final legacySnapshot = await legacyRef.get(); final legacySnapshot = await legacyRef.get();
if (legacySnapshot.exists) { if (legacySnapshot.exists && legacySnapshot.value is Map) {
await _kandangRef.set(legacySnapshot.value); final legacyData =
return; Map<dynamic, dynamic>.from(legacySnapshot.value as Map);
if (legacyData.isNotEmpty) {
await _kandangRef.set(legacyData);
return;
}
} }
} catch (e) { } catch (e) {
if (kDebugMode) { if (kDebugMode) {
@ -126,6 +137,7 @@ class KandangProvider extends ChangeNotifier {
Future<void> clearData() async { Future<void> clearData() async {
await _subscription?.cancel(); await _subscription?.cancel();
await _sensorSubscription?.cancel(); await _sensorSubscription?.cancel();
_activeUserId = null;
_kandangs = []; _kandangs = [];
_isLoading = true; _isLoading = true;
_infra1Value = 0; _infra1Value = 0;

View File

@ -5,18 +5,24 @@ import '../models/penjadwalan_model.dart';
class PenjadwalanProvider extends ChangeNotifier { class PenjadwalanProvider extends ChangeNotifier {
final FirebaseDatabase _database = FirebaseDatabase.instance; final FirebaseDatabase _database = FirebaseDatabase.instance;
late DatabaseReference _penjadwalanRef; late final DatabaseReference _penjadwalanRef = _database.ref(
'kontrol/penjadwalan',
);
List<Penjadwalan> _penjadwalans = []; List<Penjadwalan> _penjadwalans = [];
bool _isLoading = true; bool _isLoading = true;
StreamSubscription? _subscription; StreamSubscription? _subscription;
String? _activeUserId;
List<Penjadwalan> get penjadwalans => _penjadwalans; List<Penjadwalan> get penjadwalans => _penjadwalans;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
/// Initialize provider dengan user ID dan load data dari Firebase /// Initialize provider dengan user ID dan load data dari Firebase
Future<void> initializeWithUser(String userId) async { Future<void> initializeWithUser(String userId) async {
_penjadwalanRef = _database.ref('kontrol/penjadwalan'); if (_activeUserId == userId && _subscription != null) {
return;
}
_activeUserId = userId;
try { try {
// Pastikan jadwal awal tersedia agar login pertama langsung ada isi. // Pastikan jadwal awal tersedia agar login pertama langsung ada isi.
@ -81,7 +87,10 @@ class PenjadwalanProvider extends ChangeNotifier {
Future<void> _ensureInitialPenjadwalanData() async { Future<void> _ensureInitialPenjadwalanData() async {
final snapshot = await _penjadwalanRef.get(); final snapshot = await _penjadwalanRef.get();
if (snapshot.exists) return; if (snapshot.exists && snapshot.value is Map) {
final current = Map<dynamic, dynamic>.from(snapshot.value as Map);
if (current.isNotEmpty) return;
}
await _penjadwalanRef.set({ await _penjadwalanRef.set({
'penjadwalan1': { 'penjadwalan1': {
@ -118,6 +127,7 @@ class PenjadwalanProvider extends ChangeNotifier {
/// Clear semua data saat logout /// Clear semua data saat logout
Future<void> clearData() async { Future<void> clearData() async {
await _subscription?.cancel(); await _subscription?.cancel();
_activeUserId = null;
_penjadwalans = []; _penjadwalans = [];
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();

View File

@ -9,7 +9,6 @@ class TelurProvider extends ChangeNotifier {
int _kandang1HariIni = 0; int _kandang1HariIni = 0;
int _kandang2HariIni = 0; int _kandang2HariIni = 0;
int _totalTelur = 0; int _totalTelur = 0;
String _lastResetDate = '';
bool _isSynced = false; bool _isSynced = false;
int get telurHariIni => _telurHariIni; int get telurHariIni => _telurHariIni;
@ -18,6 +17,20 @@ class TelurProvider extends ChangeNotifier {
int get totalTelur => _totalTelur; int get totalTelur => _totalTelur;
bool get isSynced => _isSynced; bool get isSynced => _isSynced;
int _toInt(dynamic value) {
if (value is int) return value;
if (value is num) return value.toInt();
return int.tryParse('${value ?? 0}') ?? 0;
}
int _readKandang1(Map<dynamic, dynamic> data) {
return _toInt(data['kandang1_hari_ini'] ?? data['kandang_1_hari_ini']);
}
int _readKandang2(Map<dynamic, dynamic> data) {
return _toInt(data['kandang2_hari_ini'] ?? data['kandang_2_hari_ini']);
}
TelurProvider() { TelurProvider() {
_riwayatRef = _database.ref('riwayat/summary'); _riwayatRef = _database.ref('riwayat/summary');
_initializeRiwayat(); _initializeRiwayat();
@ -46,14 +59,10 @@ class TelurProvider extends ChangeNotifier {
_riwayatRef.onValue.listen((event) { _riwayatRef.onValue.listen((event) {
if (event.snapshot.exists) { if (event.snapshot.exists) {
final data = event.snapshot.value as Map<dynamic, dynamic>; final data = event.snapshot.value as Map<dynamic, dynamic>;
_telurHariIni = data['telur_hari_ini'] ?? 0; _telurHariIni = _toInt(data['telur_hari_ini']);
_kandang1HariIni = data['kandang1_hari_ini'] ?? 0; _kandang1HariIni = _readKandang1(data);
_kandang2HariIni = data['kandang2_hari_ini'] ?? 0; _kandang2HariIni = _readKandang2(data);
_totalTelur = data['total_telur'] ?? 0; _totalTelur = _toInt(data['total_telur']);
_lastResetDate = data['last_reset_date'] ?? '';
// Check if auto-reset is needed
_checkAndResetIfNeeded();
notifyListeners(); notifyListeners();
} }
@ -72,11 +81,10 @@ class TelurProvider extends ChangeNotifier {
final snapshot = await _riwayatRef.get(); final snapshot = await _riwayatRef.get();
if (snapshot.exists) { if (snapshot.exists) {
final data = snapshot.value as Map<dynamic, dynamic>; final data = snapshot.value as Map<dynamic, dynamic>;
_telurHariIni = data['telur_hari_ini'] ?? 0; _telurHariIni = _toInt(data['telur_hari_ini']);
_kandang1HariIni = data['kandang1_hari_ini'] ?? 0; _kandang1HariIni = _readKandang1(data);
_kandang2HariIni = data['kandang2_hari_ini'] ?? 0; _kandang2HariIni = _readKandang2(data);
_totalTelur = data['total_telur'] ?? 0; _totalTelur = _toInt(data['total_telur']);
_lastResetDate = data['last_reset_date'] ?? '';
notifyListeners(); notifyListeners();
} }
} catch (e) { } catch (e) {
@ -84,33 +92,8 @@ class TelurProvider extends ChangeNotifier {
} }
} }
Future<void> _checkAndResetIfNeeded() async {
try {
final todayDate = DateTime.now().toIso8601String().split('T')[0];
if (_lastResetDate != todayDate) {
// Reset telur_hari_ini for new day
await _riwayatRef.update({
'telur_hari_ini': 0,
'kandang1_hari_ini': 0,
'kandang2_hari_ini': 0,
'last_reset_date': todayDate,
});
_telurHariIni = 0;
_kandang1HariIni = 0;
_kandang2HariIni = 0;
notifyListeners();
}
} catch (e) {
print('Error checking/resetting telur data: $e');
}
}
Future<void> addTelurHariIni(int jumlah) async { Future<void> addTelurHariIni(int jumlah) async {
try { try {
// Check and reset if needed first
await _checkAndResetIfNeeded();
final newTotal = _telurHariIni + jumlah; final newTotal = _telurHariIni + jumlah;
await _riwayatRef.update({'telur_hari_ini': newTotal}); await _riwayatRef.update({'telur_hari_ini': newTotal});
_telurHariIni = newTotal; _telurHariIni = newTotal;

View File

@ -13,529 +13,524 @@ class DashboardPage extends StatelessWidget {
final kandangProvider = context.watch<KandangProvider>(); final kandangProvider = context.watch<KandangProvider>();
final telurProvider = context.watch<TelurProvider>(); final telurProvider = context.watch<TelurProvider>();
return Scaffold( return Container(
body: Container( decoration: BoxDecoration(
decoration: BoxDecoration( gradient: LinearGradient(
gradient: LinearGradient( begin: Alignment.topCenter,
begin: Alignment.topCenter, end: Alignment.bottomCenter,
end: Alignment.bottomCenter, colors: [
colors: [ Colors.orange.shade100,
Colors.orange.shade100, Colors.amber.shade100,
Colors.amber.shade100, Colors.greenAccent.shade100,
Colors.greenAccent.shade100, ],
],
),
), ),
child: SafeArea( ),
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header // Header
Text( Text(
'Dashboard', 'Dashboard',
style: TextStyle( style: TextStyle(
fontSize: 28, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.orange.shade700, color: Colors.orange.shade700,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Ringkasan produksi telur hari ini', 'Ringkasan produksi telur hari ini',
style: TextStyle(fontSize: 14, color: Colors.grey.shade600), style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
// Total Telur Keseluruhan // Total Telur Keseluruhan
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [Colors.orange.shade300, Colors.orange.shade600], colors: [Colors.orange.shade300, Colors.orange.shade600],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.orange.shade300.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total Telur Hari Ini',
style: TextStyle(
fontSize: 16,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
), ),
),
const SizedBox(height: 12),
Row(
children: [
Text(
'${telurProvider.telurHariIni}',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(width: 12),
Text('🥚', style: TextStyle(fontSize: 40)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Text(
'Kandang 1: ${telurProvider.kandang1HariIni}',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
'Kandang 2: ${telurProvider.kandang2HariIni}',
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
const SizedBox(height: 30),
// Jadwal Panen Hari Ini
Consumer<PenjadwalanProvider>(
builder: (context, penjadwalanProvider, _) {
final jadwals = penjadwalanProvider.penjadwalans
.where((j) => j.aktif)
.toList();
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.orange.shade300.withOpacity(0.3), color: Colors.black.withOpacity(0.1),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 5), offset: const Offset(0, 5),
), ),
], ],
), ),
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Total Telur Hari Ini', 'Jadwal Panen Hari Ini',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.white.withOpacity(0.9), fontWeight: FontWeight.bold,
fontWeight: FontWeight.w500, color: Colors.grey.shade800,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( if (jadwals.isEmpty)
children: [ Center(
Text( child: Text(
'${telurProvider.telurHariIni}', 'Tidak ada jadwal panen hari ini',
style: const TextStyle( style: TextStyle(color: Colors.grey.shade600),
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
), ),
const SizedBox(width: 12), )
Text('🥚', style: TextStyle(fontSize: 40)), else
], ListView.builder(
), shrinkWrap: true,
const SizedBox(height: 12), physics: const NeverScrollableScrollPhysics(),
Row( itemCount: jadwals.length,
children: [ itemBuilder: (context, idx) {
Expanded( final jadwal = jadwals[idx];
child: Text( return Container(
'Kandang 1: ${telurProvider.kandang1HariIni}', margin: const EdgeInsets.only(bottom: 8),
style: TextStyle( padding: const EdgeInsets.all(12),
fontSize: 12, decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9), color: Colors.amber.shade50,
fontWeight: FontWeight.w600, borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.orange.shade200,
),
), ),
), child: Row(
), children: [
Expanded( Container(
child: Text( padding: const EdgeInsets.all(8),
'Kandang 2: ${telurProvider.kandang2HariIni}', decoration: BoxDecoration(
textAlign: TextAlign.right, color: Colors.orange.shade100,
style: TextStyle( borderRadius: BorderRadius.circular(
fontSize: 12, 8,
color: Colors.white.withOpacity(0.9), ),
fontWeight: FontWeight.w600, ),
), child: Icon(
), Icons.schedule,
), color: Colors.orange.shade600,
], size: 20,
),
],
),
),
const SizedBox(height: 30),
// Jadwal Panen Hari Ini
Consumer<PenjadwalanProvider>(
builder: (context, penjadwalanProvider, _) {
final jadwals = penjadwalanProvider.penjadwalans
.where((j) => j.aktif)
.toList();
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Panen Hari Ini',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 12),
if (jadwals.isEmpty)
Center(
child: Text(
'Tidak ada jadwal panen hari ini',
style: TextStyle(color: Colors.grey.shade600),
),
)
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: jadwals.length,
itemBuilder: (context, idx) {
final jadwal = jadwals[idx];
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.orange.shade200,
), ),
), ),
child: Row( const SizedBox(width: 12),
children: [ Expanded(
Container( child: Column(
padding: const EdgeInsets.all(8), crossAxisAlignment:
decoration: BoxDecoration( CrossAxisAlignment.start,
color: Colors.orange.shade100, children: [
borderRadius: BorderRadius.circular( Row(
8,
),
),
child: Icon(
Icons.schedule,
color: Colors.orange.shade600,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
jadwal.jam,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
'(${jadwal.durasi})',
style: TextStyle(
fontSize: 11,
color:
Colors.orange.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
Text(
jadwal.keterangan,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(
20,
),
),
child: Text(
'Aktif',
style: TextStyle(
fontSize: 12,
color: Colors.green.shade700,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
},
),
],
),
);
},
),
const SizedBox(height: 30),
// Status Panen Terakhir
Consumer<PanenProvider>(
builder: (context, panenProvider, _) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status Panen Terakhir',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: kandangProvider.kandangs.length,
itemBuilder: (context, idx) {
final kandang = kandangProvider.kandangs[idx];
final lastHarvest = panenProvider
.getLastHarvestStatus(kandang.id);
if (lastHarvest == null) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
),
),
child: Row(
children: [
Icon(
Icons.info,
color: Colors.grey.shade400,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
kandang.nama, jadwal.jam,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
'(${jadwal.durasi})',
style: TextStyle(
fontSize: 11,
color: Colors.orange.shade600,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
const SizedBox(height: 2),
Text(
'Belum ada riwayat panen',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
], ],
), ),
), const SizedBox(height: 4),
], Text(
jadwal.keterangan,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
), ),
); Container(
} padding: const EdgeInsets.symmetric(
horizontal: 12,
return Container( vertical: 6,
margin: const EdgeInsets.only(bottom: 8), ),
padding: const EdgeInsets.all(12), decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.green.shade100,
color: Colors.green.shade50, borderRadius: BorderRadius.circular(
borderRadius: BorderRadius.circular(12), 20,
border: Border.all(
color: Colors.green.shade200,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.check_circle,
color: Colors.green.shade600,
size: 20,
), ),
), ),
const SizedBox(width: 12), child: Text(
Expanded( 'Aktif',
child: Column( style: TextStyle(
crossAxisAlignment: fontSize: 12,
CrossAxisAlignment.start, color: Colors.green.shade700,
children: [ fontWeight: FontWeight.bold,
Row( ),
children: [ ),
Text( ),
kandang.nama, ],
style: const TextStyle( ),
fontSize: 14, );
fontWeight: FontWeight.bold, },
), ),
), ],
const SizedBox(width: 8), ),
Text( );
'(${lastHarvest.jenisPanen?.toUpperCase() ?? 'MANUAL'})', },
style: TextStyle( ),
fontSize: 11, const SizedBox(height: 30),
color: Colors.green.shade600,
fontWeight: FontWeight.w500, // Status Panen Terakhir
), Consumer<PanenProvider>(
), builder: (context, panenProvider, _) {
], return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status Panen Terakhir',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: kandangProvider.kandangs.length,
itemBuilder: (context, idx) {
final kandang = kandangProvider.kandangs[idx];
final lastHarvest =
panenProvider.getLastHarvestStatus(kandang.id);
if (lastHarvest == null) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey.shade200,
),
),
child: Row(
children: [
Icon(
Icons.info,
color: Colors.grey.shade400,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
kandang.nama,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
), ),
const SizedBox(height: 2), ),
const SizedBox(height: 2),
Text(
'Belum ada riwayat panen',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
}
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.green.shade200,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.check_circle,
color: Colors.green.shade600,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
Text( Text(
'${lastHarvest.jumlahTelur} telur @${lastHarvest.jam}', kandang.nama,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
'(${lastHarvest.jenisPanen?.toUpperCase() ?? 'MANUAL'})',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11,
color: Colors.grey.shade600, color: Colors.green.shade600,
fontWeight: FontWeight.w500,
), ),
), ),
], ],
), ),
), const SizedBox(height: 2),
Text(
'${lastHarvest.tanggalPanen.day}/${lastHarvest.tanggalPanen.month}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
),
);
},
),
],
),
);
},
),
const SizedBox(height: 30),
// Daftar Kandang
Text(
'Status Kandang',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: kandangProvider.kandangs.length,
itemBuilder: (context, index) {
final kandang = kandangProvider.kandangs[index];
// Ambil telur dari sensor berdasarkan index
// Index 0 infra1Value, Index 1 infra2Value, dst
int telurCount = 0;
if (index == 0) {
telurCount = kandangProvider.infra1Value;
} else if (index == 1) {
telurCount = kandangProvider.infra2Value;
}
// Bisa di-extend untuk infra3, infra4, dll jika diperlukan
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kandang.nama,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${kandang.jumlahAyam} ekor ayam',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
Container(
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Column(
children: [
Text( Text(
telurCount.toString(), '${lastHarvest.jumlahTelur} telur @${lastHarvest.jam}',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 12,
fontWeight: FontWeight.bold, color: Colors.grey.shade600,
color: Colors.orange.shade700,
),
),
Text(
'Telur',
style: TextStyle(
fontSize: 10,
color: Colors.orange.shade600,
), ),
), ),
], ],
), ),
), ),
Text(
'${lastHarvest.tanggalPanen.day}/${lastHarvest.tanggalPanen.month}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
], ],
), ),
);
},
),
],
),
);
},
),
const SizedBox(height: 30),
// Daftar Kandang
Text(
'Status Kandang',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: kandangProvider.kandangs.length,
itemBuilder: (context, index) {
final kandang = kandangProvider.kandangs[index];
// Ambil telur dari sensor berdasarkan index
// Index 0 infra1Value, Index 1 infra2Value, dst
int telurCount = 0;
if (index == 0) {
telurCount = kandangProvider.infra1Value;
} else if (index == 1) {
telurCount = kandangProvider.infra2Value;
}
// Bisa di-extend untuk infra3, infra4, dll jika diperlukan
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kandang.nama,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${kandang.jumlahAyam} ekor ayam',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
Container(
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Column(
children: [
Text(
telurCount.toString(),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
Text(
'Telur',
style: TextStyle(
fontSize: 10,
color: Colors.orange.shade600,
),
),
],
),
),
], ],
), ),
), ],
); ),
}, ),
), );
], },
), ),
), ],
), ),
), ),
); );

View File

@ -3,11 +3,9 @@ import 'package:provider/provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../providers/penjadwalan_provider.dart'; import '../providers/penjadwalan_provider.dart';
import '../providers/kandang_provider.dart'; import '../providers/kandang_provider.dart';
import '../providers/panen_provider.dart';
import 'dashboard_page.dart'; import 'dashboard_page.dart';
import 'kontrol_page.dart'; import 'kontrol_page.dart';
import 'riwayat_page.dart'; import 'riwayat_page.dart';
import 'panen_debug_screen.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@ -18,7 +16,6 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
int _selectedIndex = 0; int _selectedIndex = 0;
String? _initializedForUserId;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardPage(), const DashboardPage(),
@ -26,38 +23,6 @@ class _HomePageState extends State<HomePage> {
const RiwayatPage(), const RiwayatPage(),
]; ];
@override
void didChangeDependencies() {
super.didChangeDependencies();
final authProvider = context.read<AuthProvider>();
final userId = authProvider.user?.uid;
if (userId == null || userId == _initializedForUserId) return;
_initializedForUserId = userId;
// Re-init provider saat app restart/hot restart ketika user masih login.
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
try {
await context.read<PenjadwalanProvider>().initializeWithUser(userId);
if (!mounted) return;
await context.read<KandangProvider>().initializeWithUser(userId);
if (!mounted) return;
final panenProvider = context.read<PanenProvider>();
await panenProvider.loadTodaySnapshots();
await panenProvider.restorePanenHistoryFromFirebase();
} catch (_) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Sinkronisasi data sedang bermasalah, coba lagi.'),
),
);
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final authProvider = context.watch<AuthProvider>(); final authProvider = context.watch<AuthProvider>();
@ -86,14 +51,6 @@ class _HomePageState extends State<HomePage> {
child: PopupMenuButton<int>( child: PopupMenuButton<int>(
onSelected: (value) async { onSelected: (value) async {
if (value == 0) { if (value == 0) {
// Navigate to Debug Screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PanenDebugScreen(),
),
);
} else if (value == 1) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@ -168,39 +125,6 @@ class _HomePageState extends State<HomePage> {
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: 0, value: 0,
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.orange.shade400,
Colors.orange.shade600
],
),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.bug_report,
color: Colors.white,
size: 18,
),
),
const SizedBox(width: 12),
Text(
'Debug Panen',
style: TextStyle(
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
],
),
),
PopupMenuItem(
value: 1,
child: Row( child: Row(
children: [ children: [
Container( Container(

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../providers/penjadwalan_provider.dart';
import '../providers/kandang_provider.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@ -34,8 +32,7 @@ class _LoginPageState extends State<LoginPage> {
return; return;
} }
final emailRegex = RegExp(r'^[\w\-.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!email.contains('@') || !email.contains('.')) {
if (!emailRegex.hasMatch(email)) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('Email tidak valid'))); ).showSnackBar(const SnackBar(content: Text('Email tidak valid')));
@ -48,27 +45,6 @@ class _LoginPageState extends State<LoginPage> {
if (success) { if (success) {
if (!mounted) return; if (!mounted) return;
// Initialize user data di Firebase
final userId = authProvider.user?.uid;
if (userId != null) {
final penjadwalanProvider = context.read<PenjadwalanProvider>();
final kandangProvider = context.read<KandangProvider>();
try {
await penjadwalanProvider.initializeWithUser(userId);
await kandangProvider.initializeWithUser(userId);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Login berhasil, tetapi sinkronisasi data belum sempurna.',
),
),
);
}
}
if (!mounted) return; if (!mounted) return;
Navigator.pushReplacementNamed(context, '/home'); Navigator.pushReplacementNamed(context, '/home');
} else { } else {
@ -95,223 +71,237 @@ class _LoginPageState extends State<LoginPage> {
), ),
), ),
child: SafeArea( child: SafeArea(
child: SingleChildScrollView( child: LayoutBuilder(
child: Padding( builder: (context, constraints) {
padding: const EdgeInsets.all(24.0), return SingleChildScrollView(
child: Column( child: ConstrainedBox(
mainAxisAlignment: MainAxisAlignment.start, constraints: BoxConstraints(minHeight: constraints.maxHeight),
children: [ child: Padding(
const SizedBox(height: 20), padding: const EdgeInsets.all(24.0),
// Back Button
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.arrow_back_ios,
color: Colors.grey.shade700,
size: 28,
),
),
),
const SizedBox(height: 40),
// Greeting
Text(
'Welcome Back!',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
const SizedBox(height: 12),
Text(
'Masuk ke akun Anda',
style: TextStyle(fontSize: 16, color: Colors.grey.shade700),
),
const SizedBox(height: 60),
// Login Form Card
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// Email Field
Text(
'Email',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
hintStyle: TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.email_outlined,
color: Colors.orange.shade600,
),
),
),
const SizedBox(height: 20), const SizedBox(height: 20),
// Password Field // Back Button
Text( Align(
'Password', alignment: Alignment.centerLeft,
style: TextStyle( child: GestureDetector(
fontSize: 14, onTap: () => Navigator.pop(context),
fontWeight: FontWeight.w600, child: Icon(
color: Colors.grey.shade700, Icons.arrow_back_ios,
color: Colors.grey.shade700,
size: 28,
),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 40),
TextField( // Greeting
controller: _passwordController, Text(
obscureText: _obscurePassword, 'Welcome Back!',
decoration: InputDecoration( style: TextStyle(
hintText: 'Masukkan password Anda', fontSize: 32,
hintStyle: TextStyle(color: Colors.grey.shade400), fontWeight: FontWeight.bold,
filled: true, color: Colors.orange.shade700,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.lock_outlined,
color: Colors.orange.shade600,
),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
child: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: Colors.orange.shade600,
),
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Forget Password Link Text(
Align( 'Masuk ke akun Anda',
alignment: Alignment.centerRight, style: TextStyle(
child: GestureDetector( fontSize: 16, color: Colors.grey.shade700),
onTap: () {
// TODO: Navigate to forgot password page
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Fitur ini akan segera tersedia',
),
),
);
},
child: Text(
'Lupa Password?',
style: TextStyle(
fontSize: 14,
color: Colors.orange.shade600,
fontWeight: FontWeight.w600,
),
),
),
), ),
const SizedBox(height: 30), const SizedBox(height: 60),
// Login Button // Login Form Card
Consumer<AuthProvider>( Container(
builder: (context, authProvider, _) { decoration: BoxDecoration(
return SizedBox( color: Colors.white,
width: double.infinity, borderRadius: BorderRadius.circular(20),
height: 56, boxShadow: [
child: ElevatedButton( BoxShadow(
onPressed: authProvider.isLoading color: Colors.black.withOpacity(0.1),
? null blurRadius: 10,
: _handleLogin, offset: const Offset(0, 5),
style: ElevatedButton.styleFrom( ),
backgroundColor: const Color(0xFFF4D03F), ],
disabledBackgroundColor: Colors.grey.shade300, ),
elevation: 0, padding: const EdgeInsets.all(24),
shape: RoundedRectangleBorder( child: Column(
borderRadius: BorderRadius.circular(16), crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Email Field
Text(
'Email',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
hintStyle:
TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.email_outlined,
color: Colors.orange.shade600,
), ),
), ),
child: authProvider.isLoading ),
? const SizedBox( const SizedBox(height: 20),
width: 24, // Password Field
height: 24, Text(
child: CircularProgressIndicator( 'Password',
strokeWidth: 2, style: TextStyle(
), fontSize: 14,
) fontWeight: FontWeight.w600,
: Text( color: Colors.grey.shade700,
'Masuk', ),
style: TextStyle( ),
fontSize: 16, const SizedBox(height: 8),
fontWeight: FontWeight.bold, TextField(
color: Colors.grey.shade800, controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
hintText: 'Masukkan password Anda',
hintStyle:
TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.lock_outlined,
color: Colors.orange.shade600,
),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
child: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: Colors.orange.shade600,
),
),
),
),
const SizedBox(height: 12),
// Forget Password Link
Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onTap: () {
// TODO: Navigate to forgot password page
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Fitur ini akan segera tersedia',
), ),
), ),
);
},
child: Text(
'Lupa Password?',
style: TextStyle(
fontSize: 14,
color: Colors.orange.shade600,
fontWeight: FontWeight.w600,
),
),
),
), ),
); const SizedBox(height: 30),
}, // Login Button
Consumer<AuthProvider>(
builder: (context, authProvider, _) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: authProvider.isLoading
? null
: _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFFF4D03F),
disabledBackgroundColor:
Colors.grey.shade300,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(16),
),
),
child: authProvider.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
'Masuk',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
),
);
},
),
],
),
),
const SizedBox(height: 24),
// Sign Up Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Belum punya akun? ',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
GestureDetector(
onTap: () =>
Navigator.pushNamed(context, '/signup'),
child: Text(
'Daftar di sini',
style: TextStyle(
fontSize: 14,
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
),
),
),
],
), ),
], ],
), ),
), ),
const SizedBox(height: 24), ),
// Sign Up Link );
Row( },
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Belum punya akun? ',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
GestureDetector(
onTap: () => Navigator.pushNamed(context, '/signup'),
child: Text(
'Daftar di sini',
style: TextStyle(
fontSize: 14,
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
), ),
), ),
), ),

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
import '../providers/penjadwalan_provider.dart';
import '../providers/kandang_provider.dart';
class SignupPage extends StatefulWidget { class SignupPage extends StatefulWidget {
const SignupPage({super.key}); const SignupPage({super.key});
@ -46,8 +44,7 @@ class _SignupPageState extends State<SignupPage> {
return; return;
} }
final emailRegex = RegExp(r'^[\w\-.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!email.contains('@') || !email.contains('.')) {
if (!emailRegex.hasMatch(email)) {
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('Email tidak valid'))); ).showSnackBar(const SnackBar(content: Text('Email tidak valid')));
@ -83,27 +80,6 @@ class _SignupPageState extends State<SignupPage> {
if (success) { if (success) {
if (!mounted) return; if (!mounted) return;
// Initialize user data di Firebase
final userId = authProvider.user?.uid;
if (userId != null) {
final penjadwalanProvider = context.read<PenjadwalanProvider>();
final kandangProvider = context.read<KandangProvider>();
try {
await penjadwalanProvider.initializeWithUser(userId);
await kandangProvider.initializeWithUser(userId);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Akun berhasil dibuat, tetapi sinkronisasi data belum sempurna.',
),
),
);
}
}
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
).showSnackBar(const SnackBar(content: Text('Akun berhasil dibuat!'))); ).showSnackBar(const SnackBar(content: Text('Akun berhasil dibuat!')));
@ -135,305 +111,321 @@ class _SignupPageState extends State<SignupPage> {
), ),
), ),
child: SafeArea( child: SafeArea(
child: SingleChildScrollView( child: LayoutBuilder(
child: Padding( builder: (context, constraints) {
padding: const EdgeInsets.all(24.0), return SingleChildScrollView(
child: Column( child: ConstrainedBox(
mainAxisAlignment: MainAxisAlignment.start, constraints: BoxConstraints(minHeight: constraints.maxHeight),
children: [ child: Padding(
const SizedBox(height: 20), padding: const EdgeInsets.all(24.0),
// Back Button
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.arrow_back_ios,
color: Colors.grey.shade700,
size: 28,
),
),
),
const SizedBox(height: 30),
// Greeting
Text(
'Daftar Akun Baru',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.orange.shade700,
),
),
const SizedBox(height: 12),
Text(
'Bergabunglah dengan TelurKu',
style: TextStyle(fontSize: 16, color: Colors.grey.shade700),
),
const SizedBox(height: 40),
// Signup Form Card
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
padding: const EdgeInsets.all(24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
// Name Field
Text(
'Nama Lengkap',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
TextField(
controller: _nameController,
decoration: InputDecoration(
hintText: 'Masukkan nama lengkap',
hintStyle: TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.person_outlined,
color: Colors.orange.shade600,
),
),
),
const SizedBox(height: 20), const SizedBox(height: 20),
// Email Field // Back Button
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(
Icons.arrow_back_ios,
color: Colors.grey.shade700,
size: 28,
),
),
),
const SizedBox(height: 30),
// Greeting
Text( Text(
'Email', 'Daftar Akun Baru',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 32,
fontWeight: FontWeight.w600, fontWeight: FontWeight.bold,
color: Colors.grey.shade700, color: Colors.orange.shade700,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 12),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
hintStyle: TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.email_outlined,
color: Colors.orange.shade600,
),
),
),
const SizedBox(height: 20),
// Password Field
Text( Text(
'Password', 'Bergabunglah dengan TelurKu',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 16, color: Colors.grey.shade700),
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 40),
TextField( // Signup Form Card
controller: _passwordController, Container(
obscureText: _obscurePassword, decoration: BoxDecoration(
decoration: InputDecoration( color: Colors.white,
hintText: 'Minimal 6 karakter', borderRadius: BorderRadius.circular(20),
hintStyle: TextStyle(color: Colors.grey.shade400), boxShadow: [
filled: true, BoxShadow(
fillColor: Colors.grey.shade100, color: Colors.black.withOpacity(0.1),
border: OutlineInputBorder( blurRadius: 10,
borderRadius: BorderRadius.circular(12), offset: const Offset(0, 5),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.lock_outlined,
color: Colors.orange.shade600,
),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
child: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: Colors.orange.shade600,
), ),
), ],
), ),
), padding: const EdgeInsets.all(24),
const SizedBox(height: 20), child: Column(
// Confirm Password Field crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'Konfirmasi Password', // Name Field
style: TextStyle( Text(
fontSize: 14, 'Nama Lengkap',
fontWeight: FontWeight.w600, style: TextStyle(
color: Colors.grey.shade700, fontSize: 14,
), fontWeight: FontWeight.w600,
), color: Colors.grey.shade700,
const SizedBox(height: 8), ),
TextField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
decoration: InputDecoration(
hintText: 'Konfirmasi password Anda',
hintStyle: TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.lock_outlined,
color: Colors.orange.shade600,
),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscureConfirmPassword =
!_obscureConfirmPassword;
});
},
child: Icon(
_obscureConfirmPassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: Colors.orange.shade600,
), ),
), const SizedBox(height: 8),
), TextField(
), controller: _nameController,
const SizedBox(height: 20), decoration: InputDecoration(
// Terms and Conditions Checkbox hintText: 'Masukkan nama lengkap',
Row( hintStyle:
children: [ TextStyle(color: Colors.grey.shade400),
Checkbox( filled: true,
value: _agreedToTerms, fillColor: Colors.grey.shade100,
onChanged: (value) { border: OutlineInputBorder(
setState(() { borderRadius: BorderRadius.circular(12),
_agreedToTerms = value ?? false; borderSide: BorderSide.none,
});
},
activeColor: Colors.orange.shade600,
),
Expanded(
child: RichText(
text: TextSpan(
text: 'Saya setuju dengan ',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade700,
), ),
children: [ prefixIcon: Icon(
TextSpan( Icons.person_outlined,
text: 'Syarat & Ketentuan', color: Colors.orange.shade600,
style: TextStyle( ),
color: Colors.orange.shade600, ),
fontWeight: FontWeight.bold, ),
const SizedBox(height: 20),
// Email Field
Text(
'Email',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Masukkan email Anda',
hintStyle:
TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.email_outlined,
color: Colors.orange.shade600,
),
),
),
const SizedBox(height: 20),
// Password Field
Text(
'Password',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
hintText: 'Minimal 6 karakter',
hintStyle:
TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.lock_outlined,
color: Colors.orange.shade600,
),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
child: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: Colors.orange.shade600,
),
),
),
),
const SizedBox(height: 20),
// Confirm Password Field
Text(
'Konfirmasi Password',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
const SizedBox(height: 8),
TextField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
decoration: InputDecoration(
hintText: 'Konfirmasi password Anda',
hintStyle:
TextStyle(color: Colors.grey.shade400),
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.lock_outlined,
color: Colors.orange.shade600,
),
suffixIcon: GestureDetector(
onTap: () {
setState(() {
_obscureConfirmPassword =
!_obscureConfirmPassword;
});
},
child: Icon(
_obscureConfirmPassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: Colors.orange.shade600,
),
),
),
),
const SizedBox(height: 20),
// Terms and Conditions Checkbox
Row(
children: [
Checkbox(
value: _agreedToTerms,
onChanged: (value) {
setState(() {
_agreedToTerms = value ?? false;
});
},
activeColor: Colors.orange.shade600,
),
Expanded(
child: RichText(
text: TextSpan(
text: 'Saya setuju dengan ',
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade700,
),
children: [
TextSpan(
text: 'Syarat & Ketentuan',
style: TextStyle(
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
),
),
],
), ),
), ),
], ),
],
),
const SizedBox(height: 30),
// Signup Button
Consumer<AuthProvider>(
builder: (context, authProvider, _) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: authProvider.isLoading
? null
: _handleSignup,
style: ElevatedButton.styleFrom(
backgroundColor:
const Color(0xFFF4D03F),
disabledBackgroundColor:
Colors.grey.shade300,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(16),
),
),
child: authProvider.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
'Daftar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
),
);
},
),
],
),
),
const SizedBox(height: 24),
// Login Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Sudah punya akun? ',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
GestureDetector(
onTap: () =>
Navigator.pushNamed(context, '/login'),
child: Text(
'Masuk di sini',
style: TextStyle(
fontSize: 14,
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
], ],
), ),
const SizedBox(height: 30), const SizedBox(height: 20),
// Signup Button
Consumer<AuthProvider>(
builder: (context, authProvider, _) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: authProvider.isLoading
? null
: _handleSignup,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF4D03F),
disabledBackgroundColor: Colors.grey.shade300,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: authProvider.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
'Daftar',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade800,
),
),
),
);
},
),
], ],
), ),
), ),
const SizedBox(height: 24), ),
// Login Link );
Row( },
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Sudah punya akun? ',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
GestureDetector(
onTap: () => Navigator.pushNamed(context, '/login'),
child: Text(
'Masuk di sini',
style: TextStyle(
fontSize: 14,
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 20),
],
),
),
), ),
), ),
), ),

View File

@ -1,68 +1,68 @@
{ {
"images" : [ "info": {
{ "version": 1,
"size" : "16x16", "author": "xcode"
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
}, },
{ "images": [
"size" : "16x16", {
"idiom" : "mac", "size": "16x16",
"filename" : "app_icon_32.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_16.png",
}, "scale": "1x"
{ },
"size" : "32x32", {
"idiom" : "mac", "size": "16x16",
"filename" : "app_icon_32.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_32.png",
}, "scale": "2x"
{ },
"size" : "32x32", {
"idiom" : "mac", "size": "32x32",
"filename" : "app_icon_64.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_32.png",
}, "scale": "1x"
{ },
"size" : "128x128", {
"idiom" : "mac", "size": "32x32",
"filename" : "app_icon_128.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_64.png",
}, "scale": "2x"
{ },
"size" : "128x128", {
"idiom" : "mac", "size": "128x128",
"filename" : "app_icon_256.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_128.png",
}, "scale": "1x"
{ },
"size" : "256x256", {
"idiom" : "mac", "size": "128x128",
"filename" : "app_icon_256.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_256.png",
}, "scale": "2x"
{ },
"size" : "256x256", {
"idiom" : "mac", "size": "256x256",
"filename" : "app_icon_512.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_256.png",
}, "scale": "1x"
{ },
"size" : "512x512", {
"idiom" : "mac", "size": "256x256",
"filename" : "app_icon_512.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_512.png",
}, "scale": "2x"
{ },
"size" : "512x512", {
"idiom" : "mac", "size": "512x512",
"filename" : "app_icon_1024.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_512.png",
} "scale": "1x"
], },
"info" : { {
"version" : 1, "size": "512x512",
"author" : "xcode" "idiom": "mac",
} "filename": "app_icon_1024.png",
} "scale": "2x"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 848 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -9,6 +9,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.59" version: "1.3.59"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -33,6 +49,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -198,6 +230,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -240,6 +280,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -248,6 +296,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.19.0"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -368,6 +424,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -384,6 +448,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
provider: provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -493,6 +565,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.7.0 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

View File

@ -61,6 +61,7 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_launcher_icons: ^0.14.3
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@ -110,3 +111,21 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package # see https://flutter.dev/to/font-from-package
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/icons/telurku.jpg"
min_sdk_android: 21
web:
generate: true
image_path: "assets/icons/telurku.jpg"
background_color: "#ffffff"
theme_color: "#ffffff"
windows:
generate: true
image_path: "assets/icons/telurku.jpg"
icon_size: 48
macos:
generate: true
image_path: "assets/icons/telurku.jpg"

View File

@ -1,30 +1,19 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:telurku/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('basic material app renders', (WidgetTester tester) async {
// Build our app and trigger a frame. await tester.pumpWidget(
await tester.pumpWidget(const MyApp()); const MaterialApp(
home: Scaffold(
body: Center(
child: Text('TelurKu Test Smoke'),
),
),
),
);
// Verify that our counter starts at 0. expect(find.text('TelurKu Test Smoke'), findsOneWidget);
expect(find.text('0'), findsOneWidget); expect(find.byType(MaterialApp), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
}); });
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 310 KiB

View File

@ -3,8 +3,8 @@
"short_name": "telurku", "short_name": "telurku",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#ffffff",
"theme_color": "#0175C2", "theme_color": "#ffffff",
"description": "A new Flutter project.", "description": "A new Flutter project.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
@ -32,4 +32,4 @@
"purpose": "maskable" "purpose": "maskable"
} }
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB