Some changes

This commit is contained in:
Aiman Hafiz 2025-12-19 11:53:29 +08:00
parent 2a72067dd6
commit 8ff5196ab8
19 changed files with 598 additions and 1348 deletions

View File

@ -1,5 +1,24 @@
# Work Log # Work Log
## Thursday, December 18, 2025
### UI Updates
- **Admin Station Page (`station.dart`)**
- **Layout Refinement**:
- Moved Department Name to the title row, aligned to the right side.
- Moved Station User PIC (Full Name) to the subtitle row.
- **Admin Product Page (`product.dart`)**
- **Image URL Handling**:
- Updated the product image URL construction to use `ApiService.baseUrl` dynamically instead of a hardcoded URL.
- **Admin Item Page (`item.dart`)**
- **Label & Layout Adjustments**:
- Renamed `Location` label to **`Station`** (mapped to `currentStation`).
- Renamed `User` label to **`Location User`** (mapped to `currentUser`).
- Added a new **`Location`** header above the details.
- Reordered fields under the Location header to: **Location User**, **Store**, **Station**.
## Monday, December 15, 2025 ## Monday, December 15, 2025
### Features & UI Updates ### Features & UI Updates
@ -180,4 +199,4 @@
- **Technician (`technician_to_invMaster.dart`)**: Added "Show" button for documents/pictures. - **Technician (`technician_to_invMaster.dart`)**: Added "Show" button for documents/pictures.
### Fixes ### Fixes
- Fixed a compilation error in `product_request_service.dart` (missing closing brace). - Fixed a compilation error in `product_request_service.dart` (missing closing brace).

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,9 @@ import 'package:inventory_system/screens/bottom_nav_bar.dart';
import 'package:inventory_system/screens/nav_bar.dart'; import 'package:inventory_system/screens/nav_bar.dart';
import 'package:inventory_system/screens/title_bar.dart'; import 'package:inventory_system/screens/title_bar.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
class ItemScreen extends StatefulWidget { class ItemScreen extends StatefulWidget {
const ItemScreen({super.key}); const ItemScreen({super.key});
@ -126,6 +129,63 @@ class _ItemScreenState extends State<ItemScreen> {
); );
} }
Future<void> _printQrInfo(Map<String, dynamic> item) async {
final doc = pw.Document();
doc.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
build: (pw.Context context) {
return pw.Column(
mainAxisSize: pw.MainAxisSize.min,
children: [
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Column(
children: [
pw.BarcodeWidget(
barcode: pw.Barcode.qrCode(),
data: item['uniqueID'] ?? '',
width: 100,
height: 100,
),
pw.SizedBox(height: 8),
pw.Text(
item['uniqueID'] ?? '',
style: const pw.TextStyle(fontSize: 12), // monospace not strictly required for PDF unless font loaded
),
],
),
pw.SizedBox(width: 24),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(item['currentStore'] ?? 'N/A', style: const pw.TextStyle(fontSize: 16)),
pw.SizedBox(height: 8),
pw.Text(item['productShortName'] ?? item['productName'] ?? '', style: const pw.TextStyle(fontSize: 16)),
pw.SizedBox(height: 8),
pw.Text(item['serialNumber'] ?? '', style: const pw.TextStyle(fontSize: 16)),
pw.SizedBox(height: 8),
pw.Text(item['partNumber'] ?? '', style: const pw.TextStyle(fontSize: 16)),
],
),
),
],
),
],
);
},
),
);
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => doc.save(),
name: '${item['uniqueID']}_QR.pdf',
);
}
void _showPrintDialog(Map<String, dynamic> item) { void _showPrintDialog(Map<String, dynamic> item) {
showDialog( showDialog(
context: context, context: context,
@ -177,7 +237,7 @@ class _ItemScreenState extends State<ItemScreen> {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context), onPressed: () => _printQrInfo(item),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade200, backgroundColor: Colors.grey.shade200,
foregroundColor: Colors.black87, foregroundColor: Colors.black87,

View File

@ -430,9 +430,9 @@ class _ItemMovementAllScreenState extends State<ItemMovementAllScreen>
tiles = [ tiles = [
_buildDetailItem('Action', item['action'] ?? 'N/A'), _buildDetailItem('Action', item['action'] ?? 'N/A'),
_buildDetailItem('Product Category', item['productCategory'] ?? 'N/A'), _buildDetailItem('Product Category', item['productCategory'] ?? 'N/A'),
_buildDetailItem('Status', item['latestStatus'] ?? 'N/A'), _buildDetailItem('Start Status', item['latestStatus'] ?? item['toOther'] ?? 'N/A'),
_buildDetailItem('From Station', item['lastStationName'] ?? 'N/A'), _buildDetailItem('From Station', item['lastStationName'] ?? 'N/A'),
_buildDetailItem('From Store', item['lastStoreName'] ?? 'N/A'), _buildDetailItem('From Store', item['lastStoreName'] ?? item['toStoreName'] ?? 'N/A'),
_buildDetailItem('Quantity', item['quantity']?.toString() ?? 'N/A'), _buildDetailItem('Quantity', item['quantity']?.toString() ?? 'N/A'),
_buildDetailItem('Last User', item['lastUserName'] ?? 'N/A'), _buildDetailItem('Last User', item['lastUserName'] ?? 'N/A'),
_buildDetailItem('From User', item['toUserName'] ?? 'N/A'), _buildDetailItem('From User', item['toUserName'] ?? 'N/A'),
@ -460,7 +460,7 @@ class _ItemMovementAllScreenState extends State<ItemMovementAllScreen>
_buildDetailItem('From User', item['toUserName'] ?? 'N/A'), _buildDetailItem('From User', item['toUserName'] ?? 'N/A'),
_buildDetailItem('From Store', item['lastStoreName'] ?? 'N/A'), _buildDetailItem('From Store', item['lastStoreName'] ?? 'N/A'),
_buildDetailItem('To Store', item['toStoreName'] ?? 'N/A'), _buildDetailItem('To Store', item['toStoreName'] ?? 'N/A'),
_buildDetailItem('Latest Status', item['latestStatus'] ?? 'N/A'), _buildDetailItem('Latest Status', item['latestStatus'] ?? item['toOther'] ?? 'N/A'),
]; ];
break; break;
} }
@ -488,7 +488,7 @@ class _ItemMovementAllScreenState extends State<ItemMovementAllScreen>
if (note != null && note.toString().trim().isNotEmpty) ...[ if (note != null && note.toString().trim().isNotEmpty) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Note / Remark', 'Remark',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey.shade600, color: Colors.grey.shade600,
@ -503,6 +503,7 @@ class _ItemMovementAllScreenState extends State<ItemMovementAllScreen>
), ),
), ),
], ],
_buildDocumentTile(item['consignmentNote']),
], ],
); );
}, },
@ -510,6 +511,86 @@ class _ItemMovementAllScreenState extends State<ItemMovementAllScreen>
); );
} }
Widget _buildDocumentTile(String? docPath) {
final hasDoc = docPath != null && docPath.isNotEmpty;
return Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Document/Picture',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
hasDoc
? InkWell(
onTap: () => _showDocument(docPath),
child: Text(
'Show',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
decoration: TextDecoration.underline,
),
),
)
: const Text(
'No Document',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
);
}
void _showDocument(String docPath) {
showDialog(
context: context,
builder: (ctx) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
title: const Text('Document'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(ctx),
),
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black,
),
InteractiveViewer(
child: Image.network(
ApiService.baseUrl + docPath,
fit: BoxFit.contain,
errorBuilder: (ctx, err, stack) => const Padding(
padding: EdgeInsets.all(20.0),
child: Column(
children: [
Icon(Icons.broken_image, size: 50, color: Colors.grey),
Text('Failed to load image'),
],
),
),
),
),
],
),
),
);
}
Widget _buildDetailItem(String label, String value) { Widget _buildDetailItem(String label, String value) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -261,7 +261,7 @@ class _ItemMovementItemScreenState extends State<ItemMovementItemScreen> {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
'Status: ${event['latestStatus'] ?? 'N/A'}', 'Status: ${event['latestStatus'] ?? event['toOther'] ?? 'N/A'}',
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.black87), style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.black87),
), ),
], ],
@ -394,10 +394,37 @@ class _ItemMovementItemScreenState extends State<ItemMovementItemScreen> {
} }
Widget _buildMovementDetailCard(Map<String, dynamic> movement) { Widget _buildMovementDetailCard(Map<String, dynamic> movement) {
final statusColor = _statusColor(movement['action'] ?? ''); // --- Web App Logic for Labels and Colors ---
String toOther = movement['toOther'] ?? '';
String action = movement['action'] ?? '';
bool isToStation = movement['toStation'] != null;
final movementId = movement['id'].toString(); final movementId = movement['id'].toString();
final bool isInnerExpanded = _expandedMovements.contains(movementId); final bool isInnerExpanded = _expandedMovements.contains(movementId);
// 1. Status Heading
String statusHeading = 'Assign';
Color statusHeadingColor = Colors.cyan; // Default text-info
if (toOther == 'Return') {
statusHeading = 'Return';
statusHeadingColor = Colors.orange; // text-warning
} else if (toOther == 'On Delivery') {
statusHeading = 'Receive';
statusHeadingColor = Colors.blue; // text-primary
} else if (isToStation) {
statusHeading = 'Change';
statusHeadingColor = Colors.green; // text-success
} else if (toOther == 'Faulty' || toOther == 'Calibration' || toOther == 'Repair') {
statusHeading = toOther;
statusHeadingColor = Colors.grey.shade600; // text-numb (usually greyish)
} else if (action == 'Register') {
statusHeading = 'Register';
statusHeadingColor = Colors.purple; // text-weird (arbitrary distinct color)
} else if (action == 'Assign' && !isToStation) {
statusHeading = 'Assign';
statusHeadingColor = Colors.cyan; // text-info
}
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -413,15 +440,15 @@ class _ItemMovementItemScreenState extends State<ItemMovementItemScreen> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
movement['action'] ?? '-', statusHeading,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: statusColor, color: statusHeadingColor,
), ),
), ),
), ),
_chip(movement['latestStatus'] ?? 'N/A', Colors.green.shade700), _chip(movement['latestStatus'] ?? movement['toOther'] ?? 'N/A', Colors.green.shade700),
const SizedBox(width: 8), const SizedBox(width: 8),
_outlinedButton( _outlinedButton(
isInnerExpanded ? 'Hide Details' : 'Show Details', isInnerExpanded ? 'Hide Details' : 'Show Details',
@ -451,9 +478,9 @@ class _ItemMovementItemScreenState extends State<ItemMovementItemScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
Expanded(child: _actionButton('Remark', isPrimary: true)), Expanded(child: _actionButton('Remark', isPrimary: true, onTap: () => _showRemark(movement['remark']))),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: _actionButton('Consignment Note')), Expanded(child: _actionButton('Consignment Note', isPrimary: true, color: Colors.teal.shade600, onTap: () => _showDocument(movement['consignmentNote']))),
], ],
), ),
], ],
@ -465,6 +492,66 @@ class _ItemMovementItemScreenState extends State<ItemMovementItemScreen> {
); );
} }
void _showRemark(String? remark) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Remark'),
content: Text(remark?.isNotEmpty == true ? remark! : 'No remark available.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
void _showDocument(String? docPath) {
if (docPath == null || docPath.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No document available')),
);
return;
}
showDialog(
context: context,
builder: (ctx) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
title: const Text('Document'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(ctx),
),
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black,
),
InteractiveViewer(
child: Image.network(
ApiService.baseUrl + docPath,
fit: BoxFit.contain,
errorBuilder: (ctx, err, stack) => const Padding(
padding: EdgeInsets.all(20.0),
child: Column(
children: [
Icon(Icons.broken_image, size: 50, color: Colors.grey),
Text('Failed to load image'),
],
),
),
),
),
],
),
),
);
}
Widget _buildMovementFlow(Map<String, dynamic> movement) { Widget _buildMovementFlow(Map<String, dynamic> movement) {
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@ -620,12 +707,12 @@ class _ItemMovementItemScreenState extends State<ItemMovementItemScreen> {
); );
} }
Widget _actionButton(String text, {bool isPrimary = false}) { Widget _actionButton(String text, {bool isPrimary = false, Color? color, VoidCallback? onTap}) {
return ElevatedButton( return ElevatedButton(
onPressed: () {}, onPressed: onTap,
style: isPrimary style: isPrimary
? ElevatedButton.styleFrom( ? ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade600, backgroundColor: color ?? Colors.blue.shade600,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),

View File

@ -257,10 +257,11 @@ class _ItemMovementStationScreenState extends State<ItemMovementStationScreen> {
border: Border(top: BorderSide(color: Colors.grey.shade300)), border: Border(top: BorderSide(color: Colors.grey.shade300)),
), ),
child: DataTable( child: DataTable(
columnSpacing: 20, columnSpacing: 10,
horizontalMargin: 10,
headingRowHeight: 40, headingRowHeight: 40,
dataRowMinHeight: 48, dataRowMinHeight: 48,
dataRowMaxHeight: 56, dataRowMaxHeight: 80,
headingTextStyle: const TextStyle( headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87, color: Colors.black87,
@ -278,8 +279,17 @@ class _ItemMovementStationScreenState extends State<ItemMovementStationScreen> {
return DataRow( return DataRow(
cells: [ cells: [
DataCell(Text(item['code']!)), DataCell(Text(item['code']!)),
DataCell(Text(item['description']!)), DataCell(
DataCell(Text(formattedDate)), ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: Text(
item['description']!,
softWrap: true,
style: const TextStyle(fontSize: 12),
),
),
),
DataCell(Text(formattedDate, style: const TextStyle(fontSize: 12))),
], ],
); );
}).toList(), }).toList(),

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:inventory_system/services/api_service.dart'; // 1. Import ApiService import 'package:inventory_system/services/api_service.dart';
import 'package:inventory_system/services/auth_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/admin/home_screen/home_screen.dart';
import 'package:inventory_system/screens/user/home_screen/home_screen_user.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/routes/slide_route.dart';
import 'package:inventory_system/services/session_manager.dart'; import 'package:inventory_system/services/session_manager.dart'; //To set the server URL
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@ -70,7 +70,7 @@ class _LoginScreenState extends State<LoginScreen> {
} }
} }
// 2. ADD THIS METHOD to show the URL settings dialog // To show the URL settings dialog
void _showUrlDialog(BuildContext context) { void _showUrlDialog(BuildContext context) {
final TextEditingController urlController = TextEditingController(); final TextEditingController urlController = TextEditingController();
urlController.text = ApiService.baseUrl; // Set current URL urlController.text = ApiService.baseUrl; // Set current URL
@ -119,9 +119,8 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( // 3. Use a Stack to overlay the settings button body: Stack( // Use a Stack to overlay the settings button
children: [ children: [
// This is your existing UI
Container( Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,

View File

@ -40,13 +40,13 @@ class NavBar extends StatelessWidget {
child: Builder( child: Builder(
builder: (context) { builder: (context) {
final currentUser = SessionManager.instance.currentUser; final currentUser = SessionManager.instance.currentUser;
final String displayName = currentUser?['fullName'] ?? 'Not Logged In'; final String displayName = currentUser?['fullName'] ?? 'Not Logged In'; //Display logged in user
final bool isSuperAdmin = currentUser?['isSuperAdmin'] ?? false; final bool isSuperAdmin = currentUser?['isSuperAdmin'] ?? false;
final bool isInventoryMaster = currentUser?['isInventoryMaster'] ?? false; final bool isInventoryMaster = currentUser?['isInventoryMaster'] ?? false;
String userRole = 'User'; String userRole = 'User';
if (isSuperAdmin) { if (isSuperAdmin) { //To determine and display role of user
userRole = 'Administrator'; userRole = 'Administrator';
} else if (isInventoryMaster) { } else if (isInventoryMaster) {
userRole = 'Inventory Master'; userRole = 'Inventory Master';
@ -97,12 +97,12 @@ class NavBar extends StatelessWidget {
else else
..._buildUserListTiles(context), ..._buildUserListTiles(context),
const Divider(), const Divider(),
ListTile( // ListTile(
leading: const Icon(Icons.settings), // leading: const Icon(Icons.settings),
title: const Text('Settings'), // title: const Text('Settings'),
selected: selectedScreen == AppScreen.settings, // selected: selectedScreen == AppScreen.settings,
onTap: () {}, // onTap: () {},
), // ),
ListTile( ListTile(
leading: const Icon(Icons.logout), leading: const Icon(Icons.logout),
title: const Text('Logout'), title: const Text('Logout'),
@ -116,6 +116,7 @@ class NavBar extends StatelessWidget {
); );
} }
// Admin/Inventory Master Navigation Bar
List<Widget> _buildAdminListTiles(BuildContext context) { List<Widget> _buildAdminListTiles(BuildContext context) {
return [ return [
ListTile( ListTile(
@ -209,6 +210,7 @@ class NavBar extends StatelessWidget {
]; ];
} }
// User Nav Bar
List<Widget> _buildUserListTiles(BuildContext context) { List<Widget> _buildUserListTiles(BuildContext context) {
return [ return [
ListTile( ListTile(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'dart:async'; import 'dart:async';
class SplashScreen extends StatefulWidget { class SplashScreen extends StatefulWidget {
@ -97,7 +98,7 @@ class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderSt
child: Text( child: Text(
'INVENTORY\nSYSTEM', 'INVENTORY\nSYSTEM',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: GoogleFonts.roboto(
fontSize: 36, fontSize: 36,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
letterSpacing: 4, letterSpacing: 4,

View File

@ -456,9 +456,9 @@ class _ItemMovementAllUserScreenState extends State<ItemMovementAllUserScreen>
tiles = [ tiles = [
_buildDetailItem('Action', item['action'] ?? 'N/A'), _buildDetailItem('Action', item['action'] ?? 'N/A'),
_buildDetailItem('Product Category', item['productCategory'] ?? 'N/A'), // Use productCategory _buildDetailItem('Product Category', item['productCategory'] ?? 'N/A'), // Use productCategory
_buildDetailItem('Status', item['latestStatus'] ?? 'N/A'), // Use latestStatus _buildDetailItem('Start Status', item['latestStatus'] ?? item['toOther'] ?? 'N/A'), // Use latestStatus
_buildDetailItem('From Station', item['lastStationName'] ?? 'N/A'), _buildDetailItem('From Station', item['lastStationName'] ?? 'N/A'),
_buildDetailItem('From Store', item['lastStoreName'] ?? 'N/A'), _buildDetailItem('From Store', item['lastStoreName'] ?? item['toStoreName'] ?? 'N/A'),
_buildDetailItem('Quantity', item['quantity']?.toString() ?? 'N/A'), _buildDetailItem('Quantity', item['quantity']?.toString() ?? 'N/A'),
_buildDetailItem('From User', item['lastUserName'] ?? 'N/A'), _buildDetailItem('From User', item['lastUserName'] ?? 'N/A'),
_buildDetailItem('To User', item['toUserName'] ?? 'N/A'), _buildDetailItem('To User', item['toUserName'] ?? 'N/A'),
@ -485,7 +485,7 @@ class _ItemMovementAllUserScreenState extends State<ItemMovementAllUserScreen>
_buildDetailItem('To User', item['toUserName'] ?? 'N/A'), _buildDetailItem('To User', item['toUserName'] ?? 'N/A'),
_buildDetailItem('From Store', item['lastStoreName'] ?? 'N/A'), _buildDetailItem('From Store', item['lastStoreName'] ?? 'N/A'),
_buildDetailItem('To Store', item['toStoreName'] ?? 'N/A'), _buildDetailItem('To Store', item['toStoreName'] ?? 'N/A'),
_buildDetailItem('Latest Status', item['latestStatus'] ?? 'N/A'), _buildDetailItem('Latest Status', item['latestStatus'] ?? item['toOther'] ?? 'N/A'),
]; ];
break; break;
} }
@ -528,6 +528,7 @@ class _ItemMovementAllUserScreenState extends State<ItemMovementAllUserScreen>
), ),
), ),
], ],
_buildDocumentTile(item['consignmentNote']),
], ],
); );
}, },
@ -535,6 +536,86 @@ class _ItemMovementAllUserScreenState extends State<ItemMovementAllUserScreen>
); );
} }
Widget _buildDocumentTile(String? docPath) {
final hasDoc = docPath != null && docPath.isNotEmpty;
return Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Document/Picture',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
hasDoc
? InkWell(
onTap: () => _showDocument(docPath),
child: Text(
'Show',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
decoration: TextDecoration.underline,
),
),
)
: const Text(
'No Document',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
],
),
);
}
void _showDocument(String docPath) {
showDialog(
context: context,
builder: (ctx) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
title: const Text('Document'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(ctx),
),
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black,
),
InteractiveViewer(
child: Image.network(
ApiService.baseUrl + docPath,
fit: BoxFit.contain,
errorBuilder: (ctx, err, stack) => const Padding(
padding: EdgeInsets.all(20.0),
child: Column(
children: [
Icon(Icons.broken_image, size: 50, color: Colors.grey),
Text('Failed to load image'),
],
),
),
),
),
],
),
),
);
}
/// A helper widget to display a label and its value. /// A helper widget to display a label and its value.
Widget _buildDetailItem(String label, String value) { Widget _buildDetailItem(String label, String value) {
return Column( return Column(

View File

@ -402,21 +402,26 @@ class _ItemMovementItemUserScreenState extends State<ItemMovementItemUserScreen>
// 1. Status Heading // 1. Status Heading
String statusHeading = 'Assign'; String statusHeading = 'Assign';
Color statusHeadingColor = Colors.blue; Color statusHeadingColor = Colors.cyan; // Default text-info
if (toOther == 'Return') { if (toOther == 'Return') {
statusHeading = 'Return'; statusHeading = 'Return';
statusHeadingColor = Colors.orange; // Web: text-warning statusHeadingColor = Colors.orange; // text-warning
} else if (toOther == 'On Delivery') { } else if (toOther == 'On Delivery') {
statusHeading = 'Receive'; statusHeading = 'Receive';
statusHeadingColor = Colors.blue; // Web: text-primary statusHeadingColor = Colors.blue; // text-primary
} else if (isToStation) { } else if (isToStation) {
statusHeading = 'Change'; statusHeading = 'Change';
statusHeadingColor = Colors.green; // Web: text-success statusHeadingColor = Colors.green; // text-success
} else { } else if (toOther == 'Faulty' || toOther == 'Calibration' || toOther == 'Repair') {
// Default 'Assign' (text-info in web, usually light blue/cyan) statusHeading = toOther;
statusHeading = 'Assign'; statusHeadingColor = Colors.grey.shade600; // text-numb
statusHeadingColor = Colors.cyan; } else if (action == 'Register') {
statusHeading = 'Register';
statusHeadingColor = Colors.purple; // text-weird
} else if (action == 'Assign' && !isToStation) {
statusHeading = 'Assign';
statusHeadingColor = Colors.cyan; // text-info
} }
// 2. Completion Status // 2. Completion Status
@ -509,22 +514,82 @@ class _ItemMovementItemUserScreenState extends State<ItemMovementItemUserScreen>
const SizedBox(height: 16), const SizedBox(height: 16),
_buildMovementFlow(movement), _buildMovementFlow(movement),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
Expanded(child: _actionButton('Remark', isPrimary: true)), Expanded(child: _actionButton('Remark', isPrimary: true, onTap: () => _showRemark(movement['remark']))),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: _actionButton('Consignment Note')), Expanded(child: _actionButton('Consignment Note', isPrimary: true, color: Colors.teal.shade600, onTap: () => _showDocument(movement['consignmentNote']))),
], ],
), ),
], ],
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
], ],
), ),
); );
} }
void _showRemark(String? remark) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Remark'),
content: Text(remark?.isNotEmpty == true ? remark! : 'No remark available.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
void _showDocument(String? docPath) {
if (docPath == null || docPath.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No document available')),
);
return;
}
showDialog(
context: context,
builder: (ctx) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
title: const Text('Document'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(ctx),
),
elevation: 0,
backgroundColor: Colors.transparent,
foregroundColor: Colors.black,
),
InteractiveViewer(
child: Image.network(
ApiService.baseUrl + docPath,
fit: BoxFit.contain,
errorBuilder: (ctx, err, stack) => const Padding(
padding: EdgeInsets.all(20.0),
child: Column(
children: [
Icon(Icons.broken_image, size: 50, color: Colors.grey),
Text('Failed to load image'),
],
),
),
),
),
],
),
),
);
}
String _formatDate(String? dateStr) { String _formatDate(String? dateStr) {
if (dateStr == null || dateStr.isEmpty) return ''; if (dateStr == null || dateStr.isEmpty) return '';
try { try {
@ -696,12 +761,12 @@ class _ItemMovementItemUserScreenState extends State<ItemMovementItemUserScreen>
); );
} }
Widget _actionButton(String text, {bool isPrimary = false}) { Widget _actionButton(String text, {bool isPrimary = false, Color? color, VoidCallback? onTap}) {
return ElevatedButton( return ElevatedButton(
onPressed: () {}, onPressed: onTap,
style: isPrimary style: isPrimary
? ElevatedButton.styleFrom( ? ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade600, backgroundColor: color ?? Colors.blue.shade600,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),

View File

@ -527,7 +527,13 @@ class _ItemMovementStationUserScreenState extends State<ItemMovementStationUserS
Widget _flowStep(IconData icon, String label) => Column(children: [Icon(icon, size: 28, color: Colors.blueGrey), const SizedBox(height: 4), Text(label, style: const TextStyle(fontWeight: FontWeight.bold))]); Widget _flowStep(IconData icon, String label) => Column(children: [Icon(icon, size: 28, color: Colors.blueGrey), const SizedBox(height: 4), Text(label, style: const TextStyle(fontWeight: FontWeight.bold))]);
Widget _kvGrid(String l1, String? v1, String l2, String? v2) => Row(children: [Expanded(child: _kv(l1, v1)), Expanded(child: _kv(l2, v2))]); Widget _kvGrid(String l1, String? v1, String l2, String? v2) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 1, child: _kv(l1, v1)),
Expanded(flex: 1, child: _kv(l2, v2))
],
);
Widget _kv(String label, String? value) => Padding(padding: const EdgeInsets.symmetric(vertical: 4.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), const SizedBox(height: 2), Text(value ?? '-', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600))])); Widget _kv(String label, String? value) => Padding(padding: const EdgeInsets.symmetric(vertical: 4.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), const SizedBox(height: 2), Text(value ?? '-', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600))]));

