@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 +16 -0
- package/app/components/Format/Currency.vue +17 -0
- package/app/components/Format/Date.vue +24 -0
- package/app/components/Format/DateTime.vue +24 -0
- package/app/components/Format/Number.vue +17 -0
- package/app/components/Format/Percent.vue +38 -0
- package/app/components/Format/TimeFromNow.vue +38 -0
- package/app/components/Layout/Admin/index.vue +46 -0
- package/app/components/Layout/User/index.vue +43 -0
- package/app/components/NotificationList.vue +65 -0
- package/app/composables/useRequestOptions.ts +37 -1
- package/package.json +1 -1
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
|
}
|