@finema/finework-layer 0.2.74 → 0.2.76

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 (50) hide show
  1. package/.husky/pre-commit +1 -1
  2. package/.playground/.env.example +5 -0
  3. package/.playground/app/assets/css/main.css +6 -6
  4. package/.playground/app/pages/layout-admin/[id]/index.vue +145 -145
  5. package/.playground/app/pages/layout-admin/test/[id]/index.vue +286 -286
  6. package/.playground/app/pages/layout-admin.vue +285 -285
  7. package/.playground/app/pages/layout-user.vue +284 -0
  8. package/.playground/app/pages/submenu/layout-admin.vue +210 -210
  9. package/.playground/public/admin/clock-in-admin-logo.png +0 -0
  10. package/.playground/public/admin/clock-in-logo.png +0 -0
  11. package/.playground/public/admin/clock-in.png +0 -0
  12. package/.playground/public/admin/pmo-logo.png +0 -0
  13. package/.playground/public/admin/spider-web.png +0 -0
  14. package/.playground/public/admin/super-admin-logo.png +0 -0
  15. package/.playground/public/admin/super-admin.png +0 -0
  16. package/.playground/public/admin/timesheet-admin-logo.png +0 -0
  17. package/.playground/public/admin/timesheet-logo.png +0 -0
  18. package/.playground/public/admin/timesheet.png +0 -0
  19. package/CHANGELOG.md +374 -357
  20. package/app/app.config.ts +144 -144
  21. package/app/app.vue +10 -10
  22. package/app/assets/css/main.css +77 -77
  23. package/app/components/Button/ActionIcon.vue +29 -29
  24. package/app/components/Button/Back.vue +22 -22
  25. package/app/components/Format/Currency.vue +17 -0
  26. package/app/components/Format/Date.vue +24 -0
  27. package/app/components/Format/DateTime.vue +24 -0
  28. package/app/components/Format/Number.vue +17 -0
  29. package/app/components/Format/Percent.vue +38 -0
  30. package/app/components/Format/TimeFromNow.vue +38 -0
  31. package/app/components/InfoItemList.vue +196 -196
  32. package/app/components/Layout/Admin/Sidebar.vue +329 -329
  33. package/app/components/Layout/Admin/index.vue +224 -224
  34. package/app/components/Layout/Apps.vue +45 -45
  35. package/app/components/Layout/User/index.vue +102 -102
  36. package/app/components/Notifications/index.vue +147 -0
  37. package/app/components/StatusBox.vue +56 -56
  38. package/app/composables/useAuth.ts +207 -207
  39. package/app/composables/useNotification.ts +64 -0
  40. package/app/composables/useRequestOptions.ts +86 -69
  41. package/app/constants/routes.ts +86 -86
  42. package/app/error.vue +218 -218
  43. package/app/middleware/auth.ts +45 -45
  44. package/app/middleware/common.ts +12 -12
  45. package/app/middleware/guest.ts +7 -7
  46. package/app/middleware/permissions.ts +29 -29
  47. package/bun.lock +2758 -2758
  48. package/index.d.ts +16 -16
  49. package/nuxt.config.ts +41 -41
  50. package/package.json +38 -38
