@finema/finework-layer 0.2.73 → 0.2.75

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.75](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.2.74...0.2.75) (2025-12-03)
4
+
5
+ ### Features
6
+
7
+ * add formatting components for Currency, Date, DateTime, Number, Percent, and TimeFromNow ([e11525f](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/e11525fe616bbd2bf25defea2f3b9aae3f06e102))
8
+
9
+ ### Bug Fixes
10
+
11
+ * notification ([af8af78](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/af8af785855cccb995e5274ef5063f939b4226b7))
12
+
13
+ ## [0.2.74](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.2.73...0.2.74) (2025-12-01)
14
+
15
+ ### Features
16
+
17
+ * file public ([61086f7](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/61086f782eec2c8bef272c3ba44f854e00d002ee))
18
+
3
19
  ## [0.2.73](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.2.72...0.2.73) (2025-12-01)
4
20
 
5
21
  ### Bug Fixes
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <component :is="props.as || 'span'">
3
+ {{ getValue }} {{ unit ? unit : '' }}
4
+ </component>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ const props = defineProps<{
9
+ value: number | string | null | undefined
10
+ as?: string
11
+ unit?: string
12
+ }>()
13
+
14
+ const getValue = computed(() => {
15
+ return NumberHelper.toCurrency(props.value)
16
+ })
17
+ </script>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <component
3
+ :is="props.as || 'span'"
4
+ :title="getValue"
5
+ >
6
+ {{ getValue ?? '–' }}
7
+ </component>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ const props = defineProps<{
12
+ value: string | Date
13
+ as?: string
14
+ }>()
15
+
16
+ const config = useAppConfig()
17
+ const localeInject = inject('locale')
18
+
19
+ const getValue = computed(() => {
20
+ return (localeInject ?? config.core.locale) === 'th'
21
+ ? TimeHelper.displayDateThai(props.value)
22
+ : TimeHelper.displayDate(props.value)
23
+ })
24
+ </script>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <component
3
+ :is="props.as || 'span'"
4
+ :title="getValue"
5
+ >
6
+ {{ getValue }}
7
+ </component>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ const props = defineProps<{
12
+ value: string | Date
13
+ as?: string
14
+ }>()
15
+
16
+ const config = useAppConfig()
17
+ const localeInject = inject('locale')
18
+
19
+ const getValue = computed(() => {
20
+ return (localeInject ?? config.core.locale) === 'th'
21
+ ? `${TimeHelper.displayDateThai(props.value)} น.`
22
+ : TimeHelper.displayDateTime(props.value)
23
+ })
24
+ </script>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <component :is="props.as || 'span'">
3
+ {{ getValue }} {{ unit ? unit : '' }}
4
+ </component>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ const props = defineProps<{
9
+ value: number | string | null | undefined
10
+ as?: string
11
+ unit?: string
12
+ }>()
13
+
14
+ const getValue = computed(() => {
15
+ return NumberHelper.withFixed(props.value || 0)
16
+ })
17
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <component :is="props.as || 'span'">
3
+ {{ getValue }}{{ unit || '%' }}
4
+ </component>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ const props = defineProps<{
9
+ value: number | string | null | undefined
10
+ total: number | string | null | undefined
11
+ as?: string
12
+ unit?: string
13
+ remaining?: boolean
14
+ }>()
15
+
16
+ const getValue = computed(() => {
17
+ const total = Number(props.total)
18
+ const value = Number(props.value)
19
+
20
+ if (Number.isNaN(total) || total === 0) return '--'
21
+
22
+ const used = Number.isNaN(value) ? 0 : value
23
+
24
+ if (props.remaining) {
25
+ return NumberHelper.withFixed(((total - used) / total) * 100)
26
+ }
27
+
28
+ let percent
29
+
30
+ if (props.remaining) {
31
+ percent = ((total - used) / total) * 100
32
+ } else {
33
+ percent = (used / total) * 100
34
+ }
35
+
36
+ return NumberHelper.withFixed(Math.abs(percent))
37
+ })
38
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <component
3
+ :is="props.as || 'span'"
4
+ :title="TimeHelper.displayDateTime(props.value)"
5
+ >
6
+ {{ getValue }}
7
+ </component>
8
+ </template>
9
+
10
+ <script lang="ts" setup>
11
+ import { formatDistanceToNow, differenceInDays } from 'date-fns'
12
+ import * as locales from 'date-fns/locale'
13
+
14
+ const props = defineProps<{
15
+ value: string | Date
16
+ as?: string
17
+ }>()
18
+
19
+ const config = useAppConfig()
20
+ const localeInject = inject('locale')
21
+
22
+ const getValue = computed(() => {
23
+ const locale = (localeInject ?? config.core.locale) === 'th'
24
+ ? locales.th
25
+ : locales.enUS
26
+
27
+ const daysDiff = differenceInDays(new Date(), props.value)
28
+
29
+ if (daysDiff <= 6) {
30
+ return formatDistanceToNow(props.value, {
31
+ addSuffix: true,
32
+ locale,
33
+ })
34
+ }
35
+
36
+ return TimeHelper.displayDateTime(props.value)
37
+ })
38
+ </script>
@@ -80,6 +80,28 @@
80
80
  </div>
81
81
 
82
82
  <div class="flex items-center justify-center gap-4">
83
+ <Slideover
84
+ :ui="{
85
+ content: 'mt-18',
86
+ header: 'border-0',
87
+ }"
88
+ :modal="false"
89
+ title="Notifications"
90
+ >
91
+ <Chip
92
+ inset
93
+ color="success"
94
+ >
95
+ <Icon
96
+ name="ph:bell"
97
+ class="size-6 cursor-pointer"
98
+ />
99
+ </Chip>
100
+
101
+ <template #body>
102
+ <NotificationList :items="notifications" />
103
+ </template>
104
+ </Slideover>
83
105
  <DropdownMenu
84
106
  size="xl"
85
107
  :items="userMenuItems"
@@ -189,6 +211,30 @@ const isCollapsed = useCookie<boolean>('app.admin.sidebar.isCollapsed', {
189
211
  path: '/',
190
212
  })
191
213
 
214
+ const notifications = [
215
+ {
216
+ id: 1,
217
+ userName: 'Phoenix Baker',
218
+ avatarSrc: '/mock/user-1.png',
219
+ date: '2025-01-01T08:00:00Z',
220
+ type: 'file',
221
+ description: 'Added a file to Marketing site redesign',
222
+ fileName: 'Tech requirements.pdf',
223
+ fileSize: '720 KB',
224
+ fileIcon: 'ph:file-pdf',
225
+ isRead: true,
226
+ },
227
+ {
228
+ id: 2,
229
+ userName: 'Lana Steiner',
230
+ avatarSrc: '/mock/user-2.png',
231
+ date: '2025-01-01T08:00:00Z',
232
+ type: 'invite',
233
+ description: 'Invited Alisa Hester to the team',
234
+ isRead: false,
235
+ },
236
+ ]
237
+
192
238
  const userMenuItems: DropdownMenuItem[] = [
193
239
  {
194
240
  label: 'View profile',
@@ -18,6 +18,28 @@
18
18
  </div>
19
19
 
20
20
  <div class="flex flex-1 items-center justify-end gap-2">
21
+ <Slideover
22
+ :ui="{
23
+ content: 'mt-18',
24
+ header: 'border-0',
25
+ }"
26
+ :modal="false"
27
+ title="Notifications"
28
+ >
29
+ <Chip
30
+ inset
31
+ color="success"
32
+ >
33
+ <Icon
34
+ name="ph:bell"
35
+ class="size-6 cursor-pointer"
36
+ />
37
+ </Chip>
38
+
39
+ <template #body>
40
+ <NotificationList :items="notifications" />
41
+ </template>
42
+ </Slideover>
21
43
  <DropdownMenu
22
44
  :items="userMenuItems"
23
45
  size="xl"
@@ -99,4 +121,25 @@ const userMenuItems: DropdownMenuItem[] = [
99
121
  external: true,
100
122
  },
101
123
  ]
124
+
125
+ const notifications = [
126
+ {
127
+ id: 1,
128
+ userName: 'Phoenix Baker',
129
+ avatarSrc: '/mock/user-1.png',
130
+ date: '2025-01-01T08:00:00Z',
131
+ type: 'file',
132
+ description: 'Added a file to Marketing site redesign',
133
+ isRead: true,
134
+ },
135
+ {
136
+ id: 2,
137
+ userName: 'Lana Steiner',
138
+ avatarSrc: '/mock/user-2.png',
139
+ date: '2025-01-01T08:00:00Z',
140
+ type: 'invite',
141
+ description: 'Invited Alisa Hester to the team',
142
+ isRead: false,
143
+ },
144
+ ]
102
145
  </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <div class="space-y-6">
