@finema/finework-layer 2.0.60 → 2.0.62

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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/app/app.vue +1 -1
  3. package/app/assets/css/main.css +5 -0
  4. package/app/components/HelpHub/Action.vue +28 -0
  5. package/app/components/HelpHub/ManualForm.vue +224 -0
  6. package/app/components/HelpHub/ManualViewer.vue +464 -0
  7. package/app/components/HelpHub/index.vue +116 -0
  8. package/app/composables/useCurrentModule.ts +9 -0
  9. package/app/constants/module-route.ts +48 -0
  10. package/app/loaders/manual/manual.ts +60 -0
  11. package/docs/help-hub.md +100 -0
  12. package/package.json +1 -1
  13. package/public/admin/asset.svg +15 -0
  14. package/public/admin/clock-in-admin-logo.svg +28 -0
  15. package/public/admin/clock-in-logo.svg +22 -0
  16. package/public/admin/contract.svg +36 -0
  17. package/public/admin/cost-sheet.svg +14 -0
  18. package/public/admin/document.svg +24 -0
  19. package/public/admin/efactoring.svg +18 -0
  20. package/public/admin/employee.svg +9 -0
  21. package/public/admin/evaluation.svg +24 -0
  22. package/public/admin/finesign.png +0 -0
  23. package/public/admin/guarantee.svg +20 -0
  24. package/public/admin/loan.svg +30 -0
  25. package/public/admin/newsletter.svg +18 -0
  26. package/public/admin/pmo-logo.svg +23 -0
  27. package/public/admin/pmo_delivery.svg +16 -0
  28. package/public/admin/pmo_presale_bidding.svg +25 -0
  29. package/public/admin/pmo_setup.svg +13 -0
  30. package/public/admin/pmo_warranty.svg +21 -0
  31. package/public/admin/presale_tool.svg +38 -0
  32. package/public/admin/purchase.svg +17 -0
  33. package/public/admin/quotation.svg +16 -0
  34. package/public/admin/recruit.svg +16 -0
  35. package/public/admin/report.svg +36 -0
  36. package/public/admin/request-admin.svg +39 -0
  37. package/public/admin/request.svg +37 -0
  38. package/public/admin/resource-allocation.svg +21 -0
  39. package/public/admin/resource.svg +15 -0
  40. package/public/admin/spider-web.png +0 -0
  41. package/public/admin/super-admin-logo.svg +17 -0
  42. package/public/admin/support.svg +14 -0
  43. package/public/admin/timesheet-admin-logo.svg +82 -0
  44. package/public/admin/timesheet-logo.svg +80 -0
  45. package/public/admin/todo.svg +16 -0
  46. package/public/admin/tor.svg +38 -0
  47. package/app/components/FeedbackTicket/index.vue +0 -27
@@ -0,0 +1,464 @@
1
+ <template>
2
+ <Slideover
3
+ :open="isOpen"
4
+ :portal="true"
5
+ :modal="false"
6
+ :dismissible="false"
7
+ :ui="{ content: 'w-full max-w-[560px]' }"
8
+ side="right"
9
+ @update:open="(val) => emit('update', val)"
10
+ >
11
+ <template #content>
12
+ <div class="flex h-full flex-col">
13
+ <!-- Header -->
14
+ <div class="flex items-center justify-between border-b border-gray-200 px-5 py-4">
15
+ <div class="flex min-w-0 items-center gap-3">
16
+ <button
17
+ v-if="mode !== 'list'"
18
+ class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
19
+ type="button"
20
+ title="กลับไปหน้ารายการ"
21
+ @click="backToList"
22
+ >
23
+ <Icon
24
+ name="ph:arrow-left"
25
+ class="size-5"
26
+ />
27
+ </button>
28
+ <div
29
+ v-else
30
+ class="bg-primary-100 flex size-9 shrink-0 items-center justify-center rounded-lg"
31
+ >
32
+ <Icon
33
+ name="ph:book-open"
34
+ class="text-primary-600 size-5"
35
+ />
36
+ </div>
37
+ <div class="min-w-0">
38
+ <h2 class="truncate text-base font-semibold text-gray-900">
39
+ {{ headerTitle }}
40
+ </h2>
41
+ <p class="truncate text-xs text-gray-500">
42
+ {{ headerSubtitle }}
43
+ </p>
44
+ </div>
45
+ </div>
46
+ <div class="flex shrink-0 items-center gap-1">
47
+ <a
48
+ v-if="mode === 'view' && current?.file"
49
+ :href="current.file.url"
50
+ target="_blank"
51
+ rel="noopener"
52
+ class="flex size-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
53
+ title="เปิดแบบเต็มจอ"
54
+ >
55
+ <Icon
56
+ name="ph:arrow-square-out"
57
+ class="size-5"
58
+ />
59
+ </a>
60
+ <a
61
+ v-if="mode === 'view' && current?.file"
62
+ :href="current.file.url"
63
+ :download="`${current.name}.pdf`"
64
+ class="flex size-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
65
+ title="ดาวน์โหลด PDF"
66
+ >
67
+ <Icon
68
+ name="ph:download-simple"
69
+ class="size-5"
70
+ />
71
+ </a>
72
+ <button
73
+ v-if="mode === 'view' && current && canManage(current.app)"
74
+ class="flex size-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
75
+ type="button"
76
+ title="แก้ไข"
77
+ @click="startEdit(current)"
78
+ >
79
+ <Icon
80
+ name="ph:pencil-simple"
81
+ class="size-5"
82
+ />
83
+ </button>
84
+ <button
85
+ class="flex size-8 cursor-pointer items-center justify-center rounded-lg text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
86
+ type="button"
87
+ @click="emit('update', false)"
88
+ >
89
+ <Icon
90
+ name="ph:x"
91
+ class="size-5"
92
+ />
93
+ </button>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- List -->
98
+ <template v-if="mode === 'list'">
99
+ <div class="flex-1 overflow-y-auto p-3">
100
+ <Loader :loading="loading && !manuals.length">
101
+ <div
102
+ v-if="!groups.length"
103
+ class="flex flex-col items-center justify-center gap-2 px-8 py-16 text-center"
104
+ >
105
+ <div class="mb-2 flex size-16 items-center justify-center rounded-full bg-gray-100 text-gray-400">
106
+ <Icon
107
+ name="ph:book-open"
108
+ class="size-8"
109
+ />
110
+ </div>
111
+ <h3 class="text-base font-bold text-gray-900">
112
+ ยังไม่มีคู่มือ
113
+ </h3>
114
+ <p class="max-w-xs text-sm text-gray-500">
115
+ {{ canManageAny
116
+ ? 'เริ่มต้นด้วยการเพิ่มคู่มือของแอพที่คุณดูแล'
117
+ : 'ทีมงานกำลังจัดทำคู่มืออยู่ หากติดปัญหาแจ้งทีมได้ผ่าน Feedback' }}
118
+ </p>
119
+ <Button
120
+ v-if="!canManageAny"
121
+ class="mt-3"
122
+ icon="ph:chat-circle-text"
123
+ @click="emit('feedback')"
124
+ >
125
+ ส่ง Feedback
126
+ </Button>
127
+ </div>
128
+
129
+ <div
130
+ v-for="group in groups"
131
+ :key="group.app"
132
+ class="mb-3 last:mb-0"
133
+ >
134
+ <div class="flex items-center gap-2 px-2 pb-1">
135
+ <img
136
+ :src="logoOf(group.app)"
137
+ :alt="group.label"
138
+ class="size-4 rounded"
139
+ />
140
+ <span class="text-xs font-semibold tracking-wide text-gray-500 uppercase">
141
+ {{ group.label }}
142
+ </span>
143
+ <span
144
+ v-if="group.app === currentModule"
145
+ class="bg-primary-50 text-primary-600 rounded-full px-1.5 py-0.5 text-[10px] font-semibold"
146
+ >
147
+ หน้านี้
148
+ </span>
149
+ </div>
150
+ <button
151
+ v-for="manual in group.items"
152
+ :key="manual.id"
153
+ class="group flex w-full cursor-pointer items-center gap-3 rounded-xl p-3 text-left transition hover:bg-gray-50"
154
+ type="button"
155
+ @click="openManual(manual)"
156
+ >
157
+ <div class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-gray-500">
158
+ <Icon
159
+ name="ph:file-pdf"
160
+ class="size-5"
161
+ />
162
+ </div>
163
+ <div class="min-w-0 flex-1">
164
+ <div class="truncate font-semibold text-gray-900">
165
+ {{ manual.name }}
166
+ </div>
167
+ <div class="truncate text-xs text-gray-500">
168
+ {{ manual.description || `ปรับปรุง ${formatDate(manual.updated_at)}` }}
169
+ </div>
170
+ </div>
171
+ <span
172
+ v-if="canManage(manual.app)"
173
+ class="flex items-center gap-1 opacity-0 transition group-hover:opacity-100"
174
+ >
175
+ <span
176
+ class="flex size-7 items-center justify-center rounded-lg text-gray-400 hover:bg-gray-200 hover:text-gray-600"
177
+ title="แก้ไข"
178
+ @click.stop="startEdit(manual)"
179
+ >
180
+ <Icon
181
+ name="ph:pencil-simple"
182
+ class="size-4"
183
+ />
184
+ </span>
185
+ <span
186
+ class="flex size-7 items-center justify-center rounded-lg text-gray-400 hover:bg-red-100 hover:text-red-600"
187
+ title="ลบ"
188
+ @click.stop="confirmDelete = manual"
189
+ >
190
+ <Icon
191
+ name="ph:trash"
192
+ class="size-4"
193
+ />
194
+ </span>
195
+ </span>
196
+ <Icon
197
+ v-else
198
+ name="ph:caret-right"
199
+ class="size-4 shrink-0 text-gray-300"
200
+ />
201
+ </button>
202
+ </div>
203
+ </Loader>
204
+ </div>
205
+
206
+ <div class="flex items-center justify-between gap-3 border-t border-gray-100 px-5 py-3">
207
+ <span class="text-xs text-gray-500">
208
+ ไม่เจอคู่มือที่ต้องการ?
209
+ <button
210
+ class="text-primary-600 cursor-pointer font-semibold hover:underline"
211
+ type="button"
212
+ @click="emit('feedback')"
213
+ >
214
+ ส่ง Feedback
215
+ </button>
216
+ </span>
217
+ <Button
218
+ v-if="canManageAny"
219
+ size="sm"
220
+ icon="ph:plus"
221
+ variant="outline"
222
+ color="neutral"
223
+ @click="startCreate"
224
+ >
225
+ เพิ่มคู่มือ
226
+ </Button>
227
+ </div>
228
+ </template>
229
+
230
+ <!-- Form (create / edit) -->
231
+ <div
232
+ v-else-if="mode === 'form'"
233
+ class="flex flex-1 flex-col overflow-hidden"
234
+ >
235
+ <HelpHubManualForm
236
+ :manual="editing"
237
+ :default-app="editing ? editing.app : (currentModule || undefined)"
238
+ @saved="onSaved"
239
+ @cancel="backToList"
240
+ />
241
+ </div>
242
+
243
+ <!-- View -->
244
+ <div
245
+ v-else
246
+ class="relative flex-1 overflow-hidden bg-gray-600"
247
+ >
248
+ <iframe
249
+ v-if="current?.file"
250
+ :key="current.file.url"
251
+ :src="`${current.file.url}#view=FitH`"
252
+ title="User manual"
253
+ class="size-full border-0"
254
+ />
255
+ <div
256
+ v-else
257
+ class="flex h-full items-center justify-center bg-white text-sm text-gray-500"
258
+ >
259
+ ไม่พบไฟล์คู่มือ
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </template>
264
+ </Slideover>
265
+
266
+ <Modal
267
+ :open="!!confirmDelete"
268
+ :ui="{ content: 'max-w-sm' }"
269
+ @update:open="(v) => { if (!v) confirmDelete = null }"
270
+ >
271
+ <template #content>
272
+ <div class="flex flex-col gap-4 p-6">
273
+ <div>
274
+ <h3 class="text-base font-semibold text-gray-900">
275
+ ลบคู่มือ
276
+ </h3>
277
+ <p class="mt-1 text-sm text-gray-500">
278
+ ลบ “{{ confirmDelete?.name }}” ออกจากระบบ? ไฟล์ PDF จะยังอยู่ในคลังไฟล์
279
+ </p>
280
+ </div>
281
+ <div class="flex justify-end gap-2">
282
+ <Button
283
+ variant="outline"
284
+ color="neutral"
285
+ @click="confirmDelete = null"
286
+ >
287
+ ยกเลิก
288
+ </Button>
289
+ <Button
290
+ color="error"
291
+ :loading="deleteLoader.status.value.isLoading"
292
+ @click="runDelete"
293
+ >
294
+ ลบ
295
+ </Button>
296
+ </div>
297
+ </div>
298
+ </template>
299
+ </Modal>
300
+ </template>
301
+
302
+ <script lang="ts" setup>
303
+ import type { IManual } from '#layer/app/loaders/manual/manual'
304
+ import { useManualDeleteLoader } from '#layer/app/loaders/manual/manual'
305
+ import { Permission, UserModuleLabel, UserModuleLogo } from '#layer/app/composables/useAuth'
306
+ import type { UserModule } from '#layer/app/composables/useAuth'
307
+
308
+ const emit = defineEmits<{
309
+ (e: 'update', value: boolean): void
310
+ (e: 'refetch'): void
311
+ (e: 'feedback'): void
312
+ }>()
313
+
314
+ const props = defineProps<{
315
+ isOpen: boolean
316
+ manuals: IManual[]
317
+ currentModule: UserModule | null
318
+ loading: boolean
319
+ }>()
320
+
321
+ const auth = useAuth()
322
+ const noti = useNotification()
323
+ const deleteLoader = useManualDeleteLoader()
324
+
325
+ type Mode = 'list' | 'view' | 'form'
326
+
327
+ const mode = ref<Mode>('list')
328
+ const selectedId = ref<string | null>(null)
329
+ const editing = ref<IManual | null>(null)
330
+ const confirmDelete = ref<IManual | null>(null)
331
+
332
+ const current = computed<IManual | null>(
333
+ () => props.manuals.find((m) => m.id === selectedId.value) ?? null,
334
+ )
335
+
336
+ const canManage = (app: string) =>
337
+ auth.isSuperAdmin.value || auth.hasPermission(app as UserModule, Permission.SUPER)
338
+
339
+ const canManageAny = computed(() => {
340
+ if (auth.isSuperAdmin.value) return true
341
+
342
+ return props.manuals.some((m) => canManage(m.app))
343
+ || (!!props.currentModule && auth.hasPermission(props.currentModule as UserModule, Permission.SUPER))
344
+ })
345
+
346
+ const appLabel = (app: string) =>
347
+ UserModuleLabel[app as keyof typeof UserModuleLabel] || app
348
+
349
+ const logoOf = (app: string) =>
350
+ UserModuleLogo[app as keyof typeof UserModuleLogo]
351
+
352
+ const formatDate = (iso: string) => {
353
+ try {
354
+ return new Date(iso).toLocaleDateString('th-TH', {
355
+ day: 'numeric',
356
+ month: 'short',
357
+ year: 'numeric',
358
+ })
359
+ } catch {
360
+ return iso
361
+ }
362
+ }
363
+
364
+ const groups = computed(() => {
365
+ const byApp = new Map<string, IManual[]>()
366
+
367
+ for (const manual of props.manuals) {
368
+ const list = byApp.get(manual.app) ?? []
369
+
370
+ list.push(manual)
371
+ byApp.set(manual.app, list)
372
+ }
373
+
374
+ return [...byApp.entries()]
375
+ .map(([app, items]) => ({
376
+ app,
377
+ label: appLabel(app),
378
+ items: [...items].sort((a, b) => a.sort_order - b.sort_order || a.name.localeCompare(b.name)),
379
+ }))
380
+ .sort((a, b) => {
381
+ if (a.app === props.currentModule) return -1
382
+ if (b.app === props.currentModule) return 1
383
+
384
+ return a.label.localeCompare(b.label)
385
+ })
386
+ })
387
+
388
+ const headerTitle = computed(() => {
389
+ if (mode.value === 'view' && current.value) return current.value.name
390
+ if (mode.value === 'form') return editing.value ? 'แก้ไขคู่มือ' : 'เพิ่มคู่มือ'
391
+
392
+ return 'คู่มือการใช้งาน'
393
+ })
394
+
395
+ const headerSubtitle = computed(() => {
396
+ if (mode.value === 'view' && current.value) {
397
+ return `${appLabel(current.value.app)} · ปรับปรุง ${formatDate(current.value.updated_at)}`
398
+ }
399
+
400
+ if (mode.value === 'form') return 'ไฟล์ PDF · เห็นเฉพาะผู้มีสิทธิ์ในแอพนั้น'
401
+
402
+ return 'เลือกคู่มือที่ต้องการเปิด'
403
+ })
404
+
405
+ const openManual = (manual: IManual) => {
406
+ selectedId.value = manual.id
407
+ mode.value = 'view'
408
+ }
409
+
410
+ const startCreate = () => {
411
+ editing.value = null
412
+ mode.value = 'form'
413
+ }
414
+
415
+ const startEdit = (manual: IManual) => {
416
+ editing.value = manual
417
+ mode.value = 'form'
418
+ }
419
+
420
+ const backToList = () => {
421
+ mode.value = 'list'
422
+ editing.value = null
423
+ }
424
+
425
+ const onSaved = () => {
426
+ emit('refetch')
427
+ backToList()
428
+ }
429
+
430
+ const runDelete = () => {
431
+ const target = confirmDelete.value
432
+ if (!target) return
433
+
434
+ deleteLoader.run({
435
+ urlBind: {
436
+ id: target.id,
437
+ },
438
+ })
439
+ }
440
+
441
+ useWatchTrue(() => deleteLoader.status.value.isSuccess, () => {
442
+ confirmDelete.value = null
443
+
444
+ if (current.value && !props.manuals.some((m) => m.id === current.value?.id)) {
445
+ mode.value = 'list'
446
+ }
447
+
448
+ emit('refetch')
449
+ })
450
+
451
+ useWatchTrue(() => deleteLoader.status.value.isError, () => {
452
+ noti.error({
453
+ title: 'ลบไม่สำเร็จ',
454
+ description: StringHelper.getError(deleteLoader.status.value.errorData, 'ลองใหม่อีกครั้ง'),
455
+ })
456
+ })
457
+
458
+ // Always land on the picker when (re)opened.
459
+ useWatchTrue(() => props.isOpen, () => {
460
+ mode.value = 'list'
461
+ selectedId.value = null
462
+ editing.value = null
463
+ })
464
+ </script>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <div class="fixed right-7 bottom-7 z-40 md:bottom-8.5 print:hidden">
3
+ <!-- click-away scrim -->
4
+ <div
5
+ v-if="isDialOpen"
6
+ class="fixed inset-0"
7
+ @click="isDialOpen = false"
8
+ />
9
+
10
+ <div class="relative flex flex-col items-end gap-3.5">
11
+ <div
12
+ v-if="isDialOpen"
13
+ class="flex flex-col items-end gap-3.5 motion-safe:animate-[helpHubDialIn_160ms_ease-out]"
14
+ >
15
+ <HelpHubAction
16
+ label="ส่ง Feedback"
17
+ icon="ph:chat-circle-text"
18
+ @click="open('feedback')"
19
+ />
20
+ <HelpHubAction
21
+ v-if="showManualAction"
22
+ label="คู่มือการใช้งาน"
23
+ icon="ph:book-open"
24
+ @click="open('manual')"
25
+ />
26
+ </div>
27
+
28
+ <button
29
+ class="flex size-10 cursor-pointer items-center justify-center rounded-full shadow-lg ring-2 transition-all duration-200 md:size-12"
30
+ :class="isDialOpen
31
+ ? 'bg-gray-900 ring-gray-700 hover:bg-gray-800'
32
+ : 'bg-primary-600 ring-primary-200 hover:bg-primary-700 hover:scale-105 hover:shadow-xl'"
33
+ type="button"
34
+ :aria-expanded="isDialOpen"
35
+ aria-label="ความช่วยเหลือ — ส่ง Feedback หรือดูคู่มือการใช้งาน"
36
+ :title="isDialOpen ? 'ปิด' : 'ความช่วยเหลือ'"
37
+ @click="isDialOpen = !isDialOpen"
38
+ >
39
+ <Icon
40
+ :name="isDialOpen ? 'ph:x' : 'ph:question'"
41
+ class="size-6 text-white transition-transform duration-200 md:size-7"
42
+ :class="{ 'rotate-90': isDialOpen }"
43
+ />
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <FeedbackTicketForm
49
+ :is-open="panel === 'feedback'"
50
+ @update="(v) => { if (!v) panel = null }"
51
+ @success="panel = null"
52
+ />
53
+
54
+ <HelpHubManualViewer
55
+ :is-open="panel === 'manual'"
56
+ :manuals="manuals"
57
+ :current-module="currentModule"
58
+ :loading="manualList.status.value.isLoading"
59
+ @update="(v) => { if (!v) panel = null }"
60
+ @refetch="manualList.run()"
61
+ @feedback="open('feedback')"
62
+ />
63
+ </template>
64
+
65
+ <script lang="ts" setup>
66
+ import { useManualListLoader } from '#layer/app/loaders/manual/manual'
67
+ import { Permission } from '#layer/app/composables/useAuth'
68
+ import type { UserModule } from '#layer/app/composables/useAuth'
69
+
70
+ type Panel = 'feedback' | 'manual' | null
71
+
72
+ const auth = useAuth()
73
+ const route = useRoute()
74
+ const currentModule = useCurrentModule()
75
+ const manualList = useManualListLoader()
76
+
77
+ const isDialOpen = ref(false)
78
+ const panel = ref<Panel>(null)
79
+
80
+ const manuals = computed(() => manualList.data.value ?? [])
81
+
82
+ const canManageAny = computed(() => {
83
+ if (auth.isSuperAdmin.value) return true
84
+
85
+ const mod = currentModule.value
86
+
87
+ return !!mod && auth.hasPermission(mod as UserModule, Permission.SUPER)
88
+ })
89
+
90
+ const showManualAction = computed(() => manuals.value.length > 0 || canManageAny.value)
91
+
92
+ const open = (next: Exclude<Panel, null>) => {
93
+ isDialOpen.value = false
94
+ panel.value = next
95
+ }
96
+
97
+ const onKeydown = (e: KeyboardEvent) => {
98
+ if (e.key === 'Escape' && isDialOpen.value) {
99
+ isDialOpen.value = false
100
+ }
101
+ }
102
+
103
+ onMounted(() => {
104
+ window.addEventListener('keydown', onKeydown)
105
+
106
+ if (auth.isAuthenticated.value && !manualList.status.value.isSuccess) {
107
+ manualList.run()
108
+ }
109
+ })
110
+
111
+ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
112
+
113
+ watch(() => route.path, () => {
114
+ isDialOpen.value = false
115
+ })
116
+ </script>
@@ -0,0 +1,9 @@
1
+ import type { UserModule } from '#layer/app/composables/useAuth'
2
+ import { getModuleFromPath } from '#layer/app/constants/module-route'
3
+
4
+ /** The app/module the user is currently in, derived from the route. `null` on global pages. */
5
+ export const useCurrentModule = () => {
6
+ const route = useRoute()
7
+
8
+ return computed<UserModule | null>(() => getModuleFromPath(route.path))
9
+ }
@@ -0,0 +1,48 @@
1
+ import { UserModule } from '#layer/app/composables/useAuth'
2
+
3
+ /**
4
+ * Route-path prefix -> the app/module it belongs to.
5
+ * Order matters: longer / more specific prefixes must come first.
6
+ */
7
+ export const MODULE_ROUTE_PREFIXES: readonly (readonly [string, UserModule])[] = [
8
+ ['/timesheet-admin', UserModule.TIMESHEET],
9
+ ['/timesheet', UserModule.TIMESHEET],
10
+ ['/clockin-admin', UserModule.CHECKIN],
11
+ ['/clockin', UserModule.CHECKIN],
12
+ ['/pmo-setup', UserModule.PMO_SETUP],
13
+ ['/pmo-presale-bidding', UserModule.PMO_PRESALE_BIDDING],
14
+ ['/pmo-delivery', UserModule.PMO_DELIVERY],
15
+ ['/pmo-warranty', UserModule.PMO_WARRANTY],
16
+ ['/pmo', UserModule.PMO],
17
+ ['/resource-allocation', UserModule.RESOURCE_ALLOCATION],
18
+ ['/cost-sheet', UserModule.COSTSHEET],
19
+ ['/e-factoring', UserModule.EFACTORING],
20
+ ['/legal', UserModule.CONTRACT],
21
+ ['/quotation', UserModule.QUOTATION],
22
+ ['/newsletter', UserModule.NEWSLETTER],
23
+ ['/announcement', UserModule.NEWSLETTER],
24
+ ['/announcements', UserModule.NEWSLETTER],
25
+ ['/todo', UserModule.TODO],
26
+ ['/employee', UserModule.EMPLOYEE],
27
+ ['/asset', UserModule.ASSET],
28
+ ['/request-admin', UserModule.REQUEST],
29
+ ['/request', UserModule.REQUEST],
30
+ ['/resource', UserModule.RESOURCE],
31
+ ['/lg', UserModule.LG],
32
+ ['/evaluation', UserModule.EVALUATION],
33
+ ['/loan', UserModule.LOAN],
34
+ ['/purchase', UserModule.PURCHASE],
35
+ ['/tor', UserModule.TOR],
36
+ ['/support', UserModule.SUPPORT],
37
+ ['/recruit', UserModule.RECRUIT],
38
+ ['/docs-management', UserModule.DOCUMENT],
39
+ ]
40
+
41
+ /** Resolve the current app/module from a route path, or `null` for global pages. */
42
+ export const getModuleFromPath = (path: string): UserModule | null => {
43
+ const hit = MODULE_ROUTE_PREFIXES.find(
44
+ ([prefix]) => path === prefix || path.startsWith(`${prefix}/`),
45
+ )
46
+
47
+ return hit ? hit[1] : null
48
+ }
@@ -0,0 +1,60 @@
1
+ export interface IManualFile {
2
+ id: string
3
+ name: string
4
+ url: string
5
+ path: string
6
+ size: number
7
+ }
8
+
9
+ export interface IManual {
10
+ id: string
11
+ app: string
12
+ name: string
13
+ description: string | null
14
+ file_id: string
15
+ sort_order: number
16
+ file: IManualFile | null
17
+ created_at: string
18
+ updated_at: string
19
+ }
20
+
21
+ /** All manuals the signed-in user is allowed to see (filtered server-side). */
22
+ export const useManualListLoader = () => {
23
+ const options = useRequestOptions()
24
+
25
+ return useObjectLoader<IManual[]>({
26
+ method: 'GET',
27
+ url: '/manuals',
28
+ getRequestOptions: options.auth,
29
+ })
30
+ }
31
+
32
+ export const useManualAddLoader = () => {
33
+ const options = useRequestOptions()
34
+
35
+ return useObjectLoader<IManual>({
36
+ method: 'POST',
37
+ url: '/manuals',
38
+ getRequestOptions: options.auth,
39
+ })
40
+ }
41
+
42
+ export const useManualUpdateLoader = () => {
43
+ const options = useRequestOptions()
44
+
45
+ return useObjectLoader<IManual>({
46
+ method: 'PUT',
47
+ url: '/manuals/:id',
48
+ getRequestOptions: options.auth,
49
+ })
50
+ }
51
+
52
+ export const useManualDeleteLoader = () => {
53
+ const options = useRequestOptions()
54
+
55
+ return useObjectLoader({
56
+ method: 'DELETE',
57
+ url: '/manuals/:id',
58
+ getRequestOptions: options.auth,
59
+ })
60
+ }