@@ -1,102 +1,102 @@
1
- <template>
2
- <div class="overflow-hidden pt-[80px]">
3
- <div class="flex min-h-[calc(100vh-80px)] flex-col">
4
- <nav
5
- class="fixed top-0 left-0 z-20 flex min-h-[80px] w-screen items-center justify-center bg-white"
6
- style="box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.04)"
7
- >
8
- <div class="mx-auto flex w-full px-4 md:px-8">
9
- <div class="flex items-center gap-4">
10
- <NuxtLink :to="routes.home.to">
11
- <img
12
- src="/logo-mini.png"
13
- alt="logo_mini"
14
- class="h-[27px] w-[172px] cursor-pointer"
15
- />
16
- </NuxtLink>
17
- <Apps />
18
- </div>
19
-
20
- <div class="flex flex-1 items-center justify-end gap-2">
21
- <DropdownMenu
22
- :items="userMenuItems"
23
- size="xl"
24
- :content="{
25
- align: 'end',
26
- side: 'bottom',
27
- }"
28
- :ui="{
29
- content: 'w-48',
30
- }"
31
- >
32
- <div class="relative flex cursor-pointer items-center gap-2">
33
- <div class="hidden flex-col justify-end text-right lg:flex">
34
- <p class="text-sm font-bold">
35
- {{
36
- auth.me.value?.display_name || auth.me.value?.full_name
37
- }}
38
- </p>
39
- <p class="text-muted text-xs">
40
- {{ auth.me.value?.email }}
41
- </p>
42
- </div>
43
- <Avatar
44
- class="border-muted size-[36px] border text-2xl"
45
- icon="ri:user-line"
46
- :src="auth.me.value?.avatar_url"
47
- />
48
- <Icon
49
- name="i-ph:caret-down-light"
50
- class="size-5"
51
- />
52
- </div>
53
- </DropdownMenu>
54
- </div>
55
- </div>
56
- </nav>
57
- <main class="mx-auto w-full max-w-7xl flex-1 px-4 pb-20 2xl:max-w-7/10">
58
- <div
59
- v-if="app.pageMeta.title"
60
- class="mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4 lg:flex-row lg:items-center"
61
- >
62
- <div class="flex flex-1 flex-col">
63
- <h1
64
- class="text-3xl font-bold wrap-break-word lg:max-w-2/3"
65
- :title="app.pageMeta.title"
66
- >
67
- {{ app.pageMeta.title }}
68
- <span id="page-title-extra" />
69
- </h1>
70
-
71
- <div id="page-subtitle" />
72
- </div>
73
- <div id="page-header" />
74
- </div>
75
- <slot />
76
- </main>
77
- <div id="page-footer" />
78
- </div>
79
- </div>
80
- </template>
81
-
82
- <script lang="ts" setup>
83
- import type { DropdownMenuItem } from '@nuxt/ui'
84
- import Apps from '../Apps.vue'
85
-
86
- const app = useApp()
87
- const auth = useAuth()
88
- const userMenuItems: DropdownMenuItem[] = [
89
- {
90
- label: 'View profile',
91
- icon: 'i-lucide-user',
92
- to: routes.account.profile.to,
93
- },
94
-
95
- {
96
- label: routes.logout.label,
97
- icon: 'i-lucide-log-out',
98
- to: routes.logout.to,
99
- external: true,
100
- },
101
- ]
102
- </script>
1
+ <template>
2
+ <div class="overflow-hidden pt-[80px]">
3
+ <div class="flex min-h-[calc(100vh-80px)] flex-col">
4
+ <nav
5
+ class="fixed top-0 left-0 z-20 flex min-h-[80px] w-screen items-center justify-center border-b border-gray-300 bg-white"
6
+ >
7
+ <div class="mx-auto flex w-full px-4 md:px-8">
8
+ <div class="flex items-center gap-4">
9
+ <NuxtLink :to="routes.home.to">
10
+ <img
11
+ src="/logo-mini.png"
12
+ alt="logo_mini"
13
+ class="h-[27px] w-[172px] cursor-pointer"
14
+ />
15
+ </NuxtLink>
16
+ <Apps />
17
+ </div>
18
+
19
+ <div class="flex flex-1 items-center justify-end gap-4">
20
+ <Notifications :ui="{ content: 'mt-[80px]' }" />
21
+ <DropdownMenu
22
+ :items="userMenuItems"
23
+ size="xl"
24
+ :content="{
25
+ align: 'end',
26
+ side: 'bottom',
27
+ }"
28
+ :ui="{
29
+ content: 'w-48',
30
+ }"
31
+ >
32
+ <div class="relative flex cursor-pointer items-center gap-2">
33
+ <div class="hidden flex-col justify-end text-right lg:flex">
34
+ <p class="text-sm font-bold">
35
+ {{
36
+ auth.me.value?.display_name || auth.me.value?.full_name
37
+ }}
38
+ </p>
39
+ <p class="text-muted text-xs">
40
+ {{ auth.me.value?.email }}
41
+ </p>
42
+ </div>
43
+ <Avatar
44
+ class="border-muted size-[36px] border text-2xl"
45
+ icon="ri:user-line"
46
+ :src="auth.me.value?.avatar_url"
47
+ />
48
+ <Icon
49
+ name="i-ph:caret-down-light"
50
+ class="size-5"
51
+ />
52
+ </div>
53
+ </DropdownMenu>
54
+ </div>
55
+ </div>
56
+ </nav>
57
+ <main class="mx-auto w-full max-w-7xl flex-1 px-4 pb-20 2xl:max-w-7/10">
58
+ <div
59
+ v-if="app.pageMeta.title"
60
+ class="mb-4 flex flex-col justify-between gap-1 md:mb-6 md:gap-4 lg:flex-row lg:items-center"
61
+ >
62
+ <div class="flex flex-1 flex-col">
63
+ <h1
64
+ class="text-3xl font-bold wrap-break-word lg:max-w-2/3"
65
+ :title="app.pageMeta.title"
66
+ >
67
+ {{ app.pageMeta.title }}
68
+ <span id="page-title-extra" />
69
+ </h1>
70
+
71
+ <div id="page-subtitle" />
72
+ </div>
73
+ <div id="page-header" />
74
+ </div>
75
+ <slot />
76
+ </main>
77
+ <div id="page-footer" />
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <script lang="ts" setup>
83
+ import type { DropdownMenuItem } from '@nuxt/ui'
84
+ import Apps from '../Apps.vue'
85
+
86
+ const app = useApp()
87
+ const auth = useAuth()
88
+ const userMenuItems: DropdownMenuItem[] = [
89
+ {
90
+ label: 'View profile',
91
+ icon: 'i-lucide-user',
92
+ to: routes.account.profile.to,
93
+ },
94
+
95
+ {
96
+ label: routes.logout.label,
97
+ icon: 'i-lucide-log-out',
98
+ to: routes.logout.to,
99
+ external: true,
100
+ },
101
+ ]
102
+ </script>
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <Slideover
3
+ :ui="{
4
+ ...ui || {},
5
+ header: 'border-0',
6
+ body: 'flex-1 overflow-y-auto p-2 sm:p-2',
7
+ title: 'text-xl font-bold',
8
+ }"
9
+ :modal="false"
10
+ title="Notifications"
11
+ >
12
+ <Chip
13
+ inset
14
+ color="success"
15
+ :show="Number(unreadCount.data?.count) > 0"
16
+ @click="markAllRead.run()"
17
+ >
18
+ <Icon
19
+ name="ph:bell"
20
+ class="size-6 cursor-pointer"
21
+ />
22
+ </Chip>
23
+
24
+ <template #body>
25
+ <Empty
26
+ v-if="notification.fetch.items.length === 0 && notification.fetch.status.isSuccess"
27
+ icon="ph:bell"
28
+ message="You have no notifications"
29
+ />
30
+ <Loader
31
+ v-else
32
+ :loading="notification.fetch.status.isLoading"
33
+ >
34
+ <div class="space-y-2 divide-y-1 divide-gray-200">
35
+ <div
36
+ v-for="item in notification.fetch.items"
37
+ :key="item.id"
38
+ >
39
+ <NuxtLink
40
+ :to="getLink(item)"
41
+ :target="getLink(item) === '#' ? '_self' : '_blank'"
42
+ :class="['mb-2 flex cursor-pointer items-start gap-2 rounded-lg p-4 hover:bg-gray-100']"
43
+ >
44
+ <Chip
45
+ v-if="getImage(item)"
46
+ position="bottom-right"
47
+ size="3xl"
48
+ inset
49
+ :color="getNotificationColor(item)"
50
+ >
51
+ <img
52
+
53
+ :src="getImage(item)!"
54
+ class="w-12"
55
+ />
56
+ </Chip>
57
+
58
+ <div class="flex-1">
59
+ <div class="flex items-start justify-between gap-2">
60
+ <p
61
+ class="line-clamp-1 text-sm font-medium text-gray-700"
62
+ :title="item.title"
63
+ >
64
+ {{ item.title }}
65
+ </p>
66
+ <FormatTimeFromNow
67
+ class="text-muted flex-1 text-right text-xs whitespace-nowrap"
68
+ :value="item.sent_at"
69
+ />
70
+ </div>
71
+
72
+ <p
73
+ class="text-sm text-neutral-500"
74
+ :title="item.message"
75
+ >
76
+ {{ item.message }}
77
+ </p>
78
+ </div>
79
+ </NuxtLink>
80
+ </div>
81
+ </div>
82
+ </Loader>
83
+ </template>
84
+ </Slideover>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ defineProps<{
89
+ ui?: {
90
+ content?: string
91
+ }
92
+ }>()
93
+
94
+ const notification = useNotificationLoader()
95
+ const unreadCount = useNotificationUnreadCount()
96
+ const markAllRead = useNotificationMarkAllRead()
97
+
98
+ onMounted(() => {
99
+ notification.fetchPage()
100
+ unreadCount.run()
101
+ })
102
+
103
+ const getLink = (item: INotificationItem) => {
104
+ if (item.data?.url) {
105
+ return item.data.url
106
+ }
107
+
108
+ if (item.source_type === 'PMO_PROJECT') {
109
+ return `/pmo/projects/${item.source_id}`
110
+ }
111
+
112
+ return '#'
113
+ }
114
+
115
+ const getImage = (item: INotificationItem): string | null => {
116
+ if (item.source_type?.startsWith('PMO')) {
117
+ return `/admin/pmo-logo.png`
118
+ } else if (item.source_type?.startsWith('TIMESHEET')) {
119
+ return `/admin/timesheet-logo.png`
120
+ } else if (item.source_type?.startsWith('CLOCKIN')) {
121
+ return `/admin/clockin-logo.png`
122
+ } else if (item.source_type?.startsWith('SETTING')) {
123
+ return `/admin/setting-logo.png`
124
+ }
125
+
126
+ return null
127
+ }
128
+
129
+ const getNotificationColor = (item: INotificationItem): string => {
130
+ switch (item.type) {
131
+ case NotificationType.INFO:
132
+ return 'info'
133
+ case NotificationType.SUCCESS:
134
+ return 'success'
135
+ case NotificationType.WARNING:
136
+ return 'warning'
137
+ case NotificationType.ERROR:
138
+ return 'error'
139
+ default:
140
+ return 'info'
141
+ }
142
+ }
143
+
144
+ useWatchTrue(() => markAllRead.status.value.isSuccess, () => {
145
+ unreadCount.run()
146
+ })
147
+ </script>
@@ -1,56 +1,56 @@
1
- <template>
2
- <slot v-if="skip" />
3
- <template v-else>
4
- <Card v-if="status.isLoading">
5
- <div class="flex items-center justify-center py-12">
6
- <Loader />
7
- </div>
8
- </Card>
9
-
10
- <!-- Not Found State -->
11
- <Card v-else-if="status.isSuccess && !data || status.isError && status.errorData?.code === 'NOT_FOUND'">
12
- <div class="py-12 text-center">
13
- <Icon
14
- name="lucide:search-x"
15
- class="mx-auto mb-4 text-gray-400"
16
- size="48"
17
- />
18
- <h3 class="mb-2 text-lg font-medium text-gray-900">
19
- ไม่พบข้อมูล
20
- </h3>
21
- <p class="text-gray-600">
22
- ไม่พบข้อมูลที่คุณค้นหา กรุณาลองใหม่อีกครั้ง
23
- </p>
24
- </div>
25
- </Card>
26
- <Card v-else-if="status.isError">
27
- <div class="py-12 text-center">
28
- <Icon
29
- name="lucide:alert-circle"
30
- class="mx-auto mb-4 text-red-500"
31
- size="48"
32
- />
33
- <h3 class="mb-2 text-lg font-medium text-gray-900">
34
- เกิดข้อผิดพลาด
35
- </h3>
36
- <p class="text-gray-600">
37
- ไม่สามารถโหลดข้อมูลได้ กรุณาลองใหม่อีกครั้ง
38
- </p>
39
- </div>
40
- </Card>
41
- <slot
42
- v-else-if="data"
43
- :data="data"
44
- />
45
- </template>
46
- </template>
47
-
48
- <script lang="ts" setup generic="T">
49
- import type { IStatus } from '#core/types/lib'
50
-
51
- defineProps<{
52
- status: IStatus
53
- data: T
54
- skip?: boolean
55
- }>()
56
- </script>
1
+ <template>
2
+ <slot v-if="skip" />
3
+ <template v-else>
4
+ <Card v-if="status.isLoading">
5
+ <div class="flex items-center justify-center py-12">
6
+ <Loader />
7
+ </div>
8
+ </Card>
9
+
10
+ <!-- Not Found State -->
11
+ <Card v-else-if="status.isSuccess && !data || status.isError && status.errorData?.code === 'NOT_FOUND'">
12
+ <div class="py-12 text-center">
13
+ <Icon
14
+ name="lucide:search-x"
15
+ class="mx-auto mb-4 text-gray-400"
16
+ size="48"
17
+ />
18
+ <h3 class="mb-2 text-lg font-medium text-gray-900">
19
+ ไม่พบข้อมูล
20
+ </h3>
21
+ <p class="text-gray-600">
22
+ ไม่พบข้อมูลที่คุณค้นหา กรุณาลองใหม่อีกครั้ง
23
+ </p>
24
+ </div>
25
+ </Card>
26
+ <Card v-else-if="status.isError">
27
+ <div class="py-12 text-center">
28
+ <Icon
29
+ name="lucide:alert-circle"
30
+ class="mx-auto mb-4 text-red-500"
31
+ size="48"
32
+ />
33
+ <h3 class="mb-2 text-lg font-medium text-gray-900">
34
+ เกิดข้อผิดพลาด
35
+ </h3>
36
+ <p class="text-gray-600">
37
+ ไม่สามารถโหลดข้อมูลได้ กรุณาลองใหม่อีกครั้ง
38
+ </p>
39
+ </div>
40
+ </Card>
41
+ <slot
42
+ v-else-if="data"
43
+ :data="data"
44
+ />
45
+ </template>
46
+ </template>
47
+
48
+ <script lang="ts" setup generic="T">
49
+ import type { IStatus } from '#core/types/lib'
50
+
51
+ defineProps<{
52
+ status: IStatus
53
+ data: T
54
+ skip?: boolean
55
+ }>()
56
+ </script>