diff --git a/WORK_LOG.md b/WORK_LOG.md index 0678694..9b27430 100644 --- a/WORK_LOG.md +++ b/WORK_LOG.md @@ -1,5 +1,34 @@ # Work Log +## Friday, December 19, 2025 + +### Features, Fixes & UI Updates +- **Item Movement Screens (Admin & User)** + - **Details View (`item_movement_all` & `_user`)**: + - Relabeled "Status" to **"Start Status"** in the "Pending Item" tab. + - Fixed "Start Status" displaying "N/A" by adding a fallback to the `toOther` field. + - Fixed "From Store" displaying "N/A" by adding a fallback to `toStoreName`. + - Renamed "Note / Remark" to **"Remark"** and added a "Document/Picture" viewer for the consignment note, matching the product request page's functionality. + - **Item View (`item_movement_item` & `_user`)**: + - Implemented the status display logic from the web app, showing correct labels (e.g., "Receive", "Change") and colors based on movement properties (`toOther`, `toStation`). + - Activated the "Remark" and "Consignment Note" buttons to open dialogs. + - Changed the "Consignment Note" button to a solid teal color for better visibility. + - Fixed a compilation error by restoring variables (`movementId`, `isInnerExpanded`) that were accidentally removed during a refactor. + - **Station View (`item_movement_station` & `_user`)**: + - Fixed a UI overflow issue where dates in the `DataTable` were cut off by adjusting column spacing and row heights. + - Fixed a similar issue for long descriptions by constraining column width and allowing text to wrap. + +- **Item Screen (Admin)** + - **Print QR Info**: + - Added `pdf` and `printing` packages to the project. + - Implemented PDF generation for the "Print QR Info" button, allowing the user to save or print a document containing the item details and QR code. + - **Plugin Exception Handling**: + - Addressed a `MissingPluginException` by performing a `flutter clean` and advising the user to restart the app to correctly link the new native dependencies. + +- **Splash Screen (`splash_screen.dart`)** + - **Font Implementation**: + - Corrected the usage of the `google_fonts` package by applying the `GoogleFonts.roboto()` `TextStyle` directly, ensuring the custom font loads and renders as expected. + ## Thursday, December 18, 2025 ### UI Updates diff --git a/lib/screens/admin/product/product_form.dart b/lib/screens/admin/product/product_form.dart index f14f083..0c2b024 100644 --- a/lib/screens/admin/product/product_form.dart +++ b/lib/screens/admin/product/product_form.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:inventory_system/services/product_service.dart'; +import 'package:inventory_system/services/api_service.dart'; class ProductFormScreen extends StatefulWidget { final Map? product; @@ -119,7 +120,9 @@ class _ProductFormScreenState extends State { 'ProductShortName': _shortNameController.text, 'ManufacturerId': _selectedManufacturer['manufacturerId'], 'Category': _selectedCategory, - 'ModelNo': _modelNoController.text, + 'ModelNo': (_modelNoController.text.isEmpty && _selectedCategory == 'Disposable') + ? _shortNameController.text + : _modelNoController.text, 'ImageProduct': imagePayload, }; @@ -196,7 +199,10 @@ class _ProductFormScreenState extends State { _buildTextField( controller: _modelNoController, hintText: 'Enter model number', - validator: (value) => (value == null || value.isEmpty) ? 'Model number is required' : null, + validator: (value) { + if (_selectedCategory == 'Disposable') return null; + return (value == null || value.isEmpty) ? 'Model number is required' : null; + }, ), const SizedBox(height: 20), _buildLabel('Manufacturer'), @@ -273,7 +279,7 @@ class _ProductFormScreenState extends State { if (_imageFile != null) { return Image.file(_imageFile!, fit: BoxFit.cover, width: double.infinity); } else if (widget.product?['imageProduct'] != null && widget.product!['imageProduct'].isNotEmpty) { - const String baseUrl = 'https://dev9.pstw.com.my'; + final String baseUrl = ApiService.baseUrl; final String fullImageUrl = baseUrl + widget.product!['imageProduct']; return Image.network( fullImageUrl, diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index a6c491b..b643654 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -3,8 +3,8 @@ import 'package:inventory_system/services/api_service.dart'; import 'package:inventory_system/services/auth_service.dart'; import 'package:inventory_system/screens/admin/home_screen/home_screen.dart'; import 'package:inventory_system/screens/user/home_screen/home_screen_user.dart'; -import 'package:inventory_system/routes/slide_route.dart'; import 'package:inventory_system/services/session_manager.dart'; //To set the server URL +import 'package:shared_preferences/shared_preferences.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -23,6 +23,23 @@ class _LoginScreenState extends State { final AuthService _authService = AuthService(); + @override + void initState() { + super.initState(); + _loadSavedCredentials(); + } + + Future _loadSavedCredentials() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _rememberMe = prefs.getBool('remember_me') ?? false; + if (_rememberMe) { + _emailController.text = prefs.getString('saved_email') ?? ''; + _passwordController.text = prefs.getString('saved_password') ?? ''; + } + }); + } + @override void dispose() { _emailController.dispose(); @@ -48,6 +65,17 @@ class _LoginScreenState extends State { if (!mounted) return; if (result['success'] == true) { + final prefs = await SharedPreferences.getInstance(); + if (_rememberMe) { + await prefs.setBool('remember_me', true); + await prefs.setString('saved_email', _emailController.text.trim()); + await prefs.setString('saved_password', _passwordController.text); + } else { + await prefs.remove('remember_me'); + await prefs.remove('saved_email'); + await prefs.remove('saved_password'); + } + SessionManager.instance.startSession(result['data']); final bool isAdmin = result['data']?['isAdmin'] ?? false; Navigator.pushReplacement( @@ -344,25 +372,25 @@ class _LoginScreenState extends State { ), const SizedBox(height: 16), Center( - child: TextButton( - onPressed: () { - // TODO: Implement forgot password - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Forgot password functionality to be implemented'), - ), - ); - }, - child: const Text( - 'Forgot password?', - style: TextStyle( - fontSize: 14, - color: Color(0xFF1976D2), - fontWeight: FontWeight.w500, - ), - ), - ), + // child: TextButton( + // onPressed: () { + // // TODO: Implement forgot password + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text( + // 'Forgot password functionality to be implemented'), + // ), + // ); + // }, + // child: const Text( + // 'Forgot password?', + // style: TextStyle( + // fontSize: 14, + // color: Color(0xFF1976D2), + // fontWeight: FontWeight.w500, + // ), + // ), + // ), ), ], ), diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index ecb8658..6dc6cab 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:inventory_system/services/auth_service.dart'; +import 'package:inventory_system/services/session_manager.dart'; +import 'package:inventory_system/screens/admin/home_screen/home_screen.dart'; +import 'package:inventory_system/screens/user/home_screen/home_screen_user.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -45,9 +50,45 @@ class _SplashScreenState extends State with SingleTickerProviderSt _controller.forward(); - Timer(const Duration(seconds: 3), () { - Navigator.pushReplacementNamed(context, '/login'); - }); + _checkSession(); + } + + Future _checkSession() async { + // Wait for the animation to be mostly done so the splash isn't skipped too fast + await Future.delayed(const Duration(seconds: 3)); + + final prefs = await SharedPreferences.getInstance(); + final rememberMe = prefs.getBool('remember_me') ?? false; + final email = prefs.getString('saved_email'); + final password = prefs.getString('saved_password'); + + if (rememberMe && email != null && password != null && email.isNotEmpty && password.isNotEmpty) { + // Attempt silent login + try { + final authService = AuthService(); + final result = await authService.signIn(usernameOrEmail: email, password: password); + + if (!mounted) return; + + if (result['success'] == true) { + SessionManager.instance.startSession(result['data']); + final bool isAdmin = result['data']?['isAdmin'] ?? false; + + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => isAdmin ? const HomeScreen() : const UserHomeScreen(), + ), + ); + return; + } + } catch (e) { + // Login failed, proceed to login screen + } + } + + if (!mounted) return; + Navigator.pushReplacementNamed(context, '/login'); } @override diff --git a/lib/services/manufacturer_service.dart b/lib/services/manufacturer_service.dart index 96cf298..84656e8 100644 --- a/lib/services/manufacturer_service.dart +++ b/lib/services/manufacturer_service.dart @@ -5,7 +5,6 @@ import 'package:http/http.dart' as http; import 'package:inventory_system/services/api_service.dart'; class ManufacturerService { - Future> fetchManufacturers() async { final uri = Uri.parse('${ApiService.baseUrl}/InvMainAPI/ManufacturerList'); debugPrint("Fetching manufacturers from: $uri"); diff --git a/lib/services/supplier_service.dart b/lib/services/supplier_service.dart index 60933cf..8511a34 100644 --- a/lib/services/supplier_service.dart +++ b/lib/services/supplier_service.dart @@ -6,7 +6,6 @@ import 'package:inventory_system/services/api_service.dart'; import 'package:inventory_system/services/session_manager.dart'; class SupplierService { - Future> fetchSuppliers() async { final uri = Uri.parse('${ApiService.baseUrl}/InvMainAPI/SupplierList'); final cookie = SessionManager.instance.getCookie();