@things-factory/worksheet-base 4.3.753 → 4.3.756

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist-server/controllers/outbound/picking-worksheet-controller.js +97 -27
  2. package/dist-server/controllers/outbound/picking-worksheet-controller.js.map +1 -1
  3. package/dist-server/controllers/outbound/sorting-worksheet-controller.js +117 -24
  4. package/dist-server/controllers/outbound/sorting-worksheet-controller.js.map +1 -1
  5. package/dist-server/controllers/render-ro-do.js +324 -96
  6. package/dist-server/controllers/render-ro-do.js.map +1 -1
  7. package/dist-server/graphql/resolvers/worksheet/find-sorting-release-orders-by-task-no.js +244 -123
  8. package/dist-server/graphql/resolvers/worksheet/find-sorting-release-orders-by-task-no.js.map +1 -1
  9. package/dist-server/graphql/resolvers/worksheet/index.js +1 -1
  10. package/dist-server/graphql/resolvers/worksheet/index.js.map +1 -1
  11. package/dist-server/graphql/resolvers/worksheet/loading/index.js +3 -1
  12. package/dist-server/graphql/resolvers/worksheet/loading/index.js.map +1 -1
  13. package/dist-server/graphql/resolvers/worksheet/loading/validate-qc-seals.js +79 -0
  14. package/dist-server/graphql/resolvers/worksheet/loading/validate-qc-seals.js.map +1 -0
  15. package/dist-server/graphql/types/worksheet/index.js +5 -1
  16. package/dist-server/graphql/types/worksheet/index.js.map +1 -1
  17. package/dist-server/graphql/types/worksheet/validate-qc-seals-result.js +12 -0
  18. package/dist-server/graphql/types/worksheet/validate-qc-seals-result.js.map +1 -0
  19. package/dist-server/graphql/types/worksheet/worksheet-info.js +1 -0
  20. package/dist-server/graphql/types/worksheet/worksheet-info.js.map +1 -1
  21. package/package.json +21 -21
  22. package/server/controllers/outbound/picking-worksheet-controller.ts +105 -31
  23. package/server/controllers/outbound/sorting-worksheet-controller.ts +137 -25
  24. package/server/controllers/render-ro-do.ts +378 -136
  25. package/server/graphql/resolvers/worksheet/find-sorting-release-orders-by-task-no.ts +305 -128
  26. package/server/graphql/resolvers/worksheet/index.ts +3 -2
  27. package/server/graphql/resolvers/worksheet/loading/index.ts +5 -0
  28. package/server/graphql/resolvers/worksheet/loading/validate-qc-seals.ts +91 -0
  29. package/server/graphql/types/worksheet/index.ts +5 -1
  30. package/server/graphql/types/worksheet/validate-qc-seals-result.ts +9 -0
  31. package/server/graphql/types/worksheet/worksheet-info.ts +1 -0
@@ -8,153 +8,330 @@ import {
8
8
  } from '@things-factory/sales-base'
9
9
  import { Domain } from '@things-factory/shell'
10
10
  import { User } from '@things-factory/auth-base'
11
- import { Location } from '@things-factory/warehouse-base'
12
11
  import { SortingWorksheetController } from '../../../controllers/'
13
12
  import { PartnerSetting, Setting } from '@things-factory/setting-base'
14
13
 
15
14
  import { WORKSHEET_STATUS, WORKSHEET_TYPE } from '../../../constants'
16
15
  import { Worksheet as WorksheetEntity, WorksheetDetail as WorksheetDetailEntity } from '../../../entities'
17
16
 