3
+ <div
4
+ v-for="(item, index) in items"
5
+ :key="item.id"
6
+ >
7
+ <div class="flex gap-3">
8
+ <Avatar
9
+ :src="item.avatarSrc || item.userName"
10
+ :alt="item.userName"
11
+ class="size-12 rounded-full object-cover"
12
+ />
13
+
14
+ <div class="flex-1">
15
+ <div class="flex items-center gap-2">
16
+ <p class="font-medium">
17
+ {{ item.userName }}
18
+ </p>
19
+ <FormatTimeFromNow
20
+ class="text-muted text-xs"
21
+ :value="new Date(new Date().getTime() - 5 * 60 * 1000)"
22
+ />
23
+ </div>
24
+
25
+ <p class="text-sm text-neutral-700">
26
+ {{ item.description }}
27
+ </p>
28
+ </div>
29
+
30
+ <span
31
+ v-if="item.isRead"
32
+ class="bg-success mt-1 size-3 self-start rounded-full"
33
+ />
34
+ </div>
35
+ <Separator
36
+ v-if="index < items.length - 1"
37
+ class="my-4"
38
+ />
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ export type NotificationType = 'file' | 'invite'
45
+
46
+ export interface NotificationItem {
47
+ id: number
48
+ userName: string
49
+ avatarSrc: string
50
+ date: string
51
+ type: NotificationType
52
+ description: string
53
+
54
+ // file type
55
+ fileName?: string
56
+ fileSize?: string
57
+ fileIcon?: string
58
+
59
+ isRead: boolean
60
+ }
61
+
62
+ const props = defineProps<{
63
+ items: NotificationItem[]
64
+ }>()
65
+ </script>
@@ -1,5 +1,14 @@
1
1
  import type { AxiosRequestConfig } from 'axios'
2
2
 
3
+ export enum FileAppType {
4
+ COMMON = 'COMMON',
5
+ PMO = 'PMO',
6
+ HR = 'HR',
7
+ FINANCE = 'FINANCE',
8
+ QUOTATION = 'QUOTATION',
9
+ TODO = 'TODO',
10
+ }
11
+
3
12
  export const useRequestOptions = () => {
4
13
  const config = useRuntimeConfig()
5
14
 
@@ -30,7 +39,25 @@ export const useRequestOptions = () => {
30
39
  }
31
40
  }
32
41
 
33
- const file = (): Omit<AxiosRequestConfig, 'baseURL'> & {
42
+ const file = (fileAppType: FileAppType = FileAppType.COMMON): Omit<AxiosRequestConfig, 'baseURL'> & {
43
+ baseURL: string
44
+ } => {
45
+ return {
46
+ baseURL: config.public.baseAPI + '/uploads',
47
+ headers: {
48
+ Authorization: `Bearer ${useAuth().token.value}`,
49
+ },
50
+ transformRequest: (data) => {
51
+ if (data instanceof FormData) {
52
+ data.append('app', fileAppType)
53
+ }
54
+
55
+ return data
56
+ },
57
+ }
58
+ }
59
+
60
+ const filePublic = (fileAppType: FileAppType = FileAppType.COMMON): Omit<AxiosRequestConfig, 'baseURL'> & {
34
61
  baseURL: string
35
62
  } => {
36
63
  return {
@@ -38,6 +65,14 @@ export const useRequestOptions = () => {
38
65
  headers: {
39
66
  Authorization: `Bearer ${useAuth().token.value}`,
40
67
  },
68
+ transformRequest: (data) => {
69
+ if (data instanceof FormData) {
70
+ data.append('is_public', 'true')
71
+ data.append('app', fileAppType)
72
+ }
73
+
74
+ return data
75
+ },
41
76
  }
42
77
  }
43
78
 
@@ -46,5 +81,6 @@ export const useRequestOptions = () => {
46
81
  mock,
47
82
  auth,
48
83
  file,
84
+ filePublic,
49
85
  }
50
86
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@finema/finework-layer",
3
3
  "type": "module",
4
- "version": "0.2.73",
4
+ "version": "0.2.75",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground -o",