diff --git a/apps/api/package.json b/apps/api/package.json index 4da0ccd..5ff363a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "pvdthings-api", - "version": "1.21.1", + "version": "1.21.2", "description": "", "main": "server.js", "scripts": { diff --git a/apps/api/services/inventory/mapItem.js b/apps/api/services/inventory/mapItem.js index 8e7c16b..3490c94 100644 --- a/apps/api/services/inventory/mapItem.js +++ b/apps/api/services/inventory/mapItem.js @@ -4,6 +4,7 @@ function mapItem(record) { return { id: record.id, + thingId: record.get('Thing')?.[0], number: Number(record.get('ID')), name: record.get('Name')[0], name_es: record.get('name_es')?.[0], diff --git a/apps/api/services/inventory/service.js b/apps/api/services/inventory/service.js index 06932c0..cc94067 100644 --- a/apps/api/services/inventory/service.js +++ b/apps/api/services/inventory/service.js @@ -3,7 +3,8 @@ const mapItem = require('./mapItem'); const items = base(Table.Inventory); const inventoryFields = [ - 'ID', + 'ID', + 'Thing', 'Name', 'Brand', 'Description', diff --git a/apps/librarian/lib/core/api/inventory.dart b/apps/librarian/lib/core/api/inventory.dart index e30553f..25f0057 100644 --- a/apps/librarian/lib/core/api/inventory.dart +++ b/apps/librarian/lib/core/api/inventory.dart @@ -1,5 +1,9 @@ part of 'api.dart'; +Future fetchInventoryItems() async { + return await DioClient.instance.get('/inventory'); +} + Future fetchInventoryItem({required int number}) async { return await DioClient.instance.get('/inventory/$number'); } diff --git a/apps/librarian/lib/core/api/models/item_model.dart b/apps/librarian/lib/core/api/models/item_model.dart index dfcf674..fe271be 100644 --- a/apps/librarian/lib/core/api/models/item_model.dart +++ b/apps/librarian/lib/core/api/models/item_model.dart @@ -3,6 +3,7 @@ import 'manual_model.dart'; class ItemModel { ItemModel({ required this.id, + required this.thingId, required this.number, required this.name, required this.available, @@ -20,6 +21,7 @@ class ItemModel { }); final String id; + final String thingId; final int number; final String name; final String? description; @@ -43,6 +45,7 @@ class ItemModel { factory ItemModel.fromJson(Map json) { return ItemModel( id: json['id'] as String, + thingId: json['thingId'] as String, number: json['number'] as int, name: json['name'] as String? ?? 'Unknown Thing', description: json['description'] as String?, diff --git a/apps/librarian/lib/core/data/inventory_repository.dart b/apps/librarian/lib/core/data/inventory_repository.dart index 49abb9a..11fbb60 100644 --- a/apps/librarian/lib/core/data/inventory_repository.dart +++ b/apps/librarian/lib/core/data/inventory_repository.dart @@ -44,6 +44,12 @@ class InventoryRepository extends Notifier>> { return DetailedThingModel.fromJson(response.data as Map); } + Future> getItems() async { + final response = await api.fetchInventoryItems(); + final objects = response.data as List; + return objects.map((e) => ItemModel.fromJson(e)).toList(); + } + Future getItem({required int number}) async { try { final response = await api.fetchInventoryItem(number: number); diff --git a/apps/librarian/lib/dashboard/pages/dashboard_page.dart b/apps/librarian/lib/dashboard/pages/dashboard_page.dart index c44abdb..0ab22fd 100644 --- a/apps/librarian/lib/dashboard/pages/dashboard_page.dart +++ b/apps/librarian/lib/dashboard/pages/dashboard_page.dart @@ -11,6 +11,7 @@ import 'package:librarian_app/modules/borrowers/list/searchable_borrowers_list.d import 'package:librarian_app/dashboard/providers/end_drawer_provider.dart'; import 'package:librarian_app/dashboard/widgets/create_menu_item.dart'; import 'package:librarian_app/dashboard/layouts/inventory_desktop_layout.dart'; +import 'package:librarian_app/modules/things/maintenance/view.dart'; import 'package:librarian_app/modules/things/details/inventory_details_page.dart'; import 'package:librarian_app/modules/things/details/inventory/inventory_list/searchable_inventory_list.dart'; import 'package:librarian_app/modules/things/create/create_thing_dialog.dart'; @@ -94,6 +95,11 @@ class _DashboardPageState extends ConsumerState { }, ), ), + const DashboardModule( + title: 'Item Repair', + desktopLayout: MaintenanceView(), + mobileLayout: null, + ), const DashboardModule( title: 'Actions', desktopLayout: librarian_actions.Actions(), diff --git a/apps/librarian/lib/dashboard/widgets/desktop_dashboard.dart b/apps/librarian/lib/dashboard/widgets/desktop_dashboard.dart index 5b3a85d..6156dee 100644 --- a/apps/librarian/lib/dashboard/widgets/desktop_dashboard.dart +++ b/apps/librarian/lib/dashboard/widgets/desktop_dashboard.dart @@ -42,6 +42,12 @@ class DesktopDashboard extends StatelessWidget { label: Text('Things'), padding: EdgeInsets.symmetric(vertical: 8), ), + NavigationRailDestination( + selectedIcon: Icon(Icons.healing), + icon: Icon(Icons.healing_outlined), + label: Text('Repair'), + padding: EdgeInsets.symmetric(vertical: 8), + ), NavigationRailDestination( selectedIcon: Icon(Icons.electric_bolt), icon: Icon(Icons.electric_bolt_outlined), diff --git a/apps/librarian/lib/modules/things/maintenance/providers/items.dart b/apps/librarian/lib/modules/things/maintenance/providers/items.dart new file mode 100644 index 0000000..024792a --- /dev/null +++ b/apps/librarian/lib/modules/things/maintenance/providers/items.dart @@ -0,0 +1,50 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/core/api/models/item_model.dart'; +import 'package:librarian_app/modules/things/providers/items.dart'; +import 'package:librarian_app/modules/things/providers/things_repository_provider.dart'; + +final items = Provider((ref) async { + final items = await ref.watch(allItems); + final things = (await ref.watch(thingsRepositoryProvider)); + final hiddenMap = {for (final e in things) e.id: e.hidden}; + + return RepairItemsViewModel( + damagedItems: items + .where((item) => item.condition == damaged) + .map((item) => RepairItemModel( + item: item, + isThingHidden: hiddenMap[item.thingId], + )) + .toList(), + inRepairItems: items + .where((item) => item.condition == inRepair) + .map((item) => RepairItemModel( + item: item, + isThingHidden: hiddenMap[item.thingId], + )) + .toList(), + ); +}); + +const damaged = 'Damaged'; +const inRepair = 'In Repair'; + +class RepairItemsViewModel { + const RepairItemsViewModel({ + required this.damagedItems, + required this.inRepairItems, + }); + + final List damagedItems; + final List inRepairItems; +} + +class RepairItemModel { + const RepairItemModel({ + required this.item, + this.isThingHidden, + }); + + final ItemModel item; + final bool? isThingHidden; +} diff --git a/apps/librarian/lib/modules/things/maintenance/view.dart b/apps/librarian/lib/modules/things/maintenance/view.dart new file mode 100644 index 0000000..8a5024b --- /dev/null +++ b/apps/librarian/lib/modules/things/maintenance/view.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/modules/things/maintenance/providers/items.dart'; +import 'package:librarian_app/utils/pluralize.dart'; +import 'package:librarian_app/widgets/no_image.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +import '../providers/item_details_orchestrator.dart'; + +class MaintenanceView extends ConsumerWidget { + const MaintenanceView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FutureBuilder( + future: ref.watch(items), + builder: (context, snapshot) { + final damagedItems = snapshot.data?.damagedItems ?? []; + final inRepairItems = snapshot.data?.inRepairItems ?? []; + + return Skeletonizer( + enabled: snapshot.connectionState == ConnectionState.waiting, + child: MaintenanceKanban( + damagedItems: damagedItems, + inRepairItems: inRepairItems, + onTapItem: (item) { + final orchestrator = ref.read(itemDetailsOrchestrator); + orchestrator.openItem( + context, + item: item.item, + hiddenLocked: item.isThingHidden ?? false, + ); + }, + ), + ); + }, + ); + } +} + +class MaintenanceKanban extends StatelessWidget { + const MaintenanceKanban({ + super.key, + required this.damagedItems, + required this.inRepairItems, + this.onTapItem, + }); + + final List damagedItems; + final List inRepairItems; + final void Function(RepairItemModel)? onTapItem; + + @override + Widget build(BuildContext context) { + return GridView.count( + childAspectRatio: 1 / 1, + crossAxisCount: 2, + crossAxisSpacing: 4, + children: [ + KanbanColumn( + title: 'Damaged', + items: damagedItems, + onTapItem: onTapItem, + ), + KanbanColumn( + title: 'In Repair', + items: inRepairItems, + onTapItem: onTapItem, + ), + ], + ); + } +} + +class KanbanColumn extends StatelessWidget { + const KanbanColumn({ + super.key, + required this.title, + required this.items, + this.onTapItem, + }); + + final String title; + final List items; + final void Function(RepairItemModel)? onTapItem; + + @override + Widget build(BuildContext context) { + return Card.outlined( + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + trailing: Text(pluralize(items.length, 'Item')), + ), + const Divider(height: 1), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: GridView.count( + crossAxisCount: 4, + children: items + .map((item) => ItemCard( + number: item.item.number, + imageUrl: item.item.imageUrls.firstOrNull, + onTap: () => onTapItem?.call(item), + )) + .toList(), + ), + ), + ), + ], + ), + ); + } +} + +class ItemCard extends StatelessWidget { + const ItemCard({ + super.key, + required this.number, + this.imageUrl, + this.onTap, + }); + + final int number; + final String? imageUrl; + final void Function()? onTap; + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.antiAlias, + color: Theme.of(context).colorScheme.secondaryContainer, + child: InkWell( + onTap: onTap, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Container( + color: Theme.of(context).canvasColor.withOpacity(0.5), + child: imageUrl != null + ? Image.network( + imageUrl!, + fit: BoxFit.cover, + ) + : const NoImage(), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '#$number', + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/librarian/lib/modules/things/providers/items.dart b/apps/librarian/lib/modules/things/providers/items.dart new file mode 100644 index 0000000..f244f47 --- /dev/null +++ b/apps/librarian/lib/modules/things/providers/items.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'things_repository_provider.dart'; + +final allItems = Provider((ref) async { + ref.watch(thingsRepositoryProvider); + return await ref.read(thingsRepositoryProvider.notifier).getItems(); +}); diff --git a/apps/librarian/lib/utils/pluralize.dart b/apps/librarian/lib/utils/pluralize.dart new file mode 100644 index 0000000..136d293 --- /dev/null +++ b/apps/librarian/lib/utils/pluralize.dart @@ -0,0 +1,9 @@ +String pluralize(int count, String word) { + var string = '$count $word'; + + if (count > 1 || count == 0) { + string += 's'; + } + + return string; +} diff --git a/apps/librarian/pubspec.lock b/apps/librarian/pubspec.lock index f8d9f44..0d9db04 100644 --- a/apps/librarian/pubspec.lock +++ b/apps/librarian/pubspec.lock @@ -704,6 +704,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae" + url: "https://pub.dev" + source: hosted + version: "1.4.2" sky_engine: dependency: transitive description: flutter diff --git a/apps/librarian/pubspec.yaml b/apps/librarian/pubspec.yaml index 0466fc3..e17303c 100644 --- a/apps/librarian/pubspec.yaml +++ b/apps/librarian/pubspec.yaml @@ -10,7 +10,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. -version: 1.0.0+18 +version: 1.0.0+19 environment: sdk: '>=3.0.0' @@ -38,6 +38,7 @@ dependencies: riverpod_annotation: ^2.1.6 collection: ^1.17.2 url_launcher: ^6.2.5 + skeletonizer: ^1.4.2 dev_dependencies: flutter_test: