@finema/finework-layer 2.0.51 → 2.0.52

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 (40) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/app/components/Layout/Apps.vue +3 -1
  3. package/app/components/Notifications/index.vue +51 -24
  4. package/app/components/PortalApp.vue +258 -573
  5. package/app/composables/useAuth.ts +41 -22
  6. package/app/constants/routes.ts +8 -0
  7. package/package.json +1 -1
  8. package/public/admin/asset.svg +20 -0
  9. package/public/admin/clock-in-admin-logo.svg +33 -0
  10. package/public/admin/clock-in-logo.svg +27 -0
  11. package/public/admin/contract.svg +41 -0
  12. package/public/admin/cost-sheet.svg +19 -0
  13. package/public/admin/document.svg +29 -0
  14. package/public/admin/efactoring.svg +23 -0
  15. package/public/admin/employee.svg +16 -0
  16. package/public/admin/evaluation.svg +29 -0
  17. package/public/admin/finesign.png +0 -0
  18. package/public/admin/guarantee.svg +25 -0
  19. package/public/admin/loan.svg +35 -0
  20. package/public/admin/newsletter.svg +23 -0
  21. package/public/admin/pmo-logo.svg +28 -0
  22. package/public/admin/pmo_delivery.svg +21 -0
  23. package/public/admin/pmo_presale_bidding.svg +30 -0
  24. package/public/admin/pmo_setup.svg +18 -0
  25. package/public/admin/pmo_warranty.svg +26 -0
  26. package/public/admin/purchase.svg +22 -0
  27. package/public/admin/quotation.svg +21 -0
  28. package/public/admin/recruit.svg +21 -0
  29. package/public/admin/report.svg +41 -0
  30. package/public/admin/request-admin.svg +44 -0
  31. package/public/admin/request.svg +42 -0
  32. package/public/admin/resource-allocation.svg +26 -0
  33. package/public/admin/resource.svg +22 -0
  34. package/public/admin/spider-web.png +0 -0
  35. package/public/admin/super-admin-logo.svg +22 -0
  36. package/public/admin/support.svg +19 -0
  37. package/public/admin/timesheet-admin-logo.svg +87 -0
  38. package/public/admin/timesheet-logo.svg +85 -0
  39. package/public/admin/todo.svg +21 -0
  40. package/public/admin/tor.svg +43 -0
@@ -1,284 +1,126 @@
1
1
  <template>
2
- <DefineTemplate>
3
- <div class="space-y-6">
4
- <div
5
- v-if="dailyApps.length > 0"
6
- class="space-y-5"
7
- >
8
- <div class="text-lg font-bold text-gray-600">
9
- Daily work Apps
10
- </div>
11
- <div
12
- class="grid"
13
- :class="[isNavbar ?`gap-5 md:grid-cols-2 xl:grid-cols-4` : `grid-cols-1 gap-2`]"
14
- >
15
- <component
16
- :is="app.status === StatusPortal.DEVELOPING ? 'div' : NuxtLink"
17
- v-for="app in dailyApps"
18
- :key="app.name"
19
- :to="app.status !== StatusPortal.DEVELOPING ? app.to : undefined"
20
- class="relative flex items-center gap-3 rounded-lg px-2.5 py-[15px]"
21
- :class="app.status === StatusPortal.DEVELOPING
22
- ? 'cursor-not-allowed bg-gray-200 opacity-60'
23
- : 'bg-gray-100 hover:bg-gray-200'"
24
- @click="app.status !== StatusPortal.DEVELOPING && handleLinkClick(app.to)"
25
- >
26
- <div
27
- v-if="app.status === StatusPortal.DEVELOPING"
28
- class="absolute top-2 right-2 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600"
29
- >
30
- กำลังพัฒนา
31
- </div>
32
- <img
33
- :src="app.logo"
34
- :alt="app.label"
35
- class="h-[45px] w-[45px] bg-white"
36
- />
37
- <div>
38
- <div class="text-lg font-bold">
39
- {{ app.label }}
40
- </div>
41
- <div class="text-sm text-gray-500">
42
- {{ app.description }}
43
- </div>
44
- </div>
45
- </component>
46
- </div>
47
- </div>
48
- <div
49
- v-if="managementApps.length > 0"
50
- class="space-y-5"
51
- >
52
- <div class="text-lg font-bold text-gray-600">
53
- Management
54
- </div>
55
- <div
56
- class="grid"
57
- :class="[isNavbar ?`gap-5 md:grid-cols-2 xl:grid-cols-4` : `grid-cols-1 gap-2`]"
58
- >
59
- <component
60
- :is="app.status === StatusPortal.DEVELOPING ? 'div' : NuxtLink"
61
- v-for="app in managementApps"
62
- :key="app.name"
63
- :to="app.status !== StatusPortal.DEVELOPING ? app.to : undefined"
64
- class="relative flex items-center gap-3 rounded-lg px-2.5 py-[15px]"
65
- :class="app.status === StatusPortal.DEVELOPING
66
- ? 'cursor-not-allowed bg-gray-200 opacity-60'
67
- : 'bg-gray-100 hover:bg-gray-200'"
68
- @click="app.status !== StatusPortal.DEVELOPING && handleLinkClick(app.to)"
69
- >
70
- <div
71
- v-if="app.status === StatusPortal.DEVELOPING"
72
- class="absolute top-2 right-2 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600"
73
- >
74
- กำลังพัฒนา
75
- </div>
76
- <img
77
- :src="app.logo"
78
- :alt="app.label"
79
- class="h-[45px] w-[45px] bg-white"
80
- />
81
- <div>
82
- <div class="text-lg font-bold">
83
- {{ app.label }}
84
- </div>
85
- <div class="text-sm text-gray-500">
86
- {{ app.description }}
87
- </div>
88
- </div>
89
- </component>
90
- </div>
91
- </div>
2
+ <Card
3
+ v-if="categories.length > 0"
4
+ :ui="{
5
+ body: isNavbar
6
+ ? 'md:max-h-[calc(100vh-115px)] overflow-auto'
7
+ : 'h-full overflow-auto p-4!',
8
+ }"
9
+ >
10
+ <div class="space-y-6 bg-white">
11
+ <Input
12
+ v-model="search"
13
+ icon="ph:magnifying-glass"
14
+ placeholder="Search app"
15
+ size="lg"
16
+ :ui="{ base: 'rounded-lg' }"
17
+ />
92
18
 
93
19
  <div
94
- v-if="financeApps.length > 0"
95
- class="space-y-5"
20
+ v-for="category in filteredCategories"
21
+ :key="category.key"
22
+ class="space-y-3"
96
23
  >
97
- <div class="text-lg font-bold text-gray-600">
98
- Finance
24
+ <div class="text-base font-bold">
25
+ {{ category.title }}
99
26
  </div>
100
- <div
101
- class="grid"
102
- :class="[isNavbar ?`gap-5 md:grid-cols-2 xl:grid-cols-4` : `grid-cols-1 gap-2`]"
103
- >
104
- <component
105
- :is="app.status === StatusPortal.DEVELOPING ? 'div' : NuxtLink"
106
- v-for="app in financeApps"
107
- :key="app.name"
108
- :to="app.status !== StatusPortal.DEVELOPING ? app.to : undefined"
109
- class="relative flex items-center gap-3 rounded-lg px-2.5 py-[15px]"
110
- :class="app.status === StatusPortal.DEVELOPING
111
- ? 'cursor-not-allowed bg-gray-200 opacity-60'
112
- : 'bg-gray-100 hover:bg-gray-200'"
113
- @click="app.status !== StatusPortal.DEVELOPING && handleLinkClick(app.to)"
114
- >
115
- <div
116
- v-if="app.status === StatusPortal.DEVELOPING"
117
- class="absolute top-2 right-2 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600"
118
- >
119
- กำลังพัฒนา
120
- </div>
121
27
 
122
- <img
123
- :src="app.logo"
124
- :alt="app.label"
125
- class="h-[45px] w-[45px] bg-white"
126
- />
127
-
128
- <div>
129
- <div class="text-lg font-bold">
130
- {{ app.label }}
131
- </div>
132
- <div class="text-sm text-gray-500">
133
- {{ app.description }}
134
- </div>
135
- </div>
136
- </component>
137
- </div>
138
- </div>
139
-
140
- <div
141
- v-if="peopleApps.length >0"
142
- class="space-y-5"
143
- >
144
- <div class="text-lg font-bold text-gray-600">
145
- People
146
- </div>
28
+ <!-- Navbar: compact icon-on-top grid -->
147
29
  <div
148
- class="grid"
149
- :class="[isNavbar ?`gap-5 md:grid-cols-2 xl:grid-cols-4` : `grid-cols-1 gap-2`]"
30
+ v-if="isNavbar"
31
+ class="grid gap-x-2 gap-y-4"
32
+ :class="gridColsClass"
150
33
  >
151
34
  <component
152
- :is="app.status === StatusPortal.DEVELOPING ? 'div' : NuxtLink"
153
- v-for="app in peopleApps"
35
+ :is="linkComponent(app)"
36
+ v-for="app in category.apps"
154
37
  :key="app.name"
155
- :to="app.status !== StatusPortal.DEVELOPING ? app.to : undefined"
156
- class="relative flex items-center gap-3 rounded-lg px-2.5 py-[15px]"
157
- :class="app.status === StatusPortal.DEVELOPING
158
- ? 'cursor-not-allowed bg-gray-200 opacity-60'
159
- : 'bg-gray-100 hover:bg-gray-200'"
160
- @click="app.status !== StatusPortal.DEVELOPING && handleLinkClick(app.to)"
38
+ :to="linkTo(app)"
39
+ class="group flex flex-col items-center gap-2 rounded-xl p-2 text-center transition-colors"
40
+ :class="isDeveloping(app) ? 'cursor-not-allowed opacity-60' : 'hover:bg-gray-100'"
41
+ @click="onAppClick(app)"
161
42
  >
162
- <div
163
- v-if="app.status === StatusPortal.DEVELOPING"
164
- class="absolute top-2 right-2 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600"
165
- >
166
- กำลังพัฒนา
167
- </div>
168
- <img
169
- :src="app.logo"
170
- :alt="app.label"
171
- class="h-[45px] w-[45px] bg-white"
172
- />
173
- <div>
174
- <div class="text-lg font-bold">
175
- {{ app.label }}
176
- </div>
177
- <div class="text-sm text-gray-500">
178
- {{ app.description }}
179
- </div>
43
+ <div class="relative">
44
+ <img
45
+ :src="app.logo"
46
+ :alt="app.label"
47
+ class="size-12.5 rounded-xl object-contain"
48
+ />
49
+ <span
50
+ v-if="isDeveloping(app)"
51
+ class="absolute -top-1.5 -right-1.5 rounded-full bg-orange-100 px-1.5 py-0.5 text-[10px] font-semibold text-orange-600"
52
+ >
53
+ กำลังพัฒนา
54
+ </span>
180
55
  </div>
56
+ <span class="line-clamp-2 text-sm font-semibold">
57
+ {{ app.label }}
58
+ </span>
181
59
  </component>
182
60
  </div>
183
- </div>
184
61
 
185
- <div
186
- v-if="legalApps.length > 0"
187
- class="space-y-5"
188
- >
189
- <div class="text-lg font-bold text-gray-600">
190
- Legal
191
- </div>
62
+ <!-- Standalone: long list with descriptions -->
192
63
  <div
193
- class="grid"
194
- :class="[isNavbar ?`gap-5 md:grid-cols-2 xl:grid-cols-4` : `grid-cols-1 gap-2`]"
64
+ v-else
65
+ class="flex flex-col"
195
66
  >
196
67
  <component
197
- :is="app.status === StatusPortal.DEVELOPING ? 'div' : NuxtLink"
198
- v-for="app in legalApps"
68
+ :is="linkComponent(app)"
69
+ v-for="app in category.apps"
199
70
  :key="app.name"
200
- :to="app.status !== StatusPortal.DEVELOPING ? app.to : undefined"
201
- class="relative flex items-center gap-3 rounded-lg px-2.5 py-[15px]"
202
- :class="app.status === StatusPortal.DEVELOPING
203
- ? 'cursor-not-allowed bg-gray-200 opacity-60'
204
- : 'bg-gray-100 hover:bg-gray-200'"
205
- @click="app.status !== StatusPortal.DEVELOPING && handleLinkClick(app.to)"
71
+ :to="linkTo(app)"
72
+ class="flex items-start gap-3 border-b border-gray-100 px-1 py-3 transition-colors last:border-0"
73
+ :class="isDeveloping(app) ? 'cursor-not-allowed opacity-60' : 'hover:bg-gray-50'"
74
+ @click="onAppClick(app)"
206
75
  >
207
- <div
208
- v-if="app.status === StatusPortal.DEVELOPING"
209
- class="absolute top-2 right-2 rounded-full bg-orange-100 px-2 py-0.5 text-xs font-semibold text-orange-600"
210
- >
211
- กำลังพัฒนา
212
- </div>
213
76
  <img
214
77
  :src="app.logo"
215
78
  :alt="app.label"
216
- class="h-[45px] w-[45px] bg-white"
79
+ class="size-11 shrink-0 object-contain"
217
80
  />
218
- <div>
219
- <div class="text-lg font-bold">
81
+ <div class="min-w-0">
82
+ <div class="flex items-center gap-2 font-bold text-gray-900">
220
83
  {{ app.label }}
84
+ <span
85
+ v-if="isDeveloping(app)"
86
+ class="rounded-full bg-orange-100 px-1.5 py-0.5 text-[10px] font-semibold text-orange-600"
87
+ >
88
+ กำลังพัฒนา
89
+ </span>
221
90
  </div>
222
- <div class="text-sm text-gray-500">
91
+ <div class="line-clamp-2 text-sm text-gray-500">
223
92
  {{ app.description }}
224
93
  </div>
225
94
  </div>
226
95
  </component>
227
96
  </div>
228
97
  </div>
229
- </div>
230
- </DefineTemplate>
231
- <Card
232
- v-if="dailyApps.length > 0 || managementApps.length > 0 || financeApps.length > 0 || peopleApps.length > 0 || legalApps.length > 0"
233
- :ui="{
234
- body: isNavbar
235
- ? 'md:max-h-[100vh] md:max-h-[calc(100vh-115px)] overflow-auto'
236
- : 'h-full overflow-auto p-4!',
237
- }"
238
- >
239
- <div class="bg-white">
240
- <ReuseTemplate v-if="isNavbar" />
241
- <Collapsible
242
- v-else
243
- v-model:open="open"
244
- default-open
245
- >
246
- <div class="flex items-center justify-between gap-4">
247
- <div class="mb-5 flex items-center gap-2">
248
- <img
249
- src="/icon-finema.png"
250
- alt="icon-finema"
251
- class="h-8 w-8 -translate-y-1"
252
- />
253
- <div class="text-2xl font-bold">
254
- My Apps
255
- </div>
256
- </div>
257
98
 
258
- <Button
259
- v-if="!isNavbar"
260
- variant="outline"
261
- color="neutral"
262
- :icon="open ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
263
- />
264
- </div>
265
- <template #content>
266
- <ReuseTemplate />
267
- </template>
268
- </Collapsible>
99
+ <Empty
100
+ v-if="search && filteredCategories.length === 0"
101
+ icon="ph:magnifying-glass"
102
+ message="No apps found"
103
+ />
269
104
  </div>
270
105
  </Card>
271
106
  </template>
272
107
 
273
108
  <script lang="ts" setup>
274
109
  import { resolveComponent } from 'vue'
275
- import { createReusableTemplate } from '@vueuse/core'
110
+ // Import the shared maps straight from the layer so PortalApp stays deterministic
111
+ // even in apps that shadow UserModuleLabel / UserModuleLogo with their own copy.
112
+ import {
113
+ UserModuleAdminLabel,
114
+ UserModuleAdminLogo,
115
+ UserModuleLabel,
116
+ UserModuleLogo,
117
+ } from '#layer/app/composables/useAuth'
276
118
 
277
119
  const emit = defineEmits<{
278
120
  (e: 'close'): void
279
121
  }>()
280
122
 
281
- const props = defineProps<{
123
+ defineProps<{
282
124
  isNavbar?: boolean
283
125
  }>()
284
126
 
@@ -286,11 +128,26 @@ const enum StatusPortal {
286
128
  ACTIVE = 'active',
287
129
  DEVELOPING = 'developing',
288
130
  }
131
+
132
+ interface IPortalApp {
133
+ name: string
134
+ label: string
135
+ logo: string
136
+ description: string
137
+ to: string
138
+ status: StatusPortal
139
+ }
140
+
141
+ interface IPortalCategory {
142
+ key: string
143
+ title: string
144
+ apps: IPortalApp[]
145
+ }
146
+
289
147
  const NuxtLink = resolveComponent('NuxtLink')
290
148
  const auth = useAuth()
291
149
  const route = useRoute()
292
- const open = ref(true)
293
- const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
150
+ const search = ref('')
294
151
 
295
152
  const handleLinkClick = (to: string) => {
296
153
  if (route.path === to) {
@@ -298,372 +155,200 @@ const handleLinkClick = (to: string) => {
298
155
  }
299
156
  }
300
157
 
301
- const dailyApps = computed(() => [
302
- ...(auth.hasPermission(UserModule.CHECKIN, Permission.USER, Permission.ADMIN, Permission.SUPER)
303
- ? [
304
- {
305
- name: 'Clock-In',
306
- logo: '/admin/clock-in-logo.png',
307
- label: ' Clock-In',
308
- description: 'เช็กอินเวลางาน และเลือกสถานที่ทำงาน',
309
- to: routes.clockin.home.to,
310
- status: StatusPortal.ACTIVE,
311
- },
312
- ]
158
+ const isDeveloping = (app: IPortalApp) => app.status === StatusPortal.DEVELOPING
159
+ const linkComponent = (app: IPortalApp) => (isDeveloping(app) ? 'div' : NuxtLink)
160
+ const linkTo = (app: IPortalApp) => (isDeveloping(app) ? undefined : app.to)
161
+
162
+ const onAppClick = (app: IPortalApp) => {
163
+ if (!isDeveloping(app)) {
164
+ handleLinkClick(app.to)
165
+ }
166
+ }
167
+
168
+ const USER_ADMIN_SUPER = [Permission.USER, Permission.ADMIN, Permission.SUPER]
169
+ const ADMIN_SUPER = [Permission.ADMIN, Permission.SUPER]
170
+
171
+ const can = (module: UserModule, ...permissions: AccessPermission[]) =>
172
+ auth.hasPermission(module, ...permissions)
173
+
174
+ /** App whose label/logo come from the shared UserModule maps. */
175
+ const toApp = (
176
+ module: UserModule,
177
+ to: string,
178
+ description: string,
179
+ overrides: Partial<IPortalApp> = {},
180
+ ): IPortalApp => ({
181
+ name: overrides.name ?? module,
182
+ label: overrides.label ?? UserModuleLabel[module as keyof typeof UserModuleLabel],
183
+ logo: overrides.logo ?? UserModuleLogo[module as keyof typeof UserModuleLogo],
184
+ description,
185
+ to,
186
+ status: overrides.status ?? StatusPortal.ACTIVE,
187
+ })
188
+
189
+ /** Admin-console variant of a module, using the shared admin maps. */
190
+ const toAdminApp = (
191
+ module: keyof typeof UserModuleAdminLabel,
192
+ to: string,
193
+ description: string,
194
+ ): IPortalApp => ({
195
+ name: `${module}-admin`,
196
+ label: UserModuleAdminLabel[module],
197
+ logo: UserModuleAdminLogo[module],
198
+ description,
199
+ to,
200
+ status: StatusPortal.ACTIVE,
201
+ })
202
+
203
+ const dailyApps = computed<IPortalApp[]>(() => [
204
+ ...(can(UserModule.CHECKIN, ...USER_ADMIN_SUPER)
205
+ ? [toApp(UserModule.CHECKIN, routes.clockin.home.to, 'เช็กอินเวลางาน และเลือกสถานที่ทำงาน')]
313
206
  : []),
314
- {
315
- name: 'Timesheet',
316
- logo: '/admin/timesheet-logo.png',
317
- label: ' Timesheet',
318
- description: 'บันทึกงานที่ทำในแต่ละวัน',
319
- to: routes.timesheet.home.to,
320
- status: StatusPortal.ACTIVE,
321
- },
322
- {
323
- name: 'todo',
324
- logo: '/admin/todo.png',
325
- label: 'Todo',
326
- description: 'ระบบจัดการงาน',
327
- to: routes.todo.dashboard.to,
328
- status: StatusPortal.ACTIVE,
329
- },
330
- {
331
- name: 'Request',
332
- logo: '/admin/request.png',
333
- label: 'Request',
334
- description: 'ระบบขอ ลา เบิก อนุมัติรายการต่างๆ',
335
- to: routes.request.myRequest.requests.to,
336
- status: StatusPortal.ACTIVE,
337
- },
207
+ toApp(UserModule.TIMESHEET, routes.timesheet.home.to, 'บันทึกงานที่ทำในแต่ละวัน'),
208
+ toApp(UserModule.TODO, routes.todo.dashboard.to, 'ระบบจัดการงาน'),
209
+ toApp(UserModule.REQUEST, routes.request.myRequest.requests.to, 'ระบบขอ ลา เบิก อนุมัติรายการต่างๆ'),
210
+ ...(can(UserModule.CHECKIN, ...USER_ADMIN_SUPER)
211
+ ? [toApp(UserModule.CHECKIN, routes.clockin.home.to, 'เช็กอินเวลางาน และเลือกสถานที่ทำงาน')]
212
+ : []),
213
+ toApp(UserModule.TIMESHEET, routes.timesheet.home.to, 'บันทึกงานที่ทำในแต่ละวัน'),
214
+ toApp(UserModule.TODO, routes.todo.dashboard.to, 'ระบบจัดการงาน'),
215
+ toApp(UserModule.REQUEST, routes.request.myRequest.requests.to, 'ระบบขอ ลา เบิก อนุมัติรายการต่างๆ'),
338
216
  ])
339
217
 
340
- const managementApps = computed(() => [
341
- ...(auth.hasPermission(
342
- UserModule.PMO,
343
- Permission.ADMIN,
344
- Permission.USER,
345
- Permission.SUPER,
346
- )
347
- ? [
348
- {
349
- name: 'pmo',
350
- logo: '/admin/pmo-logo.png',
351
- label: 'PMO',
352
- description: 'จัดระเบียบและบริหารโครงการของคุณ',
353
- to: routes.pmo.project.projects.to,
354
- status: StatusPortal.ACTIVE,
355
- },
356
- ]
218
+ const managementApps = computed<IPortalApp[]>(() => [
219
+ ...(can(UserModule.PMO, ...USER_ADMIN_SUPER)
220
+ ? [toApp(UserModule.PMO, routes.pmo.project.projects.to, 'จัดระเบียบและบริหารโครงการของคุณ')]
357
221
  : []),
358
- ...(auth.hasPermission(
359
- UserModule.PMO_SETUP,
360
- Permission.ADMIN,
361
- Permission.USER,
362
- Permission.SUPER,
363
- )
364
- ? [
365
- {
366
- name: 'pmo-setup',
367
- logo: '/admin/pmo_setup.png',
368
- label: 'Lead in',
369
- description: 'ตั้งค่าและบริหารจัดการโครงการ PMO',
370
- to: routes.pmoSetup.dashboard.to,
371
- status: StatusPortal.ACTIVE,
372
- },
373
- ]
222
+ ...(can(UserModule.PMO_SETUP, ...USER_ADMIN_SUPER)
223
+ ? [toApp(UserModule.PMO_SETUP, routes.pmoSetup.dashboard.to, 'ตั้งค่าและบริหารจัดการโครงการ PMO')]
374
224
  : []),
375
- ...(auth.hasPermission(
376
- UserModule.PMO_PRESALE_BIDDING,
377
- Permission.ADMIN,
378
- Permission.USER,
379
- Permission.SUPER,
380
- )
381
- ? [
382
- {
383
- name: 'pmo-presale-bidding',
384
- logo: '/admin/pmo_presale_bidding.png',
385
- label: 'Presale & Bidding',
386
- description: 'บริหารจัดการงาน Presale และ Bidding',
387
- to: routes.pmoPresaleBidding.dashboard.to,
388
- status: StatusPortal.ACTIVE,
389
- },
390
- ]
225
+ ...(can(UserModule.PMO_PRESALE_BIDDING, ...USER_ADMIN_SUPER)
226
+ ? [toApp(UserModule.PMO_PRESALE_BIDDING, routes.pmoPresaleBidding.dashboard.to, 'บริหารจัดการงาน Presale และ Bidding')]
391
227
  : []),
392
- ...(auth.hasPermission(
393
- UserModule.PMO_DELIVERY,
394
- Permission.ADMIN,
395
- Permission.USER,
396
- Permission.SUPER,
397
- )
398
- ? [
399
- {
400
- name: 'pmo-delivery',
401
- logo: '/admin/pmo_delivery.png',
402
- label: 'Delivery',
403
- description: 'บริหารจัดการโครงการช่วง Delivery',
404
- to: routes.pmoDelivery.dashboard.to,
405
- status: StatusPortal.ACTIVE,
406
- },
407
- ]
228
+ ...(can(UserModule.PMO_DELIVERY, ...USER_ADMIN_SUPER)
229
+ ? [toApp(UserModule.PMO_DELIVERY, routes.pmoDelivery.dashboard.to, 'บริหารจัดการโครงการช่วง Delivery')]
408
230
  : []),
409
- ...(auth.hasPermission(
410
- UserModule.PMO_WARRANTY,
411
- Permission.ADMIN,
412
- Permission.USER,
413
- Permission.SUPER,
414
- )
415
- ? [
416
- {
417
- name: 'pmo-warranty',
418
- logo: '/admin/pmo_warranty.png',
419
- label: 'Warranty',
420
- description: 'บริหารจัดการโครงการช่วง Warranty',
421
- to: routes.pmoWarranty.dashboard.to,
422
- status: StatusPortal.ACTIVE,
423
- },
424
- ]
231
+ ...(can(UserModule.PMO_WARRANTY, ...USER_ADMIN_SUPER)
232
+ ? [toApp(UserModule.PMO_WARRANTY, routes.pmoWarranty.dashboard.to, 'บริหารจัดการโครงการช่วง Warranty')]
425
233
  : []),
426
- ...(auth.hasPermission(UserModule.CHECKIN, Permission.ADMIN, Permission.SUPER)
427
- ? [
428
- {
429
- name: 'clockin-admin',
430
- logo: '/admin/clock-in-admin-logo.png',
431
- label: 'Clock-in Report',
432
- description: 'รายงานการลงเวลา',
433
- to: routes.adminClockin.checkinDashboard.to,
434
- status: StatusPortal.ACTIVE,
435
- },
436
- ]
234
+ ...(can(UserModule.RESOURCE_ALLOCATION, ...ADMIN_SUPER)
235
+ ? [toApp(UserModule.RESOURCE_ALLOCATION, routes.resourceAllocation.dashboard.to, 'วางแผนและจัดสรรกำลังคนในโครงการ')]
437
236
  : []),
438
-
439
- ...(auth.hasPermission(UserModule.TIMESHEET, Permission.ADMIN, Permission.SUPER)
440
- ? [
441
- {
442
- name: 'timesheet-admin',
443
- logo: '/admin/timesheet-admin-logo.png',
444
- label: 'Timesheet Report',
445
- description: 'รายงานการบันทึกข้อมูลไทม์ชีท',
446
- to: routes.adminTimesheet.summaryReport.to,
447
- status: StatusPortal.ACTIVE,
448
- },
449
- ]
237
+ ...(can(UserModule.CHECKIN, ...ADMIN_SUPER)
238
+ ? [toAdminApp(UserModule.CHECKIN, routes.adminClockin.checkinDashboard.to, 'รายงานการลงเวลา')]
450
239
  : []),
451
- ...(auth.hasPermission(UserModule.SETTING, Permission.ADMIN, Permission.SUPER)
452
- ? [
453
- {
454
- name: 'report',
455
- logo: '/admin/report.svg',
456
- label: 'Report',
457
- description: 'รายงานภาพรวมบริษัท',
458
- to: routes.report.checkinSummaryDashboard.to,
459
- status: StatusPortal.ACTIVE,
460
- },
461
- ]
240
+ ...(can(UserModule.TIMESHEET, ...ADMIN_SUPER)
241
+ ? [toAdminApp(UserModule.TIMESHEET, routes.adminTimesheet.summaryReport.to, 'รายงานการบันทึกข้อมูลไทม์ชีท')]
462
242
  : []),
463
- ...(auth.hasPermission(UserModule.TOR, Permission.USER, Permission.ADMIN, Permission.SUPER)
464
- ? [
465
- {
466
- name: 'tor',
467
- logo: '/admin/tor.svg',
468
- label: 'Presales Tools',
469
- description: 'ระบบสร้างตารางเปรียบเทียบข้อกำหนด',
470
- to: routes.tor.home.to,
471
- status: StatusPortal.ACTIVE,
472
- },
473
- ]
243
+ ...(can(UserModule.SETTING, Permission.SUPER)
244
+ ? [toAdminApp(UserModule.SETTING, routes.report.checkinSummaryDashboard.to, 'รายงานภาพรวมบริษัท')]
474
245
  : []),
475
- ...(auth.hasPermission(UserModule.SUPPORT, Permission.USER, Permission.ADMIN, Permission.SUPER)
476
- ? [
477
- {
478
- name: 'support',
479
- logo: '/admin/support.svg',
480
- label: 'Support',
481
- description: 'Heatmap Monitor & Report',
482
- to: routes.support.home.to,
483
- status: StatusPortal.ACTIVE,
484
- },
485
- ]
246
+ ...(can(UserModule.TOR, ...USER_ADMIN_SUPER)
247
+ ? [toApp(UserModule.TOR, routes.tor.home.to, 'ระบบสร้างตารางเปรียบเทียบข้อกำหนด')]
486
248
  : []),
487
- ...(auth.hasPermission(UserModule.DOCUMENT, Permission.USER, Permission.ADMIN, Permission.SUPER)
488
- ? [
489
- {
490
- name: 'document',
491
- logo: '/admin/document.svg',
492
- label: 'Document Flow',
493
- description: 'ทะเบียนเอกสาร รับ-ส่ง',
494
- to: routes.docsManagement.root.to,
495
- status: StatusPortal.ACTIVE,
496
- },
497
- ]
249
+ ...(can(UserModule.SUPPORT, ...USER_ADMIN_SUPER)
250
+ ? [toApp(UserModule.SUPPORT, routes.support.home.to, 'Heatmap Monitor & Report')]
251
+ : []),
252
+ ...(can(UserModule.DOCUMENT, ...USER_ADMIN_SUPER)
253
+ ? [toApp(UserModule.DOCUMENT, routes.docsManagement.root.to, 'ทะเบียนเอกสาร รับ-ส่ง')]
498
254
  : []),
499
-
500
255
  ])
501
256
 
502
- const financeApps = computed(() => [
503
-
504
- ...(auth.hasPermission(UserModule.QUOTATION, Permission.USER, Permission.ADMIN, Permission.SUPER)
505
- ? [
506
- {
507
- name: 'Quotation',
508
- logo: '/admin/quotation.png',
509
- label: 'Quotation',
510
- description: 'รายรับรายจ่ายงบประมาณโครงการ',
511
- to: routes.quotation.quotations.home.to,
512
- status: StatusPortal.ACTIVE,
513
- },
514
- ]
257
+ const financeApps = computed<IPortalApp[]>(() => [
258
+ ...(can(UserModule.QUOTATION, ...USER_ADMIN_SUPER)
259
+ ? [toApp(UserModule.QUOTATION, routes.quotation.quotations.home.to, 'รายรับรายจ่ายงบประมาณโครงการ')]
515
260
  : []),
516
- ...(auth.hasPermission(UserModule.COSTSHEET, Permission.USER, Permission.ADMIN, Permission.SUPER)
517
- ? [
518
- {
519
- name: 'Cost Sheet',
520
- logo: '/admin/cost-sheet.png',
521
- label: 'Cost Sheet',
522
- description: 'รายรับรายจ่ายงบประมาณโครงการ',
523
- to: routes.costSheet.overview.to,
524
- status: StatusPortal.ACTIVE,
525
- },
526
- ]
261
+ ...(can(UserModule.COSTSHEET, ...USER_ADMIN_SUPER)
262
+ ? [toApp(UserModule.COSTSHEET, routes.costSheet.overview.to, 'รายรับรายจ่ายงบประมาณโครงการ')]
527
263
  : []),
528
- ...(auth.hasPermission(UserModule.EFACTORING, Permission.USER, Permission.ADMIN, Permission.SUPER)
529
- ? [
530
- {
531
- name: 'eFactoring',
532
- logo: '/admin/efactoring.png',
533
- label: 'eFactoring',
534
- description: 'การเบิกเงินหมุนเวียนกับธนาคาร',
535
- to: routes.eFactoring.dashboard.to,
536
- status: StatusPortal.ACTIVE,
537
- },
538
- ]
264
+ ...(can(UserModule.EFACTORING, ...USER_ADMIN_SUPER)
265
+ ? [toApp(UserModule.EFACTORING, routes.eFactoring.dashboard.to, 'การเบิกเงินหมุนเวียนกับธนาคาร')]
539
266
  : []),
540
- ...(auth.hasPermission(UserModule.LG, Permission.USER, Permission.ADMIN, Permission.SUPER)
541
- ? [
542
- {
543
- name: 'Guarantee',
544
- logo: '/admin/guarantee.png',
545
- label: 'งบค้ำประกัน',
546
- description: 'การเบิกเงินค้ำซองและค้ำสัญญา',
547
- to: routes.lg.overview.to,
548
- status: StatusPortal.ACTIVE,
549
- },
550
- ]
267
+ ...(can(UserModule.LG, ...USER_ADMIN_SUPER)
268
+ ? [toApp(UserModule.LG, routes.lg.overview.to, 'การเบิกเงินค้ำซองและค้ำสัญญา')]
551
269
  : []),
552
- ...(auth.hasPermission(UserModule.LOAN, Permission.USER, Permission.ADMIN, Permission.SUPER)
553
- ? [
554
- {
555
- name: 'Loan',
556
- logo: '/admin/loan.svg',
557
- label: 'Loan',
558
- description: 'รายการกู้เงินจากธนาคาร',
559
- to: routes.loan.list.lists.to,
560
- status: StatusPortal.ACTIVE,
561
- },
562
- ]
270
+ ...(can(UserModule.LOAN, ...USER_ADMIN_SUPER)
271
+ ? [toApp(UserModule.LOAN, routes.loan.list.lists.to, 'รายการกู้เงินจากธนาคาร')]
563
272
  : []),
564
273
  ])
565
274
 
566
- const peopleApps = computed(() => [
567
- ...(auth.hasPermission(UserModule.EMPLOYEE, Permission.USER, Permission.ADMIN, Permission.SUPER)
568
- ? [
569
- {
570
- name: 'Employee',
571
- logo: '/admin/employee.png',
572
- label: 'Employee',
573
- description: 'ระบบจัดการข้อมูลพนักงาน',
574
- to: routes.employee.root.to,
575
- status: StatusPortal.ACTIVE,
576
- },
577
- ]
275
+ const peopleApps = computed<IPortalApp[]>(() => [
276
+ ...(can(UserModule.EMPLOYEE, ...USER_ADMIN_SUPER)
277
+ ? [toApp(UserModule.EMPLOYEE, routes.employee.root.to, 'ระบบจัดการข้อมูลพนักงาน')]
578
278
  : []),
579
- ...(auth.hasPermission(UserModule.NEWSLETTER, Permission.USER, Permission.ADMIN, Permission.SUPER)
580
- ? [
581
- {
582
- name: 'Newsletter',
583
- logo: '/admin/newsletter.png',
584
- label: 'Newsletter',
585
- description: 'ระบบจัดการข่าวสาร',
586
- to: routes.newsletter.announcement.announcements.to,
587
- status: StatusPortal.ACTIVE,
588
- },
589
- ]
279
+ ...(can(UserModule.NEWSLETTER, ...USER_ADMIN_SUPER)
280
+ ? [toApp(UserModule.NEWSLETTER, routes.newsletter.announcement.announcements.to, 'ระบบจัดการข่าวสาร')]
590
281
  : []),
591
-
592
- ...(auth.hasPermission(UserModule.ASSET, Permission.USER, Permission.ADMIN, Permission.SUPER)
593
- ? [
594
- {
595
- name: 'Assets',
596
- logo: '/admin/assets.png',
597
- label: 'Assets',
598
- description: 'ระบบจัดการสินทรัพย์',
599
- to: routes.asset.dashboard.to,
600
- status: StatusPortal.ACTIVE,
601
- },
602
- ]
282
+ ...(can(UserModule.ASSET, ...USER_ADMIN_SUPER)
283
+ ? [toApp(UserModule.ASSET, routes.asset.dashboard.to, 'ระบบจัดการสินทรัพย์')]
603
284
  : []),
604
- ...(auth.hasPermission(UserModule.RESOURCE, Permission.USER, Permission.ADMIN, Permission.SUPER)
605
- ? [
606
- {
607
- name: 'Resource',
608
- logo: '/admin/resource.svg',
609
- label: 'Resource',
610
- description: 'ระบบจัดการทรัพยากรบุคคล',
611
- to: routes.resource.talentSearchAndCvBuilder.to,
612
- status: StatusPortal.ACTIVE,
613
- },
614
- ]
285
+ ...(can(UserModule.RESOURCE, ...USER_ADMIN_SUPER)
286
+ ? [toApp(UserModule.RESOURCE, routes.resource.talentSearchAndCvBuilder.to, 'ระบบจัดการทรัพยากรบุคคล')]
615
287
  : []),
616
- ...(auth.hasPermission(UserModule.REQUEST, Permission.SUPER, Permission.ADMIN)
617
- ? [
618
- {
619
- name: 'Request Admin',
620
- logo: '/admin/request-admin.svg',
621
- label: 'Request Admin',
622
- description: 'ระบบจัดการคำขออนุมัติต่างๆ',
623
- to: routes.requestAdmin.userRequest.users.to,
624
- status: StatusPortal.ACTIVE,
625
- },
626
- ]
288
+ ...(can(UserModule.REQUEST, Permission.SUPER, Permission.ADMIN)
289
+ ? [toAdminApp(UserModule.REQUEST, routes.requestAdmin.userRequest.users.to, 'ระบบจัดการคำขออนุมัติต่างๆ')]
627
290
  : []),
628
- ...(auth.hasPermission(UserModule.EVALUATION, Permission.USER, Permission.ADMIN, Permission.SUPER)
629
- ? [
630
- {
631
- name: 'Evaluation',
632
- logo: '/admin/evaluation.png',
633
- label: 'Evaluation',
634
- description: 'ระบบจัดการการประเมิน',
635
- to: routes.evaluation.general.generals.to,
636
- status: StatusPortal.ACTIVE,
637
- },
638
- ]
291
+ ...(can(UserModule.EVALUATION, ...USER_ADMIN_SUPER)
292
+ ? [toApp(UserModule.EVALUATION, routes.evaluation.general.generals.to, 'ระบบจัดการการประเมิน')]
639
293
  : []),
640
- ...(auth.hasPermission(UserModule.RECRUIT, Permission.ADMIN, Permission.SUPER)
641
- ? [
642
- {
643
- name: 'Recruit',
644
- logo: '/admin/recruit.png',
645
- label: 'Recruit',
646
- description: 'ระบบการสรรหาบุคลากร',
647
- to: routes.recruit.root.to,
648
- status: StatusPortal.ACTIVE,
649
- },
650
- ]
294
+ ...(can(UserModule.RECRUIT, ...ADMIN_SUPER)
295
+ ? [toApp(UserModule.RECRUIT, routes.recruit.root.to, 'ระบบการสรรหาบุคลากร')]
651
296
  : []),
652
-
653
297
  ])
654
298
 
655
- const legalApps = computed(() => [
656
- ...(auth.hasPermission(UserModule.CONTRACT, Permission.USER, Permission.ADMIN, Permission.SUPER)
657
- ? [
658
- {
659
- name: 'Contract',
660
- logo: '/admin/contract.png',
661
- label: 'Contract',
662
- description: 'สร้างและจัดการสัญญา',
663
- to: routes.legal.dashboard.to,
664
- status: StatusPortal.ACTIVE,
665
- },
666
- ]
299
+ const legalApps = computed<IPortalApp[]>(() => [
300
+ ...(can(UserModule.CONTRACT, ...USER_ADMIN_SUPER)
301
+ ? [toApp(UserModule.CONTRACT, routes.legal.dashboard.to, 'สร้างและจัดการสัญญา')]
667
302
  : []),
668
303
  ])
304
+
305
+ const categories = computed<IPortalCategory[]>(() =>
306
+ [
307
+ {
308
+ key: 'daily',
309
+ title: 'Daily work Apps',
310
+ apps: dailyApps.value,
311
+ },
312
+ {
313
+ key: 'management',
314
+ title: 'Management',
315
+ apps: managementApps.value,
316
+ },
317
+ {
318
+ key: 'finance',
319
+ title: 'Finance',
320
+ apps: financeApps.value,
321
+ },
322
+ {
323
+ key: 'people',
324
+ title: 'People',
325
+ apps: peopleApps.value,
326
+ },
327
+ {
328
+ key: 'legal',
329
+ title: 'Legal',
330
+ apps: legalApps.value,
331
+ },
332
+ ].filter((category) => category.apps.length > 0),
333
+ )
334
+
335
+ const filteredCategories = computed<IPortalCategory[]>(() => {
336
+ const keyword = search.value.trim().toLowerCase()
337
+
338
+ if (!keyword) {
339
+ return categories.value
340
+ }
341
+
342
+ return categories.value
343
+ .map((category) => ({
344
+ ...category,
345
+ apps: category.apps.filter((app) =>
346
+ app.label.toLowerCase().includes(keyword)
347
+ || app.description.toLowerCase().includes(keyword),
348
+ ),
349
+ }))
350
+ .filter((category) => category.apps.length > 0)
351
+ })
352
+
353
+ const gridColsClass = 'grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 xl:grid-cols-7'
669
354
  </script>