i18n-dashboard 0.1.0

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 (176) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +715 -0
  3. package/app.vue +8 -0
  4. package/assets/css/main.css +21 -0
  5. package/assets/locales/en.json +380 -0
  6. package/bin/cli.mjs +279 -0
  7. package/components/LinkedKeyPicker.vue +135 -0
  8. package/components/PathPicker.vue +153 -0
  9. package/components/PluralEditor.vue +295 -0
  10. package/components/ScanModal.vue +153 -0
  11. package/components/TranslationHistoryModal.vue +66 -0
  12. package/components/TranslationRow.vue +541 -0
  13. package/components/dashboard/WidgetConfigModal.vue +121 -0
  14. package/components/dashboard/WidgetGrid.vue +190 -0
  15. package/components/dashboard/WidgetPicker.vue +75 -0
  16. package/components/dashboard/widgets/ActivityWidget.vue +109 -0
  17. package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
  18. package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
  19. package/components/dashboard/widgets/ReviewWidget.vue +150 -0
  20. package/components/dashboard/widgets/StatWidget.vue +133 -0
  21. package/composables/useAuth.ts +72 -0
  22. package/composables/useConfig.ts +14 -0
  23. package/composables/useDashboard.ts +89 -0
  24. package/composables/useFormats.ts +100 -0
  25. package/composables/useKeys.ts +231 -0
  26. package/composables/useLanguages.ts +221 -0
  27. package/composables/useProfile.ts +76 -0
  28. package/composables/useProject.ts +180 -0
  29. package/composables/useReview.ts +94 -0
  30. package/composables/useSettings.ts +30 -0
  31. package/composables/useStats.ts +16 -0
  32. package/composables/useT.ts +38 -0
  33. package/composables/useUsers.ts +101 -0
  34. package/composables/useWidgetData.ts +50 -0
  35. package/consts/commons.const.ts +6 -0
  36. package/consts/dashboard.const.ts +94 -0
  37. package/consts/languages.const.ts +223 -0
  38. package/enums/commons.enum.ts +7 -0
  39. package/i18n-dashboard.config.example.js +40 -0
  40. package/interfaces/commons.interface.ts +23 -0
  41. package/interfaces/job.interface.ts +10 -0
  42. package/interfaces/key.interface.ts +39 -0
  43. package/interfaces/languages.interface.ts +23 -0
  44. package/interfaces/project.interface.ts +9 -0
  45. package/interfaces/scan.interface.ts +12 -0
  46. package/interfaces/settings.interface.ts +4 -0
  47. package/interfaces/stat.interface.ts +30 -0
  48. package/interfaces/translation.interface.ts +11 -0
  49. package/interfaces/user.interface.ts +24 -0
  50. package/layouts/auth.vue +5 -0
  51. package/layouts/default.vue +327 -0
  52. package/middleware/auth.global.ts +26 -0
  53. package/nuxt.config.ts +66 -0
  54. package/package.json +89 -0
  55. package/pages/index.vue +5 -0
  56. package/pages/login.vue +74 -0
  57. package/pages/onboarding.vue +563 -0
  58. package/pages/projects/[id]/formats/datetime.vue +240 -0
  59. package/pages/projects/[id]/formats/modifiers.vue +194 -0
  60. package/pages/projects/[id]/formats/number.vue +250 -0
  61. package/pages/projects/[id]/index.vue +182 -0
  62. package/pages/projects/[id]/languages.vue +537 -0
  63. package/pages/projects/[id]/review.vue +109 -0
  64. package/pages/projects/[id]/settings.vue +515 -0
  65. package/pages/projects/[id]/translations/[keyId].vue +642 -0
  66. package/pages/projects/[id]/translations/index.vue +250 -0
  67. package/pages/projects/[id]/users.vue +276 -0
  68. package/pages/projects/index.vue +334 -0
  69. package/pages/users/[id]/profile.vue +421 -0
  70. package/pages/users/index.vue +345 -0
  71. package/plugins/loading.client.ts +3 -0
  72. package/plugins/ui-i18n.ts +6 -0
  73. package/server/api/auth/login.post.ts +28 -0
  74. package/server/api/auth/logout.post.ts +7 -0
  75. package/server/api/auth/me.get.ts +11 -0
  76. package/server/api/auth/me.put.ts +31 -0
  77. package/server/api/auth/password.put.ts +27 -0
  78. package/server/api/auth/status.get.ts +16 -0
  79. package/server/api/config.get.ts +10 -0
  80. package/server/api/dashboard/layout.get.ts +18 -0
  81. package/server/api/dashboard/layout.post.ts +18 -0
  82. package/server/api/db-config.get.ts +44 -0
  83. package/server/api/db-config.post.ts +73 -0
  84. package/server/api/export.get.ts +64 -0
  85. package/server/api/formats/datetime/[id].delete.ts +8 -0
  86. package/server/api/formats/datetime/[id].put.ts +15 -0
  87. package/server/api/formats/datetime.get.ts +11 -0
  88. package/server/api/formats/datetime.post.ts +16 -0
  89. package/server/api/formats/modifiers/[id].delete.ts +8 -0
  90. package/server/api/formats/modifiers/[id].put.ts +10 -0
  91. package/server/api/formats/modifiers.get.ts +10 -0
  92. package/server/api/formats/modifiers.post.ts +14 -0
  93. package/server/api/formats/number/[id].delete.ts +8 -0
  94. package/server/api/formats/number/[id].put.ts +15 -0
  95. package/server/api/formats/number.get.ts +11 -0
  96. package/server/api/formats/number.post.ts +16 -0
  97. package/server/api/formats/snippet.get.ts +87 -0
  98. package/server/api/fs/browse.get.ts +50 -0
  99. package/server/api/history/[translationId].get.ts +13 -0
  100. package/server/api/keys/[id].delete.ts +14 -0
  101. package/server/api/keys/[id].get.ts +41 -0
  102. package/server/api/keys/[id].patch.ts +20 -0
  103. package/server/api/keys/index.get.ts +98 -0
  104. package/server/api/keys/index.post.ts +17 -0
  105. package/server/api/languages/[code].delete.ts +15 -0
  106. package/server/api/languages/[id].put.ts +24 -0
  107. package/server/api/languages/index.get.ts +13 -0
  108. package/server/api/languages/index.post.ts +42 -0
  109. package/server/api/onboarding.post.ts +56 -0
  110. package/server/api/profile.get.ts +81 -0
  111. package/server/api/project-snapshot.get.ts +73 -0
  112. package/server/api/project-snapshot.post.ts +160 -0
  113. package/server/api/projects/[id].delete.ts +13 -0
  114. package/server/api/projects/[id].put.ts +40 -0
  115. package/server/api/projects/index.get.ts +19 -0
  116. package/server/api/projects/index.post.ts +34 -0
  117. package/server/api/scan.post.ts +165 -0
  118. package/server/api/settings/index.get.ts +9 -0
  119. package/server/api/settings/index.post.ts +20 -0
  120. package/server/api/setup.post.ts +39 -0
  121. package/server/api/stats/global.get.ts +126 -0
  122. package/server/api/stats.get.ts +70 -0
  123. package/server/api/sync.post.ts +179 -0
  124. package/server/api/translate.post.ts +52 -0
  125. package/server/api/translations/batch-translate.post.ts +121 -0
  126. package/server/api/translations/bulk-status.post.ts +24 -0
  127. package/server/api/translations/index.post.ts +62 -0
  128. package/server/api/translations/job/[id].get.ts +23 -0
  129. package/server/api/translations/status.post.ts +30 -0
  130. package/server/api/translations/translate-all.post.ts +18 -0
  131. package/server/api/ui-locale.get.ts +39 -0
  132. package/server/api/users/[id]/profile.get.ts +107 -0
  133. package/server/api/users/[id]/roles.put.ts +67 -0
  134. package/server/api/users/[id].delete.ts +36 -0
  135. package/server/api/users/[id].put.ts +43 -0
  136. package/server/api/users/index.get.ts +49 -0
  137. package/server/api/users/index.post.ts +89 -0
  138. package/server/consts/auto-translate.const.ts +2 -0
  139. package/server/consts/commons.const.ts +10 -0
  140. package/server/consts/db.const.ts +3 -0
  141. package/server/consts/scanner.const.ts +4 -0
  142. package/server/consts/translation-job.const.ts +8 -0
  143. package/server/db/index.ts +672 -0
  144. package/server/enums/auth.enum.ts +5 -0
  145. package/server/enums/translation.enum.ts +6 -0
  146. package/server/interfaces/profile.interface.ts +48 -0
  147. package/server/interfaces/project-config.interface.ts +9 -0
  148. package/server/interfaces/scanner.interface.ts +18 -0
  149. package/server/interfaces/translation-job.interface.ts +13 -0
  150. package/server/middleware/auth.ts +32 -0
  151. package/server/plugins/db.ts +6 -0
  152. package/server/routes/locale/[lang].get.ts +179 -0
  153. package/server/types/auth.type.ts +3 -0
  154. package/server/utils/auth.util.ts +89 -0
  155. package/server/utils/auto-translate.util.ts +112 -0
  156. package/server/utils/lang-api.util.ts +24 -0
  157. package/server/utils/mailer.util.ts +80 -0
  158. package/server/utils/project-config.util.ts +37 -0
  159. package/server/utils/scanner.uti.ts +307 -0
  160. package/server/utils/translation-job.util.ts +142 -0
  161. package/services/auth.service.ts +31 -0
  162. package/services/base.service.ts +140 -0
  163. package/services/job.service.ts +10 -0
  164. package/services/key.service.ts +26 -0
  165. package/services/language.service.ts +26 -0
  166. package/services/profile.service.ts +14 -0
  167. package/services/project.service.ts +23 -0
  168. package/services/scan.service.ts +14 -0
  169. package/services/settings.service.ts +14 -0
  170. package/services/stats.service.ts +11 -0
  171. package/services/translation.service.ts +36 -0
  172. package/services/user.service.ts +28 -0
  173. package/tsconfig.json +3 -0
  174. package/types/commons.type.ts +3 -0
  175. package/types/dashboard.type.ts +26 -0
  176. package/utils/config.util.ts +60 -0
