inventory_mobile/lib/screens/admin/item/item.dart
2025-12-15 16:04:14 +08:00

440 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:inventory_system/services/item_service.dart';
import 'package:inventory_system/screens/admin/item/item_form.dart';
import 'package:inventory_system/screens/bottom_nav_bar.dart';
import 'package:inventory_system/screens/nav_bar.dart';
import 'package:inventory_system/screens/title_bar.dart';
import 'package:qr_flutter/qr_flutter.dart';
class ItemScreen extends StatefulWidget {
const ItemScreen({super.key});
@override
State<ItemScreen> createState() => _ItemScreenState();
}
class _ItemScreenState extends State<ItemScreen> {
int _selectedIndex = 0;
final ItemService _itemService = ItemService();
late Future<List<dynamic>> _itemsFuture;
String _selectedFilter = 'All';
final List<String> _filters = ['All', 'Asset', 'Disposable', 'Part'];
List<dynamic> _allItems = [];
List<dynamic> _filteredItems = [];
final TextEditingController _searchController = TextEditingController();
int? _expandedItemId;
@override
void initState() {
super.initState();
_itemsFuture = _fetchItems();
_searchController.addListener(_filterAndSortItems);
}
Future<List<dynamic>> _fetchItems() async {
try {
final items = await _itemService.fetchItems();
if (mounted) {
setState(() {
_allItems = items;
_filterAndSortItems();
});
}
return items;
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load items: $e'), backgroundColor: Colors.red),
);
}
rethrow;
}
}
@override
void dispose() {
_searchController.removeListener(_filterAndSortItems);
_searchController.dispose();
super.dispose();
}
void _filterAndSortItems() {
final query = _searchController.text.toLowerCase();
final filtered = _allItems.where((item) {
final name = item['productName']?.toString().toLowerCase() ?? '';
final serial = item['serialNumber']?.toString().toLowerCase() ?? '';
final category = item['category']?.toString().toLowerCase() ?? '';
final uniqueId = item['uniqueID']?.toString().toLowerCase() ?? '';
final matchesSearch = name.contains(query) || serial.contains(query) || uniqueId.contains(query);
final matchesFilter = _selectedFilter == 'All' || category == _selectedFilter.toLowerCase();
return matchesSearch && matchesFilter;
}).toList();
// Sort the filtered list by uniqueID
filtered.sort((a, b) {
final idA = a['uniqueID'] as String? ?? '';
final idB = b['uniqueID'] as String? ?? '';
return idA.compareTo(idB);
});
setState(() {
_filteredItems = filtered;
_expandedItemId = null;
});
}
void _deleteItem(int itemId) async {
try {
await _itemService.deleteItem(itemId);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Item deleted successfully'), backgroundColor: Colors.green),
);
_fetchItems();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete item: $e'), backgroundColor: Colors.red),
);
}
}
}
void _confirmDelete(int itemId, String itemName) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete Item'),
content: Text('Are you sure you want to delete $itemName?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteItem(itemId);
},
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
}
void _showPrintDialog(Map<String, dynamic> item) {
showDialog(
context: context,
builder: (context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 48, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Column(
children: [
QrImageView(
data: item['uniqueID'] ?? '',
version: QrVersions.auto,
size: 100.0,
),
const SizedBox(height: 8),
Text(
item['uniqueID'] ?? '',
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
],
),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item['currentStore'] ?? 'N/A', style: const TextStyle(fontFamily: 'monospace', fontSize: 16)),
const SizedBox(height: 8),
Text(item['productShortName'] ?? item['productName'] ?? '', style: const TextStyle(fontFamily: 'monospace', fontSize: 16)),
const SizedBox(height: 8),
Text(item['serialNumber'] ?? '', style: const TextStyle(fontFamily: 'monospace', fontSize: 16)),
const SizedBox(height: 8),
Text(item['partNumber'] ?? '', style: const TextStyle(fontFamily: 'monospace', fontSize: 16)),
],
),
),
],
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade200,
foregroundColor: Colors.black87,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8), side: BorderSide(color: Colors.grey.shade400)),
),
child: const Text('Print QR Info'),
),
),
],
),
),
Positioned(
top: 8,
right: 8,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F7),
appBar: const TitleBar(title: 'Item'),
drawer: const NavBar(isAdmin: true, selectedScreen: AppScreen.item),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
children: [
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ItemFormScreen()),
).then((success) {
if (success == true) _fetchItems();
});
},
icon: const Icon(Icons.add, size: 18),
label: const Text('Add Item'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade100,
foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search',
hintStyle: TextStyle(color: Colors.grey.shade400),
prefixIcon: const Icon(Icons.search),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _filters.length,
separatorBuilder: (context, index) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final filter = _filters[index];
final isSelected = _selectedFilter == filter;
return ChoiceChip(
label: Text(filter),
selected: isSelected,
onSelected: (selected) {
if (selected) {
setState(() {
_selectedFilter = filter;
_filterAndSortItems();
});
}
},
backgroundColor: Colors.white,
selectedColor: Colors.purple.shade100,
labelStyle: TextStyle(color: isSelected ? Colors.purple.shade900 : Colors.black54, fontWeight: FontWeight.bold),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18), side: BorderSide(color: isSelected ? Colors.purple.shade100 : Colors.grey.shade300)),
);
},
),
),
const SizedBox(height: 16),
Expanded(
child: RefreshIndicator(
onRefresh: _fetchItems,
child: FutureBuilder<List<dynamic>>(
future: _itemsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting && _allItems.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError && _allItems.isEmpty) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (_filteredItems.isEmpty) {
return const Center(child: Text('No items found.'));
}
return ListView.builder(
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
final item = _filteredItems[index];
return _buildItemWidget(item);
},
);
},
),
),
),
],
),
),
bottomNavigationBar: BottomNavBar(selectedIndex: _selectedIndex, onItemTapped: (index) {
if (index == 0) { Navigator.pop(context); } else if (index == 1) { if (mounted) { Navigator.pushNamed(context, '/scan'); } } else { setState(() { _selectedIndex = index; }); }
}),
);
}
Widget _buildItemWidget(Map<String, dynamic> item) {
final isExpanded = _expandedItemId == item['itemID'];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Slidable(
key: Key(item['itemID'].toString()),
endActionPane: ActionPane(
motion: const ScrollMotion(),
extentRatio: 0.45,
children: [
SlidableAction(onPressed: (context) => Navigator.push(context, MaterialPageRoute(builder: (context) => ItemFormScreen(item: item))).then((success) => {if(success == true) _fetchItems()}), backgroundColor: Colors.blue, foregroundColor: Colors.white, icon: Icons.edit, label: 'Edit'),
SlidableAction(onPressed: (context) => _confirmDelete(item['itemID'], item['productName']), backgroundColor: Colors.red, foregroundColor: Colors.white, icon: Icons.delete, label: 'Delete', borderRadius: const BorderRadius.horizontal(right: Radius.circular(16))),
],
),
child: Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.shade300, width: 1)),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
Container(
width: 80, height: 80, padding: const EdgeInsets.all(4), decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8)),
child: QrImageView(data: item['uniqueID'] ?? '', version: QrVersions.auto, size: 72),
),
const SizedBox(height: 4),
Text(item['uniqueID'] ?? '', style: TextStyle(fontSize: 12, color: Colors.black)),
],
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item['productName'] ?? 'N/A', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Text('Serial Number: ${item['serialNumber'] ?? 'N/A'}', style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
const SizedBox(height: 4),
Text('Part Number: ${item['partNumber'] ?? 'N/A'}', style: TextStyle(fontSize: 13, color: Colors.grey.shade600)),
],
),
),
ElevatedButton.icon(onPressed: () => _showPrintDialog(item), icon: const Icon(Icons.print, size: 16), label: const Text('Print'), style: ElevatedButton.styleFrom(backgroundColor: Colors.green.shade400, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
],
),
),
InkWell(
onTap: () => setState(() => _expandedItemId = isExpanded ? null : item['itemID']),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration(border: Border(top: BorderSide(color: Colors.grey.shade200))),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(isExpanded ? 'Less Details' : 'More Details', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue.shade600)),
const SizedBox(width: 4),
Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, color: Colors.blue.shade600, size: 20),
],
),
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.fastOutSlowIn,
child: isExpanded
? Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.grey.shade50, border: Border(top: BorderSide(color: Colors.grey.shade200))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDetailItem('Category', item['category'] ?? 'N/A')),
const SizedBox(width: 16),
Expanded(child: _buildDetailItem('Quantity', item['quantity'].toString())),
const SizedBox(width: 16),
Expanded(child: _buildDetailItem('Price', 'RM${item['convertPrice']?.toString() ?? '0.00'}'))]),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDetailItem('Supplier', item['supplier'] ?? 'N/A')),
const SizedBox(width: 16),
Expanded(child: _buildDetailItem('Purchase Date', item['purchaseDate'] ?? 'N/A')),
const SizedBox(width: 16),
Expanded(child: _buildDetailItem('Warranty Until', item['endWDate'] ?? 'N/A'))]),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _buildDetailItem('Location', item['currentStation'] ?? 'N/A')),
const SizedBox(width: 16),
Expanded(child: _buildDetailItem('Store', item['currentStore'] ?? 'N/A')),
const SizedBox(width: 16),
Expanded(child: _buildDetailItem('User', item['currentUser'] ?? 'N/A')),
],
),
],
),
)
: const SizedBox.shrink(),
),
],
),
),
),
);
}
Widget _buildDetailRow(String label, String value) => Row(crossAxisAlignment: CrossAxisAlignment.start, children: [SizedBox(width: 60, child: Text(label, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Colors.grey.shade700))), Expanded(child: Text(value, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87)))]);
Widget _buildDetailItem(String label, String value) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(label, style: TextStyle(fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w500)), if (value.isNotEmpty) const SizedBox(height: 4), if (value.isNotEmpty) Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87))]);
}
}