17
+ // Simple in-memory cache for settings with TTL
18
+ const settingsCache = new Map<string, { value: Setting | null; expiry: number }>()
19
+ const SETTINGS_CACHE_TTL = 5 * 60 * 1000 // 5 minutes
20
+
21
+ /**
22
+ * Get setting with caching to avoid repeated database queries
23
+ */
24
+ async function getCachedSetting(
25
+ tx: EntityManager,
26
+ domain: Domain,
27
+ category: string,
28
+ name: string
29
+ ): Promise<Setting | null> {
30
+ const cacheKey = `${domain.id}:${category}:${name}`
31
+ const cached = settingsCache.get(cacheKey)
32
+
33
+ if (cached && cached.expiry > Date.now()) {
34
+ return cached.value
35
+ }
36
+
37
+ const setting = await tx.getRepository(Setting).findOne({
38
+ where: { domain, category, name }
39
+ })
40
+
41
+ settingsCache.set(cacheKey, { value: setting || null, expiry: Date.now() + SETTINGS_CACHE_TTL })
42
+ return setting || null
43
+ }
44
+
45
+ /**
46
+ * Find worksheet by release good name - returns task info if found
47
+ */
48
+ async function findWorksheetByReleaseGoodName(
49
+ tx: EntityManager,
50
+ domain: Domain,
51
+ taskNo: string
52
+ ): Promise<{ taskNo: string; selectedReleaseGood: string; status: string } | null> {
53
+ const result = await tx
54
+ .getRepository(WorksheetEntity)
55
+ .createQueryBuilder('ws')
56
+ .select('ws.task_no', 'taskNo')
57
+ .addSelect('ws.status', 'wsStatus')
58
+ .addSelect('rg.name', 'releaseGoodName')
59
+ .addSelect('rg.status', 'releaseGoodStatus')
60
+ .innerJoin('worksheet_details', 'wd', `ws.id = wd.worksheet_id`)
61
+ .innerJoin('order_inventories', 'oi', `wd.target_inventory_id = oi.id AND wd."type" = 'SORTING'`)
62
+ .innerJoin('release_goods', 'rg', `rg.id = oi.release_good_id`)
63
+ .where('rg.domain_id = :domainId', { domainId: domain.id })
64
+ .andWhere('rg.name = :name', { name: taskNo })
65
+ .andWhere('rg.status IN (:...statuses)', {
66
+ statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING, ORDER_STATUS.LOADING, ORDER_STATUS.DONE]
67
+ })
68
+ .andWhere('ws.type = :type', { type: WORKSHEET_TYPE.SORTING })
69
+ .getRawOne()
70
+
71
+ if (!result) return null
72
+
73
+ if (result.releaseGoodStatus === 'LOADING' || result.releaseGoodStatus === 'DONE') {
74
+ throw new Error(`Release Good already sorted`)
75
+ }
76
+
77
+ return {
78
+ taskNo: result.taskNo,
79
+ selectedReleaseGood: result.releaseGoodName,
80
+ status: result.wsStatus
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Find worksheet by taskNo directly - optimized to only load needed relations
86
+ */
87
+ async function findWorksheetByTaskNo(
88
+ tx: EntityManager,
89
+ domain: Domain,
90
+ taskNo: string,
91
+ loadWorksheetDetails: boolean = false
92
+ ): Promise<WorksheetEntity | null> {
93
+ const relations = loadWorksheetDetails ? ['bizplace', 'bizplace.domain', 'worksheetDetails'] : ['bizplace', 'bizplace.domain']
94
+
95
+ return tx.getRepository(WorksheetEntity).findOne({
96
+ where: { taskNo, type: WORKSHEET_TYPE.SORTING, domain },
97
+ relations
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Find worksheet by bin location - combines location lookup and worksheet query
103
+ * Returns both taskNo and worksheet status in a single optimized query
104
+ */
105
+ async function findWorksheetByBinLocation(
106
+ tx: EntityManager,
107
+ domain: Domain,
108
+ binName: string
109
+ ): Promise<{ taskNo: string; worksheetId: string; status: string } | null> {
110
+ // Combined query: location -> order inventory -> worksheet in one go
111
+ const result = await tx
112
+ .getRepository(OrderInventoryEntity)
113
+ .createQueryBuilder('oi')
114
+ .select('ws2.task_no', 'taskNo')
115
+ .addSelect('ws2.id', 'worksheetId')
116
+ .addSelect('ws2.status', 'status')
117
+ .innerJoin('locations', 'loc', 'oi.bin_location_id = loc.id')
118
+ .innerJoin('worksheets', 'ws', `oi.ref_worksheet_id = ws.id AND ws.type = 'BATCH_PICKING'`)
119
+ .innerJoin('worksheets', 'ws2', `ws2.task_no = ws.task_no AND ws2.type = 'SORTING'`)
120
+ .innerJoin('release_goods', 'rg', 'rg.id = oi.release_good_id')
121
+ .where('oi.domain_id = :domainId', { domainId: domain.id })
122
+ .andWhere('loc.domain_id = :domainId', { domainId: domain.id })
123
+ .andWhere('loc.name = :binName', { binName })
124
+ .andWhere('oi.status IN (:...orderInventoryStatus)', {
125
+ orderInventoryStatus: [ORDER_INVENTORY_STATUS.READY_TO_SORT, ORDER_INVENTORY_STATUS.SORTING]
126
+ })
127
+ .andWhere('rg.status IN (:...statuses)', {
128
+ statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING]
129
+ })
130
+ .andWhere('ws2.status IN (:...worksheetStatuses)', {
131
+ worksheetStatuses: [WORKSHEET_STATUS.DEACTIVATED, WORKSHEET_STATUS.EXECUTING]
132
+ })
133
+ .getRawOne()
134
+
135
+ if (!result) {
136
+ throw new Error(`Bin do not have any batch picking order.`)
137
+ }
138
+
139
+ return {
140
+ taskNo: result.taskNo,
141
+ worksheetId: result.worksheetId,
142
+ status: result.status
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Handle worksheet activation if needed
148
+ */
149
+ async function handleWorksheetActivation(
150
+ tx: EntityManager,
151
+ domain: Domain,
152
+ user: User,
153
+ task: WorksheetEntity
154
+ ): Promise<void> {
155
+ if (task.status !== 'DEACTIVATED') return
156
+
157
+ const sortingWSCtrl = new SortingWorksheetController(tx, domain, user)
158
+
159
+ // Use cached settings
160
+ const directActivateSetting = await getCachedSetting(
161
+ tx,
162
+ domain,
163
+ 'id-rule',
164
+ 'enable-direct-activate-sorting-worksheet'
165
+ )
166
+
167
+ if (!directActivateSetting || directActivateSetting.value?.toLowerCase() !== 'true') {
168
+ throw new Error('Kindly go to sorting worksheet page to activate the sorting worksheet')
169
+ }
170
+
171
+ // Only fetch partner setting if main setting is enabled
172
+ const partnerSetting: PartnerSetting | null = await tx.getRepository(PartnerSetting).findOne({
173
+ where: {
174
+ setting: directActivateSetting,
175
+ domain: domain,
176
+ partnerDomain: task.bizplace?.domain
177
+ }
178
+ })
179
+
180
+ // If partner setting exists and is false, don't activate
181
+ if (partnerSetting && (partnerSetting.value as string)?.toLowerCase() !== 'true') {
182
+ throw new Error('Kindly go to sorting worksheet page to activate the sorting worksheet')
183
+ }
184
+
185
+ // Load worksheet details only when needed for activation
186
+ if (!task.worksheetDetails) {
187
+ const taskWithDetails = await tx.getRepository(WorksheetEntity).findOne({
188
+ where: { id: task.id },
189
+ relations: ['worksheetDetails']
190
+ })
191
+ task.worksheetDetails = taskWithDetails?.worksheetDetails || []
192
+ }
193
+
194
+ await sortingWSCtrl.activateSorting(task.name, task.worksheetDetails)
195
+ }
196
+
197
+ /**
198
+ * Build and execute release goods query with item counts in a SINGLE query
199
+ * Optimizations:
200
+ * - Removed redundant Domain join (filter directly on ws.domain_id)
201
+ * - Combined release goods + counts into one query using aggregate functions
202
+ */
203
+ async function getReleaseGoodsWithCounts(
204
+ tx: EntityManager,
205
+ domain: Domain,
206
+ taskNo: string
207
+ ): Promise<ReleaseGoodEntity[]> {
208
+ // Single query that gets release goods with their counts using aggregation
209
+ const releaseGoods: any[] = await tx
210
+ .getRepository(WorksheetEntity)
211
+ .createQueryBuilder('ws')
212
+ .select('rg.id', 'id')
213
+ .addSelect('rg.name', 'name')
214
+ .addSelect('rg.refNo', 'refNo')
215
+ .addSelect('rg.refNo2', 'refNo2')
216
+ .addSelect('rg.status', 'status')
217
+ .addSelect('rg.district', 'district')
218
+ .addSelect('rg.attention_company', 'attentionCompany')
219
+ .addSelect('rg.delivery_address_1', 'deliveryAddress1')
220
+ .addSelect('rg.delivery_address_2', 'deliveryAddress2')
221
+ .addSelect('rg.delivery_address_3', 'deliveryAddress3')
222
+ .addSelect('rg.delivery_address_4', 'deliveryAddress4')
223
+ .addSelect('rg.delivery_address_5', 'deliveryAddress5')
224
+ .addSelect('rg.created_at', 'createdAt')
225
+ .addSelect('u.name', 'sortedByUser')
226
+ // Counts computed directly in the same query
227
+ .addSelect('COUNT(oi.id)', 'totalCount')
228
+ .addSelect('SUM(CASE WHEN oi.sorted_qty = oi.release_qty THEN 1 ELSE 0 END)', 'completedCount')
229
+ .innerJoin(WorksheetDetailEntity, 'wsd', 'ws.id = wsd.worksheet_id')
230
+ .innerJoin(OrderInventoryEntity, 'oi', 'oi.id = wsd.target_inventory_id')
231
+ .innerJoin(ReleaseGoodEntity, 'rg', 'rg.id = oi.release_good_id')
232
+ .leftJoin(User, 'u', 'rg.sorted_by_id = u.id')
233
+ // Filter directly on ws.domain_id - no need to join Domain table
234
+ .where('ws.domain_id = :domainId', { domainId: domain.id })
235
+ .andWhere('ws.taskNo = :taskNo', { taskNo })
236
+ .andWhere('ws.type = :worksheetType', { worksheetType: WORKSHEET_TYPE.SORTING })
237
+ .andWhere('ws.status = :worksheetStatus', { worksheetStatus: WORKSHEET_STATUS.EXECUTING })
238
+ .groupBy('rg.id')
239
+ .addGroupBy('rg.name')
240
+ .addGroupBy('rg.refNo')
241
+ .addGroupBy('rg.refNo2')
242
+ .addGroupBy('rg.status')
243
+ .addGroupBy('rg.district')
244
+ .addGroupBy('rg.attention_company')
245
+ .addGroupBy('rg.delivery_address_1')
246
+ .addGroupBy('rg.delivery_address_2')
247
+ .addGroupBy('rg.delivery_address_3')
248
+ .addGroupBy('rg.delivery_address_4')
249
+ .addGroupBy('rg.delivery_address_5')
250
+ .addGroupBy('rg.created_at')
251
+ .addGroupBy('u.name')
252
+ .orderBy('rg.created_at', 'ASC')
253
+ .getRawMany()
254
+
255
+ // Transform results to include itemCounts
256
+ return releaseGoods.map(rg => {
257
+ const total = parseInt(rg.totalCount) || 0
258
+ const completed = parseInt(rg.completedCount) || 0
259
+ return {
260
+ id: rg.id,
261
+ name: rg.name,
262
+ refNo: rg.refNo,
263
+ refNo2: rg.refNo2,
264
+ status: rg.status,
265
+ district: rg.district,
266
+ attentionCompany: rg.attentionCompany,
267
+ deliveryAddress1: rg.deliveryAddress1,
268
+ deliveryAddress2: rg.deliveryAddress2,
269
+ deliveryAddress3: rg.deliveryAddress3,
270
+ deliveryAddress4: rg.deliveryAddress4,
271
+ deliveryAddress5: rg.deliveryAddress5,
272
+ sortedByUser: rg.sortedByUser,
273
+ itemCounts: {
274
+ total,
275
+ pending: total - completed
276
+ }
277
+ }
278
+ }) as ReleaseGoodEntity[]
279
+ }
280
+
18
281
  export const findSortingReleaseOrdersByTaskNoResolver = {
19
282
  async findSortingReleaseOrdersByTaskNo(_: any, { taskNo }, context: any) {
283
+ const startTime = Date.now()
20
284
  const { domain, tx, user }: { domain: Domain; tx: EntityManager; user: User } = context.state
21
285
 
22
- let selectedReleaseGood = null
23
- let ws = await tx.getRepository(WorksheetEntity).createQueryBuilder('ws')
24
- .addSelect('rg.name', 'release_good_name')
25
- .addSelect('rg.status', 'release_good_status')
26
- .innerJoin('worksheet_details', 'wd', `ws.id = wd.worksheet_id`)
27
- .innerJoin('order_inventories', 'oi', `wd.target_inventory_id = oi.id and wd."type" ='SORTING'`)
28
- .innerJoin('release_goods', 'rg', `rg.id = oi.release_good_id`)
29
- .where('rg.domain_id = :domainId', { domainId: domain.id })
30
- .andWhere('rg.name = :name', { name: taskNo })
31
- .andWhere('rg.status IN (:...statuses)', {
32
- statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING, ORDER_STATUS.LOADING, ORDER_STATUS.DONE]
33
- })
34
- .andWhere('ws.type = :type', { type: WORKSHEET_TYPE.SORTING })
35
- .getRawOne()
36
-
37
- if (ws) {
38
- if (ws.release_good_status == 'LOADING' || ws.release_good_status == 'DONE') {
39
- throw new Error(`Release Good already sorted`);
40
- }
286
+ let selectedReleaseGood: string | null = null
287
+ let task: WorksheetEntity | null = null
288
+ let resolvedTaskNo = taskNo
289
+
290
+ // Step 1: Try to find by release good name first (most common case)
291
+ const releaseGoodResult = await findWorksheetByReleaseGoodName(tx, domain, taskNo)
292
+
293
+ if (releaseGoodResult) {
294
+ selectedReleaseGood = releaseGoodResult.selectedReleaseGood
295
+ resolvedTaskNo = releaseGoodResult.taskNo
41
296
 
42
- selectedReleaseGood = ws.release_good_name
43
- taskNo = ws.ws_task_no
297
+ // Only load worksheetDetails if we need them for activation
298
+ const needsActivation = releaseGoodResult.status === 'DEACTIVATED'
299
+ task = await findWorksheetByTaskNo(tx, domain, resolvedTaskNo, needsActivation)
44
300
  }
45
301
 
46
- let task = await tx.getRepository(WorksheetEntity).findOne({
47
- where: { taskNo, type: WORKSHEET_TYPE.SORTING, domain },
48
- relations: ['bizplace', 'bizplace.domain', 'worksheetDetails']
49
- })
302
+ // Step 2: Try to find by worksheet taskNo directly
303
+ if (!task) {
304
+ task = await findWorksheetByTaskNo(tx, domain, taskNo, false)
305
+ }
50
306
 
51
- // Find Task based on Bin
307
+ // Step 3: Try to find by bin location
52
308
  if (!task) {
53
- const binLocation: Location = await tx.getRepository(Location).findOne({
54
- where: { domain, name: taskNo }
55
- })
56
-
57
- if (binLocation) {
58
- const qb: SelectQueryBuilder<OrderInventoryEntity> = tx
59
- .getRepository(OrderInventoryEntity)
60
- .createQueryBuilder('orderInventory')
61
-
62
- qb.innerJoinAndSelect('orderInventory.releaseGood', 'releaseGood')
63
- .innerJoinAndSelect('worksheets', 'ws', `orderInventory.ref_worksheet_id = ws.id AND ws.type = 'BATCH_PICKING'`)
64
- .innerJoinAndSelect('worksheets', 'ws2', `ws2.task_no = ws.task_no AND ws2.type = 'SORTING'`)
65
- .innerJoinAndSelect('releaseGood.bizplace', 'bizplace')
66
- .innerJoinAndSelect('bizplace.domain', 'domain')
67
- .where('orderInventory.domain_id = :domainId', { domainId: domain.id })
68
- .andWhere('orderInventory.status IN (:...orderInventoryStatus)', {
69
- orderInventoryStatus: [ORDER_INVENTORY_STATUS.READY_TO_SORT, ORDER_INVENTORY_STATUS.SORTING]
70
- })
71
- .andWhere('orderInventory.bin_location_id = :locationId', { locationId: binLocation.id })
72
- .andWhere('releaseGood.status IN (:...statuses)', {
73
- statuses: [ORDER_STATUS.READY_TO_SORT, ORDER_STATUS.SORTING]
74
- })
75
-
76
- const orderInventoryByBin = await qb.getRawOne()
77
- if (orderInventoryByBin?.releaseGood_id) {
78
- taskNo = orderInventoryByBin.ws_task_no
79
- task = await tx.getRepository(WorksheetEntity).findOne({
80
- where: {
81
- taskNo,
82
- status: In([WORKSHEET_STATUS.DEACTIVATED, WORKSHEET_STATUS.EXECUTING]),
83
- type: WORKSHEET_TYPE.SORTING,
84
- domain
85
- },
86
- relations: ['worksheetDetails']
87
- })
88
- } else {
89
- throw new Error(`Bin do not have any batch picking order.`)
90
- }
309
+ const binResult = await findWorksheetByBinLocation(tx, domain, taskNo)
310
+
311
+ if (binResult) {
312
+ resolvedTaskNo = binResult.taskNo
313
+ // Only load worksheetDetails if we need them for activation
314
+ const needsActivation = binResult.status === 'DEACTIVATED'
315
+ task = await findWorksheetByTaskNo(tx, domain, resolvedTaskNo, needsActivation)
91
316
  }
92
317
  }
93
318
 
94
- if (!task) throw new Error('Unable to find task no.')
95
-
96
- if (task.status === 'DEACTIVATED') {
97
- const sortingWSCtrl: SortingWorksheetController = new SortingWorksheetController(tx, domain, user)
98
-
99
- const directActivateSortingWorksheet: Setting = await tx.getRepository(Setting).findOne({
100
- where: { domain: domain, category: 'id-rule', name: 'enable-direct-activate-sorting-worksheet' }
101
- })
102
-
103
- const partnerDirectActivateSortingWorksheetSetting: PartnerSetting = await tx
104
- .getRepository(PartnerSetting)
105
- .findOne({
106
- where: {
107
- setting: directActivateSortingWorksheet,
108
- domain: domain,
109
- partnerDomain: task.bizplace?.domain
110
- }
111
- })
112
-
113
- if (directActivateSortingWorksheet != undefined && directActivateSortingWorksheet.value.toLowerCase() == 'true') {
114
- if (partnerDirectActivateSortingWorksheetSetting != undefined) {
115
- if (partnerDirectActivateSortingWorksheetSetting.value.toLowerCase() == 'true')
116
- await sortingWSCtrl.activateSorting(task.name, task.worksheetDetails)
117
- } else {
118
- await sortingWSCtrl.activateSorting(task.name, task.worksheetDetails)
119
- }
120
- } else {
121
- throw new Error('Kindly go to sorting worksheet page to activate the sorting worksheet')
122
- }
319
+ if (!task) {
320
+ throw new Error('Unable to find task no.')
321
+ }
322
+
323
+ // Step 4: Handle activation if needed
324
+ await handleWorksheetActivation(tx, domain, user, task)
325
+
326
+ // Step 5: Get release goods with counts
327
+ const releaseGoods = await getReleaseGoodsWithCounts(tx, domain, resolvedTaskNo)
328
+
329
+ // Performance logging (only in development/debug mode)
330
+ const elapsed = Date.now() - startTime
331
+ if (elapsed > 1000) {
332
+ console.warn(`[PERF] findSortingReleaseOrdersByTaskNo took ${elapsed}ms for taskNo: ${taskNo}`)
123
333
  }
124
334
 
125
- const qb: SelectQueryBuilder<WorksheetEntity> = tx
126
- .getRepository(WorksheetEntity)
127
- .createQueryBuilder('ws')
128
- .select('rg.id as id')
129
- .addSelect('rg.name as name')
130
- .addSelect('rg.refNo as "refNo"')
131
- .addSelect('rg.refNo2 as "refNo2"')
132
- .addSelect('rg.status as status')
133
- .addSelect('rg.district as "district"')
134
- .addSelect('rg.attention_company as "attentionCompany"')
135
- .addSelect('rg.delivery_address_1 as "deliveryAddress1"')
136
- .addSelect('rg.delivery_address_2 as "deliveryAddress2"')
137
- .addSelect('rg.delivery_address_3 as "deliveryAddress3"')
138
- .addSelect('rg.delivery_address_4 as "deliveryAddress4"')
139
- .addSelect('rg.delivery_address_5 as "deliveryAddress5"')
140
- .addSelect('user.name as "sortedByUser"')
141
- .innerJoin(Domain, 'domain', 'ws.domain_id = domain.id')
142
- .innerJoin(WorksheetDetailEntity, 'wsd', 'ws.id = wsd.worksheet_id')
143
- .innerJoin(OrderInventoryEntity, 'oi', 'oi.id = wsd.target_inventory_id')
144
- .innerJoin(ReleaseGoodEntity, 'rg', 'rg.id = oi.release_good_id')
145
- .leftJoin(User, 'user', 'rg.sorted_by_id = user.id')
146
- .where('domain.id = :domainId', { domainId: domain.id })
147
- .andWhere('ws.taskNo = :taskNo', { taskNo: taskNo })
148
- .andWhere('ws.type = :worksheetType', { worksheetType: WORKSHEET_TYPE.SORTING })
149
- .andWhere('ws.status = :worksheetStatus', { worksheetStatus: WORKSHEET_STATUS.EXECUTING })
150
- .groupBy('rg.id')
151
- .addGroupBy('rg.name')
152
- .addGroupBy('rg.status')
153
- .addGroupBy('user.name')
154
- .orderBy('rg.createdAt', 'ASC')
155
-
156
- const releaseGoods: ReleaseGoodEntity[] = await qb.getRawMany()
157
-
158
- return { releaseGoods, taskNo, selectedReleaseGood }
335
+ return { releaseGoods, taskNo: resolvedTaskNo, selectedReleaseGood }
159
336
  }
160
337
  }
@@ -24,7 +24,7 @@ import { havingVasResolver } from './having-vas'
24
24
  import { Mutations as InspectMutations } from './inspecting'
25
25
  import { inventoriesByPalletResolver } from './inventories-by-pallet'
26
26
  import { loadedInventories } from './loaded-inventories'
27
- import { Mutations as LoadingMutations } from './loading'
27
+ import { Mutations as LoadingMutations, Query as LoadingQuery } from './loading'
28
28
  import { loadingWorksheetResolver } from './loading-worksheet'
29
29
  import { notTallyTargetInventoriesResolver } from './not-tally-target-inventories'
30
30
  import { Mutations as PackingMutations } from './packing'
@@ -115,7 +115,8 @@ export const Query = {
115
115
  ...findSortingReleaseOrdersByTaskNoResolver,
116
116
  ...findReleaseOrdersByTaskNoResolver,
117
117
  ...fetchDeliveryOrderROResolver,
118
- ...putawayReplenishmentWorksheetResolver
118
+ ...putawayReplenishmentWorksheetResolver,
119
+ ...LoadingQuery
119
120
  }
120
121
 
121
122
  export const Mutation = {
@@ -2,6 +2,7 @@ import { activateLoadingResolver } from './activate-loading'
2
2
  import { loadingResolver } from './loading'
3
3
  import { undoLoadingResolver } from './undo-loading'
4
4
  import { completeLoadingResolver } from './complete-loading'
5
+ import { validateQcSealsResolver } from './validate-qc-seals'
5
6
 
6
7
  export const Mutations = {
7
8
  ...activateLoadingResolver,
@@ -9,3 +10,7 @@ export const Mutations = {
9
10
  ...undoLoadingResolver,
10
11
  ...completeLoadingResolver
11
12
  }
13
+
14
+ export const Query = {
15
+ ...validateQcSealsResolver
16
+ }
@@ -0,0 +1,91 @@
1
+ import { EntityManager, IsNull } from 'typeorm'
2
+ import { Domain } from '@things-factory/shell'
3
+ import { User } from '@things-factory/auth-base'
4
+ import { OrderTote, OrderToteSeal, ReleaseGood } from '@things-factory/sales-base'
5
+ import { Setting } from '@things-factory/setting-base'
6
+
7
+ export const validateQcSealsResolver = {
8
+ async validateQcSeals(
9
+ _: any,
10
+ { releaseGoodNo },
11
+ context: any
12
+ ): Promise<{ valid: boolean; error?: string; minimumSealNumber?: number }> {
13
+ const { tx, domain, user }: { tx: EntityManager; domain: Domain; user: User } = context.state
14
+
15
+ try {
16
+ const releaseGood: ReleaseGood = await tx.getRepository(ReleaseGood).findOne({
17
+ where: { domain, name: releaseGoodNo },
18
+ relations: ['bizplace', 'bizplace.domain']
19
+ })
20
+
21
+ if (!releaseGood) {
22
+ return { valid: false, error: 'Release good not found', minimumSealNumber: 0 }
23
+ }
24
+
25
+ // Check enable-tote-scanning setting
26
+ let enableToteScanningSetting: Setting = await tx.getRepository(Setting).findOne({
27
+ where: {
28
+ domain: domain,
29
+ category: 'id-rule',
30
+ name: 'enable-tote-scanning'
31
+ }
32
+ })
33
+
34
+ // Check minimum-seal-number setting (needed for banner display)
35
+ let minimumSealNumberSetting: Setting = await tx.getRepository(Setting).findOne({
36
+ where: {
37
+ domain: domain,
38
+ category: 'id-rule',
39
+ name: 'minimum-seal-number'
40
+ }
41
+ })
42
+
43
+ const minimumSealNumber = parseInt(minimumSealNumberSetting?.value || '0', 10)
44
+
45
+ if (enableToteScanningSetting) {
46
+ // enable-tote-scanning is numeric: 0 = disabled, >= 1 = enabled
47
+ const enableToteScanningValue = parseInt(enableToteScanningSetting.value || '0', 10)
48
+
49
+ if (enableToteScanningValue >= 1) {
50
+ // Check if there are unsealed totes (closedDate is null)
51
+ const foundNotSealedOrderTote = await tx
52
+ .getRepository(OrderTote)
53
+ .findOne({ where: { releaseGood, closedDate: IsNull() } })
54
+
55
+ if (foundNotSealedOrderTote) {
56
+ return {
57
+ valid: false,
58
+ error: `Please seal the tote(s) before proceeding. Minimum ${minimumSealNumber} seal(s) required.`,
59
+ minimumSealNumber
60
+ }
61
+ }
62
+
63
+ // If minimum-seal-number > 0, validate seal counts per tote
64
+ if (minimumSealNumber > 0) {
65
+ // Get all order totes for this release good
66
+ const orderTotes: OrderTote[] = await tx.getRepository(OrderTote).find({
67
+ where: { releaseGood },
68
+ relations: ['orderToteSeals']
69
+ })
70
+
71
+ // Validate each tote has at least minimum-seal-number seals
72
+ for (const orderTote of orderTotes) {
73
+ const sealCount = orderTote.orderToteSeals?.length || 0
74
+ if (sealCount < minimumSealNumber) {
75
+ return {
76
+ valid: false,
77
+ error: `Tote ${orderTote.name} has ${sealCount} seal(s), but minimum ${minimumSealNumber} seal(s) required`,
78
+ minimumSealNumber
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ return { valid: true, minimumSealNumber }
87
+ } catch (error) {
88
+ return { valid: false, error: error.message || 'Validation failed', minimumSealNumber: 0 }
89
+ }
90
+ }
91
+ }
@@ -22,6 +22,7 @@ import { ProductApproval } from './product-approval'
22
22
  import { ReleaseGoodWorksheet } from './release-good-worksheet'
23
23
  import { ReturnOrderWorksheet } from './return-order-worksheet'
24
24
  import { SellercraftDocument } from './sellercraft-document'
25
+ import { ValidateQcSealsResult } from './validate-qc-seals-result'
25
26
  import { VasOrderWorksheet } from './vas-order-worksheet'
26
27
  import { WebspertDocument } from './webspert-document'
27
28
  import { Worksheet } from './worksheet'
@@ -864,6 +865,8 @@ export const Query = /* GraphQL */ `
864
865
  findSortingReleaseOrdersByTaskNo(taskNo: String!): FindReleaseOrdersByTaskNo @privilege(category: "worksheet", privilege: "query") @transaction
865
866
 
866
867
  findReleaseOrdersByTaskNo(taskNo: String!): FindReleaseOrdersByTaskNo @privilege(category: "worksheet", privilege: "query") @transaction
868
+
869
+ validateQcSeals(releaseGoodNo: String!): ValidateQcSealsResult @privilege(category: "worksheet", privilege: "query") @transaction
867
870
  `
868
871
 
869
872
  export const Types = /* GraphQL */ [
@@ -898,5 +901,6 @@ export const Types = /* GraphQL */ [
898
901
  DeliveryOrderRO,
899
902
  GenerateBatchPickInfo,
900
903
  MultipleReleaseGoodWorksheet,
901
- LocationFilter
904
+ LocationFilter,
905
+ ValidateQcSealsResult
902
906
  ]
@@ -0,0 +1,9 @@
1
+ import { gql } from 'apollo-server-koa'
2
+
3
+ export const ValidateQcSealsResult = gql`
4
+ type ValidateQcSealsResult {
5
+ valid: Boolean!
6
+ error: String
7
+ minimumSealNumber: Int
8
+ }
9
+ `
@@ -14,6 +14,7 @@ export const WorksheetInfo = gql`
14
14
  marketplaceStatus: String
15
15
  customerCompanyDomainId: String
16
16
  binLocationName: String
17
+ isReleaseGoodScan: Boolean
17
18
  bizplace: Bizplace
18
19
  containerNo: String
19
20
  airwayBill: String