Skip to content

Commit db7f5f4

Browse files
authored
Merge pull request #82 from pvdthings/dev
Release: Item Repair Page
2 parents 65ed8a5 + d4f2fcb commit db7f5f4

File tree

14 files changed

+275
-3
lines changed

14 files changed

+275
-3
lines changed

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pvdthings-api",
3-
"version": "1.21.1",
3+
"version": "1.21.2",
44
"description": "",
55
"main": "server.js",
66
"scripts": {

apps/api/services/inventory/mapItem.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ function mapItem(record) {
44

55
return {
66
id: record.id,
7+
thingId: record.get('Thing')?.[0],
78
number: Number(record.get('ID')),
89
name: record.get('Name')[0],
910
name_es: record.get('name_es')?.[0],

apps/api/services/inventory/service.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ const mapItem = require('./mapItem');
33
const items = base(Table.Inventory);
44

55
const inventoryFields = [
6-
'ID',
6+
'ID',
7+
'Thing',
78
'Name',
89
'Brand',
910
'Description',

apps/librarian/lib/core/api/inventory.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
part of 'api.dart';
22

3+
Future<Response> fetchInventoryItems() async {
4+
return await DioClient.instance.get('/inventory');
5+
}
6+
37
Future<Response> fetchInventoryItem({required int number}) async {
48
return await DioClient.instance.get('/inventory/$number');
59
}

apps/librarian/lib/core/api/models/item_model.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'manual_model.dart';
33
class ItemModel {
44
ItemModel({
55
required this.id,
6+
required this.thingId,
67
required this.number,
78
required this.name,
89
required this.available,
@@ -20,6 +21,7 @@ class ItemModel {
2021
});
2122

2223
final String id;
24+
final String thingId;
2325
final int number;
2426
final String name;
2527
final String? description;
@@ -43,6 +45,7 @@ class ItemModel {
4345
factory ItemModel.fromJson(Map<String, dynamic> json) {
4446
return ItemModel(
4547
id: json['id'] as String,
48+
thingId: json['thingId'] as String,
4649
number: json['number'] as int,
4750
name: json['name'] as String? ?? 'Unknown Thing',
4851
description: json['description'] as String?,

apps/librarian/lib/core/data/inventory_repository.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ class InventoryRepository extends Notifier<Future<List<ThingModel>>> {
4444
return DetailedThingModel.fromJson(response.data as Map<String, dynamic>);
4545
}
4646

47+
Future<List<ItemModel>> getItems() async {
48+
final response = await api.fetchInventoryItems();
49+
final objects = response.data as List;
50+
return objects.map((e) => ItemModel.fromJson(e)).toList();
51+
}
52+
4753
Future<ItemModel?> getItem({required int number}) async {
4854
try {
4955
final response = await api.fetchInventoryItem(number: number);

apps/librarian/lib/dashboard/pages/dashboard_page.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:librarian_app/modules/borrowers/list/searchable_borrowers_list.d
1111
import 'package:librarian_app/dashboard/providers/end_drawer_provider.dart';
1212
import 'package:librarian_app/dashboard/widgets/create_menu_item.dart';
1313
import 'package:librarian_app/dashboard/layouts/inventory_desktop_layout.dart';
14+
import 'package:librarian_app/modules/things/maintenance/view.dart';
1415
import 'package:librarian_app/modules/things/details/inventory_details_page.dart';
1516
import 'package:librarian_app/modules/things/details/inventory/inventory_list/searchable_inventory_list.dart';
1617
import 'package:librarian_app/modules/things/create/create_thing_dialog.dart';
@@ -94,6 +95,11 @@ class _DashboardPageState extends ConsumerState<DashboardPage> {
9495
},
9596
),
9697
),
98+
const DashboardModule(
99+
title: 'Item Repair',
100+
desktopLayout: MaintenanceView(),
101+
mobileLayout: null,
102+
),
97103
const DashboardModule(
98104
title: 'Actions',
99105
desktopLayout: librarian_actions.Actions(),

apps/librarian/lib/dashboard/widgets/desktop_dashboard.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class DesktopDashboard extends StatelessWidget {
4242
label: Text('Things'),
4343
padding: EdgeInsets.symmetric(vertical: 8),
4444
),
45+
NavigationRailDestination(
46+
selectedIcon: Icon(Icons.healing),
47+
icon: Icon(Icons.healing_outlined),
48+
label: Text('Repair'),
49+
padding: EdgeInsets.symmetric(vertical: 8),
50+
),
4551
NavigationRailDestination(
4652
selectedIcon: Icon(Icons.electric_bolt),
4753
icon: Icon(Icons.electric_bolt_outlined),
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:librarian_app/core/api/models/item_model.dart';
3+
import 'package:librarian_app/modules/things/providers/items.dart';
4+
import 'package:librarian_app/modules/things/providers/things_repository_provider.dart';
5+
6+
final items = Provider((ref) async {
7+
final items = await ref.watch(allItems);
8+
final things = (await ref.watch(thingsRepositoryProvider));
9+
final hiddenMap = {for (final e in things) e.id: e.hidden};
10+
11+
return RepairItemsViewModel(
12+
damagedItems: items
13+
.where((item) => item.condition == damaged)
14+
.map((item) => RepairItemModel(
15+
item: item,
16+
isThingHidden: hiddenMap[item.thingId],
17+
))
18+
.toList(),
19+
inRepairItems: items
20+
.where((item) => item.condition == inRepair)
21+
.map((item) => RepairItemModel(
22+
item: item,
23+
isThingHidden: hiddenMap[item.thingId],
24+
))
25+
.toList(),
26+
);
27+
});
28+
29+
const damaged = 'Damaged';
30+
const inRepair = 'In Repair';
31+
32+
class RepairItemsViewModel {
33+
const RepairItemsViewModel({
34+
required this.damagedItems,
35+
required this.inRepairItems,
36+
});
37+
38+
final List<RepairItemModel> damagedItems;
39+
final List<RepairItemModel> inRepairItems;
40+
}
41+
42+
class RepairItemModel {
43+
const RepairItemModel({
44+
required this.item,
45+
this.isThingHidden,
46+
});
47+
48+
final ItemModel item;
49+
final bool? isThingHidden;
50+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:librarian_app/modules/things/maintenance/providers/items.dart';
4+
import 'package:librarian_app/utils/pluralize.dart';
5+
import 'package:librarian_app/widgets/no_image.dart';
6+
import 'package:skeletonizer/skeletonizer.dart';
7+
8+
import '../providers/item_details_orchestrator.dart';
9+
10+
class MaintenanceView extends ConsumerWidget {
11+
const MaintenanceView({super.key});
12+
13+
@override
14+
Widget build(BuildContext context, WidgetRef ref) {
15+
return FutureBuilder(
16+
future: ref.watch(items),
17+
builder: (context, snapshot) {
18+
final damagedItems = snapshot.data?.damagedItems ?? [];
19+
final inRepairItems = snapshot.data?.inRepairItems ?? [];
20+
21+
return Skeletonizer(
22+
enabled: snapshot.connectionState == ConnectionState.waiting,
23+
child: MaintenanceKanban(
24+
damagedItems: damagedItems,
25+
inRepairItems: inRepairItems,
26+
onTapItem: (item) {
27+
final orchestrator = ref.read(itemDetailsOrchestrator);
28+
orchestrator.openItem(
29+
context,
30+
item: item.item,
31+
hiddenLocked: item.isThingHidden ?? false,
32+
);
33+
},
34+
),
35+
);
36+
},
37+
);
38+
}
39+
}
40+
41+
class MaintenanceKanban extends StatelessWidget {
42+
const MaintenanceKanban({
43+
super.key,
44+
required this.damagedItems,
45+
required this.inRepairItems,
46+
this.onTapItem,
47+
});
48+
49+
final List<RepairItemModel> damagedItems;
50+
final List<RepairItemModel> inRepairItems;
51+
final void Function(RepairItemModel)? onTapItem;
52+
53+
@override
54+
Widget build(BuildContext context) {
55+
return GridView.count(
56+
childAspectRatio: 1 / 1,
57+
crossAxisCount: 2,
58+
crossAxisSpacing: 4,
59+
children: [
60+
KanbanColumn(
61+
title: 'Damaged',
62+
items: damagedItems,
63+
onTapItem: onTapItem,
64+
),
65+
KanbanColumn(
66+
title: 'In Repair',
67+
items: inRepairItems,
68+
onTapItem: onTapItem,
69+
),
70+
],
71+
);
72+
}
73+
}
74+
75+
class KanbanColumn extends StatelessWidget {
76+
const KanbanColumn({
77+
super.key,
78+
required this.title,
79+
required this.items,
80+
this.onTapItem,
81+
});
82+
83+
final String title;
84+
final List<RepairItemModel> items;
85+
final void Function(RepairItemModel)? onTapItem;
86+
87+
@override
88+
Widget build(BuildContext context) {
89+
return Card.outlined(
90+
clipBehavior: Clip.antiAlias,
91+
color: Colors.transparent,
92+
child: Column(
93+
crossAxisAlignment: CrossAxisAlignment.start,
94+
children: [
95+
ListTile(
96+
title: Text(
97+
title,
98+
style: Theme.of(context).textTheme.titleLarge,
99+
),
100+
trailing: Text(pluralize(items.length, 'Item')),
101+
),
102+
const Divider(height: 1),
103+
Expanded(
104+
child: Padding(
105+
padding: const EdgeInsets.all(8.0),
106+
child: GridView.count(
107+
crossAxisCount: 4,
108+
children: items
109+
.map((item) => ItemCard(
110+
number: item.item.number,
111+
imageUrl: item.item.imageUrls.firstOrNull,
112+
onTap: () => onTapItem?.call(item),
113+
))
114+
.toList(),
115+
),
116+
),
117+
),
118+
],
119+
),
120+
);
121+
}
122+
}
123+
124+
class ItemCard extends StatelessWidget {
125+
const ItemCard({
126+
super.key,
127+
required this.number,
128+
this.imageUrl,
129+
this.onTap,
130+
});
131+
132+
final int number;
133+
final String? imageUrl;
134+
final void Function()? onTap;
135+
136+
@override
137+
Widget build(BuildContext context) {
138+
return Card(
139+
clipBehavior: Clip.antiAlias,
140+
color: Theme.of(context).colorScheme.secondaryContainer,
141+
child: InkWell(
142+
onTap: onTap,
143+
child: Column(
144+
crossAxisAlignment: CrossAxisAlignment.stretch,
145+
children: [
146+
Expanded(
147+
child: Container(
148+
color: Theme.of(context).canvasColor.withOpacity(0.5),
149+
child: imageUrl != null
150+
? Image.network(
151+
imageUrl!,
152+
fit: BoxFit.cover,
153+
)
154+
: const NoImage(),
155+
),
156+
),
157+
Padding(
158+
padding: const EdgeInsets.all(8.0),
159+
child: Text(
160+
'#$number',
161+
style: Theme.of(context).textTheme.titleMedium,
162+
),
163+
),
164+
],
165+
),
166+
),
167+
);
168+
}
169+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
3+
import 'things_repository_provider.dart';
4+
5+
final allItems = Provider((ref) async {
6+
ref.watch(thingsRepositoryProvider);
7+
return await ref.read(thingsRepositoryProvider.notifier).getItems();
8+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
String pluralize(int count, String word) {
2+
var string = '$count $word';
3+
4+
if (count > 1 || count == 0) {
5+
string += 's';
6+
}
7+
8+
return string;
9+
}

apps/librarian/pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,14 @@ packages:
704704
url: "https://pub.dev"
705705
source: hosted
706706
version: "2.0.0"
707+
skeletonizer:
708+
dependency: "direct main"
709+
description:
710+
name: skeletonizer
711+
sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae"
712+
url: "https://pub.dev"
713+
source: hosted
714+
version: "1.4.2"
707715
sky_engine:
708716
dependency: transitive
709717
description: flutter

apps/librarian/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
1010
# followed by an optional build number separated by a +.
1111
# Both the version and the builder number may be overridden in flutter
1212
# build by specifying --build-name and --build-number, respectively.
13-
version: 1.0.0+18
13+
version: 1.0.0+19
1414

1515
environment:
1616
sdk: '>=3.0.0'
@@ -38,6 +38,7 @@ dependencies:
3838
riverpod_annotation: ^2.1.6
3939
collection: ^1.17.2
4040
url_launcher: ^6.2.5
41+
skeletonizer: ^1.4.2
4142

4243
dev_dependencies:
4344
flutter_test:

0 commit comments

Comments
 (0)