View File

@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <printing/printing_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
} }

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux file_selector_linux
printing
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -8,11 +8,15 @@ import Foundation
import file_picker import file_picker
import file_selector_macos import file_selector_macos
import mobile_scanner import mobile_scanner
import path_provider_foundation
import printing
import shared_preferences_foundation import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View File

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
archive:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -17,6 +25,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
bidi:
dependency: transitive
description:
name: bidi
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
url: "https://pub.dev"
source: hosted
version: "2.0.13"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -57,6 +81,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.5" version: "0.3.5"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@ -184,6 +216,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
url: "https://pub.dev"
source: hosted
version: "6.3.3"
html: html:
dependency: "direct main" dependency: "direct main"
description: description:
@ -208,6 +248,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: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -360,6 +408,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -384,6 +464,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
pdf:
dependency: "direct main"
description:
name: pdf
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
url: "https://pub.dev"
source: hosted
version: "3.11.3"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -408,6 +504,22 @@ 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: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
printing:
dependency: "direct main"
description:
name: printing
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
url: "https://pub.dev"
source: hosted
version: "5.14.2"
qr: qr:
dependency: transitive dependency: transitive
description: description:

View File

@ -30,6 +30,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
google_fonts: ^6.3.3
flutter_slidable: 3.1.2 flutter_slidable: 3.1.2
qr_flutter: ^4.1.0 qr_flutter: ^4.1.0
mobile_scanner: ^6.0.2 mobile_scanner: ^6.0.2
@ -44,6 +45,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
pdf: ^3.11.3
printing: ^5.14.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <printing/printing_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
} }

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows file_selector_windows
printing
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST