@finema/finework-layer 0.2.77 → 0.2.79

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 (41) hide show
  1. package/.husky/pre-commit +1 -1
  2. package/.playground/app/assets/css/main.css +6 -6
  3. package/.playground/app/pages/layout-admin/[id]/index.vue +145 -145
  4. package/.playground/app/pages/layout-admin/test/[id]/index.vue +286 -286
  5. package/.playground/app/pages/layout-admin.vue +283 -285
  6. package/.playground/app/pages/layout-user.vue +284 -284
  7. package/.playground/app/pages/submenu/layout-admin.vue +210 -210
  8. package/.vscode/settings.json +5 -1
  9. package/CHANGELOG.md +382 -374
  10. package/app/app.config.ts +144 -144
  11. package/app/app.vue +10 -10
  12. package/app/assets/css/main.css +77 -77
  13. package/app/components/Button/ActionIcon.vue +29 -29
  14. package/app/components/Button/Back.vue +22 -22
  15. package/app/components/Format/Currency.vue +17 -17
  16. package/app/components/Format/Date.vue +24 -24
  17. package/app/components/Format/DateTime.vue +24 -24
  18. package/app/components/Format/Number.vue +17 -17
  19. package/app/components/Format/Percent.vue +38 -38
  20. package/app/components/Format/TimeFromNow.vue +38 -38
  21. package/app/components/InfoItemList.vue +196 -196
  22. package/app/components/Layout/Admin/Sidebar.vue +343 -329
  23. package/app/components/Layout/Admin/index.vue +240 -224
  24. package/app/components/Layout/Apps.vue +45 -45
  25. package/app/components/Layout/User/index.vue +102 -102
  26. package/app/components/Notifications/index.vue +162 -162
  27. package/app/components/StatusBox.vue +56 -56
  28. package/app/composables/useAuth.ts +207 -207
  29. package/app/composables/useNotification.ts +76 -76
  30. package/app/composables/useRequestOptions.ts +86 -86
  31. package/app/constants/routes.ts +86 -86
  32. package/app/error.vue +218 -218
  33. package/app/middleware/auth.ts +45 -45
  34. package/app/middleware/common.ts +12 -12
  35. package/app/middleware/guest.ts +7 -7
  36. package/app/middleware/permissions.ts +29 -29
  37. package/bun.lock +2758 -2758
  38. package/eslint.config.js +206 -2
  39. package/index.d.ts +16 -16
  40. package/nuxt.config.ts +41 -41
  41. 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 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>
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>
@@ -1,162 +1,162 @@
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
- @update:open="onOpenTrigger"
12
- >
13
- <Chip
14
- inset
15
- color="success"
16
- :show="Number(unreadCount.data?.count) > 0"
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="getMessage(item)"
75
- >
76
- {{ getMessage(item) }}
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
- const route = useRoute()
98
-
99
- onMounted(() => {
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
- const getMessage = (item: INotificationItem): string => {
145
- return item.message
146
- }
147
-
148
- useWatchTrue(() => markAllRead.status.value.isSuccess, () => {
149
- unreadCount.run()
150
- })
151
-
152
- useWatchChange(() => route.path, () => {
153
- unreadCount.run()
154
- })
155
-
156
- const onOpenTrigger = (isOpen: boolean) => {
157
- if (isOpen) {
158
- markAllRead.run()
159
- notification.fetchPage()
160
- }
161
- }
162
- </script>
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
+ @update:open="onOpenTrigger"
12
+ >
13
+ <Chip
14
+ inset
15
+ color="success"
16
+ :show="Number(unreadCount.data?.count) > 0"
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="getMessage(item)"
75
+ >
76
+ {{ getMessage(item) }}
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
+ const route = useRoute()
98
+
99
+ onMounted(() => {
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
+ const getMessage = (item: INotificationItem): string => {
145
+ return item.message
146
+ }
147
+
148
+ useWatchTrue(() => markAllRead.status.value.isSuccess, () => {
149
+ unreadCount.run()
150
+ })
151
+
152
+ useWatchChange(() => route.path, () => {
153
+ unreadCount.run()
154
+ })
155
+
156
+ const onOpenTrigger = (isOpen: boolean) => {
157
+ if (isOpen) {
158
+ markAllRead.run()
159
+ notification.fetchPage()
160
+ }
161
+ }
162
+ </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>