@finema/finework-layer 0.2.139 → 0.2.141
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 +12 -0
- package/app/components/Format/RelativeTimeFromNow.vue +106 -0
- package/app/components/Format/TimeFromNow.vue +16 -56
- package/app/components/Notifications/index.vue +89 -43
- package/package.json +1 -1
- package/public/admin/assets.png +0 -0
- package/public/admin/clock-in-admin-logo.png +0 -0
- package/public/admin/clock-in-logo.png +0 -0
- package/public/admin/clock-in.png +0 -0
- package/public/admin/contract.png +0 -0
- package/public/admin/cost-sheet.png +0 -0
- package/public/admin/efactoring.png +0 -0
- package/public/admin/employee.png +0 -0
- package/public/admin/evaluation.png +0 -0
- package/public/admin/guarantee.png +0 -0
- package/public/admin/newsletter.png +0 -0
- package/public/admin/pmo-logo.png +0 -0
- package/public/admin/purchase.png +0 -0
- package/public/admin/quotation.png +0 -0
- package/public/admin/recruit.png +0 -0
- package/public/admin/request-admin.png +0 -0
- package/public/admin/request.png +0 -0
- package/public/admin/spider-web.png +0 -0
- package/public/admin/super-admin-logo.png +0 -0
- package/public/admin/super-admin.png +0 -0
- package/public/admin/timesheet-admin-logo.png +0 -0
- package/public/admin/timesheet-logo.png +0 -0
- package/public/admin/timesheet.png +0 -0
- package/public/admin/todo.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.141](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.2.140...0.2.141) (2026-02-19)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
* improve noti ([cb22978](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/cb22978810ab73bdabb21bf0f49c35aaa19ed282))
|
|
8
|
+
|
|
9
|
+
## [0.2.140](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.2.139...0.2.140) (2026-02-17)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* refactor TimeFromNow component and add RelativeTimeFromNow component for enhanced date formatting ([b7c9d76](https://gitlab.finema.co/finema/finework/finework-frontend-layer/commit/b7c9d76dfdc8f5ec901daa582fec4e16db1ccd20))
|
|
14
|
+
|
|
3
15
|
## [0.2.139](https://gitlab.finema.co/finema/finework/finework-frontend-layer/compare/0.2.138...0.2.139) (2026-02-16)
|
|
4
16
|
|
|
5
17
|
### Bug Fixes
|
|
@@ -0,0 +1,106 @@
|
|
|
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 { differenceInDays, differenceInHours, differenceInMinutes, differenceInMonths, differenceInWeeks, startOfDay, addDays } from 'date-fns'
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
value: string | Date
|
|
15
|
+
as?: string
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const config = useAppConfig()
|
|
19
|
+
const localeInject = inject('locale')
|
|
20
|
+
|
|
21
|
+
const isThaiLocale = computed(() => {
|
|
22
|
+
return (localeInject ?? config.core.locale) === 'th'
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const getValue = computed(() => {
|
|
26
|
+
const targetDate = new Date(props.value)
|
|
27
|
+
const now = new Date()
|
|
28
|
+
|
|
29
|
+
const nowDate = startOfDay(now)
|
|
30
|
+
const dueDateOnly = startOfDay(targetDate)
|
|
31
|
+
|
|
32
|
+
if (nowDate > dueDateOnly) {
|
|
33
|
+
const dueDateEnd = addDays(dueDateOnly, 1)
|
|
34
|
+
|
|
35
|
+
const months = differenceInMonths(now, dueDateEnd)
|
|
36
|
+
|
|
37
|
+
if (months > 0) {
|
|
38
|
+
if (isThaiLocale.value) {
|
|
39
|
+
return months === 1 ? '1 เดือนที่แล้ว' : `${months} เดือนที่แล้ว`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return months === 1 ? '1 month ago' : `${months} months ago`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const weeks = differenceInWeeks(now, dueDateEnd)
|
|
46
|
+
|
|
47
|
+
if (weeks > 0) {
|
|
48
|
+
if (isThaiLocale.value) {
|
|
49
|
+
return weeks === 1 ? '1 สัปดาห์ที่แล้ว' : `${weeks} สัปดาห์ที่แล้ว`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const days = differenceInDays(now, dueDateEnd)
|
|
56
|
+
|
|
57
|
+
if (days > 0) {
|
|
58
|
+
if (isThaiLocale.value) {
|
|
59
|
+
return days === 1 ? '1 วันที่แล้ว' : `${days} วันที่แล้ว`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return days === 1 ? '1 day ago' : `${days} days ago`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hours = differenceInHours(now, dueDateEnd)
|
|
66
|
+
|
|
67
|
+
if (hours > 0) {
|
|
68
|
+
if (isThaiLocale.value) {
|
|
69
|
+
return hours === 1 ? '1 ชม.ที่แล้ว' : `${hours} ชม.ที่แล้ว`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return hours === 1 ? '1hr ago' : `${hours}hr ago`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const minutes = differenceInMinutes(now, dueDateEnd)
|
|
76
|
+
|
|
77
|
+
if (minutes > 0) {
|
|
78
|
+
if (isThaiLocale.value) {
|
|
79
|
+
return minutes === 1 ? '1 นาทีที่แล้ว' : `${minutes} นาทีที่แล้ว`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return minutes === 1 ? '1m ago' : `${minutes}m ago`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return isThaiLocale.value ? 'เมื่อสักครู่' : 'just now'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (nowDate.getTime() === dueDateOnly.getTime()) {
|
|
89
|
+
return isThaiLocale.value ? 'วันนี้' : 'Today'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tomorrow = addDays(nowDate, 1)
|
|
93
|
+
|
|
94
|
+
if (tomorrow.getTime() === dueDateOnly.getTime()) {
|
|
95
|
+
return isThaiLocale.value ? 'พรุ่งนี้' : 'Tomorrow'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const days = differenceInDays(dueDateOnly, nowDate)
|
|
99
|
+
|
|
100
|
+
if (isThaiLocale.value) {
|
|
101
|
+
return `อีก ${days} วัน`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `in ${days}d`
|
|
105
|
+
})
|
|
106
|
+
</script>
|
|
@@ -8,72 +8,32 @@
|
|
|
8
8
|
</template>
|
|
9
9
|
|
|
10
10
|
<script lang="ts" setup>
|
|
11
|
-
import {
|
|
11
|
+
import { formatDistanceToNow, differenceInDays } from 'date-fns'
|
|
12
|
+
import * as locales from 'date-fns/locale'
|
|
12
13
|
|
|
13
14
|
const props = defineProps<{
|
|
14
15
|
value: string | Date
|
|
15
16
|
as?: string
|
|
16
17
|
}>()
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const now = new Date()
|
|
21
|
-
|
|
22
|
-
// Compare only the date part
|
|
23
|
-
const nowDate = startOfDay(now)
|
|
24
|
-
const dueDateOnly = startOfDay(targetDate)
|
|
25
|
-
|
|
26
|
-
if (nowDate > dueDateOnly) {
|
|
27
|
-
const dueDateEnd = addDays(dueDateOnly, 1)
|
|
28
|
-
|
|
29
|
-
const months = differenceInMonths(now, dueDateEnd)
|
|
30
|
-
|
|
31
|
-
if (months > 0) {
|
|
32
|
-
return months === 1 ? '1 month ago' : `${months} months ago`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const weeks = differenceInWeeks(now, dueDateEnd)
|
|
36
|
-
|
|
37
|
-
if (weeks > 0) {
|
|
38
|
-
return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const days = differenceInDays(now, dueDateEnd)
|
|
42
|
-
|
|
43
|
-
if (days > 0) {
|
|
44
|
-
return days === 1 ? '1 day ago' : `${days} days ago`
|
|
45
|
-
}
|
|
19
|
+
const config = useAppConfig()
|
|
20
|
+
const localeInject = inject('locale')
|
|
46
21
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const minutes = differenceInMinutes(now, dueDateEnd)
|
|
54
|
-
|
|
55
|
-
if (minutes > 0) {
|
|
56
|
-
return minutes === 1 ? '1m ago' : `${minutes}m ago`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return 'just now'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Today
|
|
63
|
-
if (nowDate.getTime() === dueDateOnly.getTime()) {
|
|
64
|
-
return 'Today'
|
|
65
|
-
}
|
|
22
|
+
const getValue = computed(() => {
|
|
23
|
+
const locale = (localeInject ?? config.core.locale) === 'th'
|
|
24
|
+
? locales.th
|
|
25
|
+
: locales.enUS
|
|
66
26
|
|
|
67
|
-
|
|
68
|
-
const
|
|
27
|
+
const targetDate = new Date(props.value)
|
|
28
|
+
const daysDiff = differenceInDays(new Date(), targetDate)
|
|
69
29
|
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
30
|
+
if (daysDiff <= 6) {
|
|
31
|
+
return formatDistanceToNow(targetDate, {
|
|
32
|
+
addSuffix: true,
|
|
33
|
+
locale,
|
|
34
|
+
})
|
|
72
35
|
}
|
|
73
36
|
|
|
74
|
-
|
|
75
|
-
const days = differenceInDays(dueDateOnly, nowDate)
|
|
76
|
-
|
|
77
|
-
return `in ${days}d`
|
|
37
|
+
return TimeHelper.displayDateTime(props.value)
|
|
78
38
|
})
|
|
79
39
|
</script>
|
|
@@ -39,42 +39,36 @@
|
|
|
39
39
|
<NuxtLink
|
|
40
40
|
:to="getLink(item)"
|
|
41
41
|
:target="getLink(item) === '#' ? '_self' : '_blank'"
|
|
42
|
-
:class="['
|
|
42
|
+
:class="['flex cursor-pointer items-start gap-2 rounded-lg p-4 hover:bg-gray-100']"
|
|
43
43
|
>
|
|
44
|
-
<Chip
|
|
45
|
-
v-if="getImage(item)"
|
|
44
|
+
<!-- <Chip
|
|
46
45
|
position="bottom-right"
|
|
47
46
|
size="3xl"
|
|
48
47
|
inset
|
|
49
48
|
:color="getNotificationColor(item)"
|
|
50
|
-
>
|
|
49
|
+
> -->
|
|
50
|
+
<div class="relative">
|
|
51
|
+
<Avatar
|
|
52
|
+
class="min-h-12 min-w-12"
|
|
53
|
+
:src="item.actor.avatar_url"
|
|
54
|
+
:alt="item.actor.display_name"
|
|
55
|
+
/>
|
|
51
56
|
<img
|
|
52
|
-
|
|
53
57
|
:src="getImage(item)!"
|
|
54
|
-
class="w-
|
|
58
|
+
class="absolute -right-1 -bottom-1 w-5 rounded-full bg-white p-0.25"
|
|
55
59
|
/>
|
|
56
|
-
</
|
|
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>
|
|
60
|
+
</div>
|
|
61
|
+
<!-- </Chip> -->
|
|
71
62
|
|
|
63
|
+
<div class="flex flex-1 flex-col gap-0">
|
|
72
64
|
<p
|
|
73
|
-
class="text-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
class="text-gray line-clamp-2 text-sm"
|
|
66
|
+
v-html="getMessage(item)"
|
|
67
|
+
/>
|
|
68
|
+
<FormatTimeFromNow
|
|
69
|
+
class="text-xs font-medium whitespace-nowrap text-gray-600"
|
|
70
|
+
:value="item.sent_at"
|
|
71
|
+
/>
|
|
78
72
|
</div>
|
|
79
73
|
</NuxtLink>
|
|
80
74
|
</div>
|
|
@@ -101,24 +95,41 @@ onMounted(() => {
|
|
|
101
95
|
})
|
|
102
96
|
|
|
103
97
|
const getLink = (item: INotificationItem) => {
|
|
104
|
-
if (item.data?.url)
|
|
105
|
-
return item.data.url
|
|
106
|
-
}
|
|
98
|
+
if (item.data?.url) return item.data.url
|
|
107
99
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
100
|
+
switch (item.source_type) {
|
|
101
|
+
case 'PMO_PROJECT':
|
|
102
|
+
return `/pmo/projects/${item.source_id}`
|
|
103
|
+
|
|
104
|
+
case 'NEWSLETTER':
|
|
105
|
+
return `/announcement/${item.source_id}`
|
|
106
|
+
|
|
107
|
+
case 'TODO': {
|
|
108
|
+
const boardSlug = item.data?.todo_board_slug
|
|
109
|
+
const cardSlug = item.data?.todo_slug
|
|
110
|
+
|
|
111
|
+
switch (item.action_type) {
|
|
112
|
+
case 'TODO_ASSIGN_CARD':
|
|
113
|
+
case 'TODO_COMMENT_ADD':
|
|
114
|
+
case 'TODO_DUE_DATE_CHANGE':
|
|
115
|
+
case 'TODO_STATUS_CHANGE':
|
|
116
|
+
return `/todo/boards/${boardSlug}/cards/${cardSlug}`
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
case 'TODO_REMOVE_CARD':
|
|
119
|
+
case 'TODO_ASSIGN_BOARD':
|
|
120
|
+
return `/todo/boards/${boardSlug}`
|
|
121
|
+
|
|
122
|
+
case 'TODO_REMOVE_BOARD':
|
|
123
|
+
return `/todo/cards`
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
return '#'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
return '#'
|
|
132
|
+
}
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
const getImage = (item: INotificationItem): string | null => {
|
|
@@ -159,7 +170,42 @@ const getNotificationColor = (item: INotificationItem): string => {
|
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
const getMessage = (item: INotificationItem): string => {
|
|
162
|
-
|
|
173
|
+
let message = item.message
|
|
174
|
+
|
|
175
|
+
// ตรวจหาวันที่ในรูปแบบ yyyy-mm-dd และแปลงเป็น text
|
|
176
|
+
const datePattern = /\b(\d{4})-(\d{2})-(\d{2})\b/g
|
|
177
|
+
|
|
178
|
+
message = message.replace(datePattern, (match) => {
|
|
179
|
+
try {
|
|
180
|
+
const date = new Date(match)
|
|
181
|
+
|
|
182
|
+
return TimeHelper.displayDate(date)
|
|
183
|
+
? `<span class="text-primary font-medium"> ${TimeHelper.displayDate(date)}</span>`
|
|
184
|
+
: match
|
|
185
|
+
} catch {
|
|
186
|
+
return match
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if (item.data) {
|
|
191
|
+
Object.entries(item.data).forEach(([key, value]) => {
|
|
192
|
+
if (key.endsWith('_slug') && typeof value === 'string' && value) {
|
|
193
|
+
// หา slug ในข้อความและครอบด้วย <strong>
|
|
194
|
+
const slugPattern = new RegExp(`\\b${value}\\b`, 'g')
|
|
195
|
+
|
|
196
|
+
message = message.replace(slugPattern, `<span class="text-primary font-medium">${value}</span>`)
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ตรวจหาคำที่อยู่ระหว่าง "from" และ "to" และทำให้เป็น bold + text-primary font-semibold
|
|
202
|
+
const fromToPattern = /\bfrom\s+(\S+)\s+to\s+(\S+)/gi
|
|
203
|
+
|
|
204
|
+
message = message.replace(fromToPattern, (_match, from, to) => {
|
|
205
|
+
return `from <span class="text-primary font-medium">${from}</span> to <span class="text-primary font-semibold">${to}</span>`
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return message
|
|
163
209
|
}
|
|
164
210
|
|
|
165
211
|
useWatchTrue(() => markAllRead.status.value.isSuccess, () => {
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|