@@ -0,0 +1,150 @@
1
+ <script lang="ts" setup>
2
+ import type { PropType } from 'vue'
3
+ import type { WidgetSize, WidgetDataSource } from '~/types/dashboard.type'
4
+ import { TRANSLATION_STATUS } from '~/server/enums/translation.enum'
5
+
6
+ const props = defineProps({
7
+ id: {
8
+ type: String,
9
+ required: true,
10
+ },
11
+ size: {
12
+ type: String as PropType<WidgetSize>,
13
+ required: true,
14
+ },
15
+ editing: {
16
+ type: Boolean,
17
+ required: true,
18
+ },
19
+ dataSource: {
20
+ type: Object as PropType<WidgetDataSource | undefined>,
21
+ default: undefined,
22
+ },
23
+ title: {
24
+ type: String as PropType<string | undefined>,
25
+ default: undefined,
26
+ },
27
+ })
28
+
29
+ const { t } = useT()
30
+
31
+ const { effectiveSource, sourceLabel, hasProject } = useWidgetData(
32
+ props.id,
33
+ computed(() => props.dataSource),
34
+ )
35
+
36
+ const effectiveProjectId = computed(() => {
37
+ const src = effectiveSource.value
38
+ if (src.type === 'project') return src.projectId
39
+ return undefined // global = unsupported for review queue
40
+ })
41
+
42
+ const fetchKey = computed(() => `widget-review-${props.id}-${effectiveProjectId.value ?? 'none'}`)
43
+
44
+ const { data: reviewData, pending, refresh } = useAsyncData(
45
+ () => fetchKey.value,
46
+ async () => {
47
+ if (!effectiveProjectId.value) return null
48
+ return await $fetch<any>('/api/keys', {
49
+ query: {
50
+ project_id: effectiveProjectId.value,
51
+ status: TRANSLATION_STATUS.DRAFT,
52
+ limit: 200,
53
+ },
54
+ })
55
+ },
56
+ { server: false, watch: [fetchKey] },
57
+ )
58
+
59
+ const reviewItems = computed(() => {
60
+ const keys = reviewData.value?.data ?? []
61
+ const result: Array<{ id: number; key: string; key_description?: string; language_code: string; value: string }> = []
62
+ for (const k of keys) {
63
+ for (const [lang, tr] of Object.entries(k.translations as Record<string, any>)) {
64
+ if (tr?.status === TRANSLATION_STATUS.DRAFT && tr?.value) {
65
+ result.push({ id: tr.id, key: k.key, key_description: k.description, language_code: lang, value: tr.value })
66
+ }
67
+ }
68
+ }
69
+ return result
70
+ })
71
+
72
+ const maxItems = computed(() => {
73
+ if (props.size === 'lg') return 8
74
+ return 4
75
+ })
76
+
77
+ const displayedItems = computed(() => reviewItems.value.slice(0, maxItems.value))
78
+
79
+ const displayTitle = computed(() => props.title || t('review.title', 'Review queue'))
80
+
81
+ const processingId = ref<number | null>(null)
82
+ const processingAction = ref('')
83
+
84
+ async function setStatus(item: { id: number }, status: string): Promise<void> {
85
+ processingId.value = item.id
86
+ processingAction.value = status
87
+ try {
88
+ await $fetch('/api/translations/bulk-status', {
89
+ method: 'POST',
90
+ body: { ids: [item.id], status },
91
+ })
92
+ await refresh()
93
+ } catch {} finally {
94
+ processingId.value = null
95
+ processingAction.value = ''
96
+ }
97
+ }
98
+ </script>
99
+
100
+ <template>
101
+ <UCard class="h-full overflow-hidden">
102
+ <template #header>
103
+ <div class="flex items-center gap-2">
104
+ <UIcon name="i-heroicons-clipboard-document-check" class="text-gray-400" />
105
+ <span class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{ displayTitle }}</span>
106
+ <UBadge v-if="reviewItems.length" :label="String(reviewItems.length)" color="warning" variant="soft" size="xs" class="ml-auto" />
107
+ <span v-else-if="sourceLabel" class="ml-auto text-xs text-gray-400 dark:text-gray-500">{{ sourceLabel }}</span>
108
+ </div>
109
+ </template>
110
+
111
+ <div v-if="pending" class="space-y-2">
112
+ <USkeleton v-for="i in 3" :key="i" class="h-12 w-full" />
113
+ </div>
114
+
115
+ <div v-else-if="!hasProject" class="flex flex-col items-center justify-center h-full py-6 text-center">
116
+ <UIcon name="i-heroicons-clipboard-document-check" class="text-3xl text-gray-300 dark:text-gray-600 mb-2" />
117
+ <p class="text-sm text-gray-400">{{ t('dashboard.select_project', 'Select a project') }}</p>
118
+ </div>
119
+
120
+ <div v-else-if="!displayedItems.length" class="flex flex-col items-center justify-center h-full py-6 text-center">
121
+ <UIcon name="i-heroicons-check-circle" class="text-3xl text-green-400 mb-2" />
122
+ <p class="text-sm text-gray-400">{{ t('review.empty_title', 'No translations pending') }}</p>
123
+ </div>
124
+
125
+ <div v-else class="overflow-y-auto space-y-2">
126
+ <div
127
+ v-for="item in displayedItems"
128
+ :key="item.id"
129
+ class="flex items-center gap-2 p-2 rounded-lg bg-gray-50 dark:bg-gray-800"
130
+ >
131
+ <div class="min-w-0 flex-1">
132
+ <p class="text-xs font-mono text-gray-700 dark:text-gray-300 truncate">{{ item.key }}</p>
133
+ <div class="flex items-center gap-1.5 mt-0.5">
134
+ <UBadge :label="item.language_code.toUpperCase()" color="neutral" variant="soft" size="xs" />
135
+ <p class="text-xs text-gray-500 truncate">{{ item.value }}</p>
136
+ </div>
137
+ </div>
138
+ <UButton
139
+ v-if="!editing"
140
+ icon="i-heroicons-check"
141
+ size="xs"
142
+ color="success"
143
+ variant="soft"
144
+ :loading="processingId === item.id && processingAction === TRANSLATION_STATUS.REVIEWED"
145
+ @click="setStatus(item, TRANSLATION_STATUS.REVIEWED)"
146
+ />
147
+ </div>
148
+ </div>
149
+ </UCard>
150
+ </template>
@@ -0,0 +1,133 @@
1
+ <script lang="ts" setup>
2
+ import type { PropType } from 'vue'
3
+ import type { WidgetType, WidgetSize, WidgetDataSource } from '~/types/dashboard.type'
4
+
5
+ const props = defineProps({
6
+ id: {
7
+ type: String,
8
+ required: true,
9
+ },
10
+ type: {
11
+ type: String as PropType<WidgetType>,
12
+ required: true,
13
+ },
14
+ size: {
15
+ type: String as PropType<WidgetSize>,
16
+ required: true,
17
+ },
18
+ editing: {
19
+ type: Boolean,
20
+ required: true,
21
+ },
22
+ dataSource: {
23
+ type: Object as PropType<WidgetDataSource | undefined>,
24
+ default: undefined,
25
+ },
26
+ title: {
27
+ type: String as PropType<string | undefined>,
28
+ default: undefined,
29
+ },
30
+ })
31
+
32
+ const { t } = useT()
33
+
34
+ const { stats, pending, sourceLabel, hasProject } = useWidgetData(
35
+ props.id,
36
+ computed(() => props.dataSource),
37
+ )
38
+
39
+ const config = computed(() => {
40
+ switch (props.type) {
41
+ case 'stat-keys':
42
+ return {
43
+ label: t('dashboard.stat_total_keys', 'Total keys'),
44
+ icon: 'i-heroicons-key',
45
+ iconColor: 'text-blue-600',
46
+ bgColor: 'bg-blue-50 dark:bg-blue-900/20',
47
+ }
48
+ case 'stat-coverage':
49
+ return {
50
+ label: t('dashboard.stat_coverage', 'Coverage'),
51
+ icon: 'i-heroicons-chart-bar',
52
+ iconColor: 'text-green-600',
53
+ bgColor: 'bg-green-50 dark:bg-green-900/20',
54
+ }
55
+ case 'stat-languages':
56
+ return {
57
+ label: t('dashboard.stat_languages', 'Languages'),
58
+ icon: 'i-heroicons-language',
59
+ iconColor: 'text-purple-600',
60
+ bgColor: 'bg-purple-50 dark:bg-purple-900/20',
61
+ }
62
+ case 'stat-unused':
63
+ return {
64
+ label: t('dashboard.stat_unused', 'Unused'),
65
+ icon: 'i-heroicons-exclamation-triangle',
66
+ iconColor: 'text-orange-500',
67
+ bgColor: 'bg-orange-50 dark:bg-orange-900/20',
68
+ }
69
+ default:
70
+ return { label: '', icon: '', iconColor: '', bgColor: '' }
71
+ }
72
+ })
73
+
74
+ const displayLabel = computed(() => props.title || config.value.label)
75
+
76
+ const coverage = computed(() => {
77
+ const langs = stats.value?.languages
78
+ if (!langs?.length) return 0
79
+ const total = langs.reduce((sum: number, l: any) => sum + l.coverage, 0)
80
+ return Math.round(total / langs.length)
81
+ })
82
+
83
+ const displayValue = computed(() => {
84
+ if (!stats.value) return '—'
85
+ switch (props.type) {
86
+ case 'stat-keys': return stats.value.totalKeys ?? '—'
87
+ case 'stat-coverage': return `${coverage.value}%`
88
+ case 'stat-languages': return stats.value.languages?.length ?? '—'
89
+ case 'stat-unused': return stats.value.unusedKeys ?? '—'
90
+ default: return '—'
91
+ }
92
+ })
93
+ </script>
94
+
95
+ <template>
96
+ <UCard class="h-full overflow-hidden">
97
+ <div class="flex flex-col h-full justify-between">
98
+ <div v-if="pending" class="space-y-2">
99
+ <USkeleton class="h-8 w-16" />
100
+ <USkeleton class="h-4 w-24" />
101
+ </div>
102
+
103
+ <div v-else class="flex flex-col h-full">
104
+ <div class="flex items-start justify-between">
105
+ <div
106
+ class="w-10 h-10 rounded-lg flex items-center justify-center shrink-0"
107
+ :class="config.bgColor"
108
+ >
109
+ <UIcon :name="config.icon" class="text-xl" :class="config.iconColor" />
110
+ </div>
111
+ </div>
112
+
113
+ <div class="mt-auto">
114
+ <p class="text-3xl font-bold text-gray-900 dark:text-white mt-3">
115
+ {{ hasProject ? displayValue : '—' }}
116
+ </p>
117
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">{{ displayLabel }}</p>
118
+ <p v-if="!hasProject" class="text-xs text-gray-400 dark:text-gray-600 mt-1">{{ t('dashboard.select_project', 'Select a project') }}</p>
119
+ <p v-else-if="sourceLabel" class="text-xs text-gray-400 dark:text-gray-500 mt-1">{{ sourceLabel }}</p>
120
+ </div>
121
+
122
+ <div v-if="size === 'md' && type === 'stat-coverage' && stats && hasProject" class="mt-3">
123
+ <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
124
+ <div
125
+ class="bg-green-500 h-1.5 rounded-full transition-all"
126
+ :style="{ width: `${coverage}%` }"
127
+ />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </UCard>
133
+ </template>
@@ -0,0 +1,72 @@
1
+ import { authService } from '../services/auth.service'
2
+
3
+ export interface AuthUser {
4
+ id: number
5
+ email: string
6
+ name: string
7
+ is_super_admin: boolean
8
+ is_active: boolean
9
+ roles: Array<{ role: string; project_id: number | null; project_name?: string }>
10
+ }
11
+
12
+ const _currentUser = ref<AuthUser | null>(null)
13
+
14
+ export function useAuth() {
15
+ const currentUser = _currentUser
16
+
17
+ const fetchMe = async () => {
18
+ try {
19
+ const user = await authService.me()
20
+ _currentUser.value = user
21
+ return user
22
+ } catch {
23
+ _currentUser.value = null
24
+ return null
25
+ }
26
+ }
27
+
28
+ const login = async (email: string, password: string) => {
29
+ const user = await authService.login(email, password)
30
+ _currentUser.value = user
31
+ return user
32
+ }
33
+
34
+ const logout = async () => {
35
+ await authService.logout()
36
+ _currentUser.value = null
37
+ return null
38
+ }
39
+
40
+ const changePassword = async (currentPassword: string, newPassword: string) => {
41
+ await authService.changePassword(currentPassword, newPassword)
42
+ }
43
+
44
+ const getRoleForProject = (projectId: number): string | null => {
45
+ if (!_currentUser.value) return null
46
+ if (_currentUser.value.is_super_admin) return 'super_admin'
47
+ const specific = _currentUser.value.roles.find((r) => r.project_id === projectId)
48
+ if (specific) return specific.role
49
+ const global_ = _currentUser.value.roles.find((r) => r.project_id === null)
50
+ return global_?.role || null
51
+ }
52
+
53
+ const canApprove = (projectId: number): boolean => {
54
+ const role = getRoleForProject(projectId)
55
+ return role === 'super_admin' || role === 'admin' || role === 'moderator'
56
+ }
57
+
58
+ const canManageProject = (projectId: number): boolean => {
59
+ const role = getRoleForProject(projectId)
60
+ return role === 'super_admin' || role === 'admin'
61
+ }
62
+
63
+ const canManageUsers = (projectId?: number): boolean => {
64
+ if (!_currentUser.value) return false
65
+ if (_currentUser.value.is_super_admin) return true
66
+ if (!projectId) return false
67
+ const role = getRoleForProject(projectId)
68
+ return role === 'admin'
69
+ }
70
+
71
+ return { currentUser, fetchMe, login, logout, changePassword, getRoleForProject, canApprove, canManageProject, canManageUsers }
72
+ }
@@ -0,0 +1,14 @@
1
+ import { buildEnv, loadUserConfig } from '../utils/config.util'
2
+
3
+ export function useConfig () {
4
+ const config = ref(null)
5
+
6
+ onMounted(async () => {
7
+ const userConfig = await loadUserConfig()
8
+ config.value = buildEnv(userConfig)
9
+ })
10
+
11
+ return {
12
+ config
13
+ }
14
+ }
@@ -0,0 +1,89 @@
1
+ import type { WidgetConfig, WidgetDataSource } from '../types/dashboard.type'
2
+ import type { WidgetSize } from '../types/dashboard.type'
3
+ import { DEFAULT_LAYOUT } from '../consts/dashboard.const'
4
+
5
+ export function useDashboard() {
6
+ const { data, refresh } = useAsyncData('dashboard-layout', () => $fetch('/api/dashboard/layout'), { server: false })
7
+ const layout = computed<WidgetConfig[]>(() => (data.value as WidgetConfig[]) ?? DEFAULT_LAYOUT)
8
+
9
+ const editing = ref(false)
10
+ const localLayout = ref<WidgetConfig[]>([])
11
+
12
+ function startEditing() {
13
+ localLayout.value = [...layout.value]
14
+ editing.value = true
15
+ }
16
+
17
+ const draggingIndex = ref<number | null>(null)
18
+
19
+ function onDragStart(index: number) {
20
+ draggingIndex.value = index
21
+ }
22
+
23
+ function onDragOver(e: DragEvent, index: number) {
24
+ e.preventDefault()
25
+ if (draggingIndex.value === null || draggingIndex.value === index) return
26
+ const arr = [...localLayout.value]
27
+ const [item] = arr.splice(draggingIndex.value, 1)
28
+ arr.splice(index, 0, item)
29
+ localLayout.value = arr
30
+ draggingIndex.value = index
31
+ }
32
+
33
+ function onDragEnd() {
34
+ draggingIndex.value = null
35
+ }
36
+
37
+ function removeWidget(index: number) {
38
+ localLayout.value.splice(index, 1)
39
+ }
40
+
41
+ function addWidget(widget: WidgetConfig) {
42
+ localLayout.value.push(widget)
43
+ }
44
+
45
+ function resizeWidget(index: number, size: WidgetSize) {
46
+ localLayout.value[index].size = size
47
+ }
48
+
49
+ function updateWidgetConfig(index: number, patch: { dataSource?: WidgetDataSource | undefined; title?: string | undefined }) {
50
+ if (index < 0 || index >= localLayout.value.length) return
51
+ localLayout.value[index] = { ...localLayout.value[index], ...patch }
52
+ }
53
+
54
+ const saving = ref(false)
55
+
56
+ async function saveLayout() {
57
+ saving.value = true
58
+ try {
59
+ await $fetch('/api/dashboard/layout', { method: 'POST', body: localLayout.value })
60
+ await refresh()
61
+ editing.value = false
62
+ } finally {
63
+ saving.value = false
64
+ }
65
+ }
66
+
67
+ function cancelEditing() {
68
+ editing.value = false
69
+ localLayout.value = []
70
+ }
71
+
72
+ return {
73
+ layout,
74
+ editing,
75
+ localLayout,
76
+ saving,
77
+ draggingIndex,
78
+ startEditing,
79
+ cancelEditing,
80
+ saveLayout,
81
+ onDragStart,
82
+ onDragOver,
83
+ onDragEnd,
84
+ removeWidget,
85
+ addWidget,
86
+ resizeWidget,
87
+ updateWidgetConfig,
88
+ }
89
+ }
@@ -0,0 +1,100 @@
1
+ export function useFormats() {
2
+ const { currentProject } = useProject()
3
+ const projectId = computed(() => currentProject.value?.id)
4
+
5
+ // Number formats
6
+ const { data: numberFormats, refresh: refreshNumber } = useFetch<any[]>('/api/formats/number', {
7
+ query: computed(() => ({ project_id: projectId.value })),
8
+ default: () => [],
9
+ })
10
+
11
+ // Datetime formats
12
+ const { data: datetimeFormats, refresh: refreshDatetime } = useFetch<any[]>('/api/formats/datetime', {
13
+ query: computed(() => ({ project_id: projectId.value })),
14
+ default: () => [],
15
+ })
16
+
17
+ // Modifiers
18
+ const { data: modifiers, refresh: refreshModifiers } = useFetch<any[]>('/api/formats/modifiers', {
19
+ query: computed(() => ({ project_id: projectId.value })),
20
+ default: () => [],
21
+ })
22
+
23
+ async function createNumberFormat(locale: string, name: string, options: Record<string, any>) {
24
+ await $fetch('/api/formats/number', {
25
+ method: 'POST',
26
+ body: { project_id: projectId.value, locale, name, options },
27
+ })
28
+ await refreshNumber()
29
+ }
30
+
31
+ async function updateNumberFormat(id: number, locale: string, name: string, options: Record<string, any>) {
32
+ await $fetch(`/api/formats/number/${id}`, {
33
+ method: 'PUT',
34
+ body: { locale, name, options },
35
+ })
36
+ await refreshNumber()
37
+ }
38
+
39
+ async function deleteNumberFormat(id: number) {
40
+ await $fetch(`/api/formats/number/${id}`, { method: 'DELETE' })
41
+ await refreshNumber()
42
+ }
43
+
44
+ async function createDatetimeFormat(locale: string, name: string, options: Record<string, any>) {
45
+ await $fetch('/api/formats/datetime', {
46
+ method: 'POST',
47
+ body: { project_id: projectId.value, locale, name, options },
48
+ })
49
+ await refreshDatetime()
50
+ }
51
+
52
+ async function updateDatetimeFormat(id: number, locale: string, name: string, options: Record<string, any>) {
53
+ await $fetch(`/api/formats/datetime/${id}`, {
54
+ method: 'PUT',
55
+ body: { locale, name, options },
56
+ })
57
+ await refreshDatetime()
58
+ }
59
+
60
+ async function deleteDatetimeFormat(id: number) {
61
+ await $fetch(`/api/formats/datetime/${id}`, { method: 'DELETE' })
62
+ await refreshDatetime()
63
+ }
64
+
65
+ async function createModifier(name: string, body: string) {
66
+ await $fetch('/api/formats/modifiers', {
67
+ method: 'POST',
68
+ body: { project_id: projectId.value, name, body },
69
+ })
70
+ await refreshModifiers()
71
+ }
72
+
73
+ async function updateModifier(id: number, name: string, body: string) {
74
+ await $fetch(`/api/formats/modifiers/${id}`, {
75
+ method: 'PUT',
76
+ body: { name, body },
77
+ })
78
+ await refreshModifiers()
79
+ }
80
+
81
+ async function deleteModifier(id: number) {
82
+ await $fetch(`/api/formats/modifiers/${id}`, { method: 'DELETE' })
83
+ await refreshModifiers()
84
+ }
85
+
86
+ return {
87
+ numberFormats,
88
+ datetimeFormats,
89
+ modifiers,
90
+ createNumberFormat,
91
+ updateNumberFormat,
92
+ deleteNumberFormat,
93
+ createDatetimeFormat,
94
+ updateDatetimeFormat,
95
+ deleteDatetimeFormat,
96
+ createModifier,
97
+ updateModifier,
98
+ deleteModifier,
99
+ }
100
+ }