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.
- package/LICENSE +21 -0
- package/README.md +715 -0
- package/app.vue +8 -0
- package/assets/css/main.css +21 -0
- package/assets/locales/en.json +380 -0
- package/bin/cli.mjs +279 -0
- package/components/LinkedKeyPicker.vue +135 -0
- package/components/PathPicker.vue +153 -0
- package/components/PluralEditor.vue +295 -0
- package/components/ScanModal.vue +153 -0
- package/components/TranslationHistoryModal.vue +66 -0
- package/components/TranslationRow.vue +541 -0
- package/components/dashboard/WidgetConfigModal.vue +121 -0
- package/components/dashboard/WidgetGrid.vue +190 -0
- package/components/dashboard/WidgetPicker.vue +75 -0
- package/components/dashboard/widgets/ActivityWidget.vue +109 -0
- package/components/dashboard/widgets/LanguagesCoverageWidget.vue +104 -0
- package/components/dashboard/widgets/ProjectsWidget.vue +77 -0
- package/components/dashboard/widgets/ReviewWidget.vue +150 -0
- package/components/dashboard/widgets/StatWidget.vue +133 -0
- package/composables/useAuth.ts +72 -0
- package/composables/useConfig.ts +14 -0
- package/composables/useDashboard.ts +89 -0
- package/composables/useFormats.ts +100 -0
- package/composables/useKeys.ts +231 -0
- package/composables/useLanguages.ts +221 -0
- package/composables/useProfile.ts +76 -0
- package/composables/useProject.ts +180 -0
- package/composables/useReview.ts +94 -0
- package/composables/useSettings.ts +30 -0
- package/composables/useStats.ts +16 -0
- package/composables/useT.ts +38 -0
- package/composables/useUsers.ts +101 -0
- package/composables/useWidgetData.ts +50 -0
- package/consts/commons.const.ts +6 -0
- package/consts/dashboard.const.ts +94 -0
- package/consts/languages.const.ts +223 -0
- package/enums/commons.enum.ts +7 -0
- package/i18n-dashboard.config.example.js +40 -0
- package/interfaces/commons.interface.ts +23 -0
- package/interfaces/job.interface.ts +10 -0
- package/interfaces/key.interface.ts +39 -0
- package/interfaces/languages.interface.ts +23 -0
- package/interfaces/project.interface.ts +9 -0
- package/interfaces/scan.interface.ts +12 -0
- package/interfaces/settings.interface.ts +4 -0
- package/interfaces/stat.interface.ts +30 -0
- package/interfaces/translation.interface.ts +11 -0
- package/interfaces/user.interface.ts +24 -0
- package/layouts/auth.vue +5 -0
- package/layouts/default.vue +327 -0
- package/middleware/auth.global.ts +26 -0
- package/nuxt.config.ts +66 -0
- package/package.json +89 -0
- package/pages/index.vue +5 -0
- package/pages/login.vue +74 -0
- package/pages/onboarding.vue +563 -0
- package/pages/projects/[id]/formats/datetime.vue +240 -0
- package/pages/projects/[id]/formats/modifiers.vue +194 -0
- package/pages/projects/[id]/formats/number.vue +250 -0
- package/pages/projects/[id]/index.vue +182 -0
- package/pages/projects/[id]/languages.vue +537 -0
- package/pages/projects/[id]/review.vue +109 -0
- package/pages/projects/[id]/settings.vue +515 -0
- package/pages/projects/[id]/translations/[keyId].vue +642 -0
- package/pages/projects/[id]/translations/index.vue +250 -0
- package/pages/projects/[id]/users.vue +276 -0
- package/pages/projects/index.vue +334 -0
- package/pages/users/[id]/profile.vue +421 -0
- package/pages/users/index.vue +345 -0
- package/plugins/loading.client.ts +3 -0
- package/plugins/ui-i18n.ts +6 -0
- package/server/api/auth/login.post.ts +28 -0
- package/server/api/auth/logout.post.ts +7 -0
- package/server/api/auth/me.get.ts +11 -0
- package/server/api/auth/me.put.ts +31 -0
- package/server/api/auth/password.put.ts +27 -0
- package/server/api/auth/status.get.ts +16 -0
- package/server/api/config.get.ts +10 -0
- package/server/api/dashboard/layout.get.ts +18 -0
- package/server/api/dashboard/layout.post.ts +18 -0
- package/server/api/db-config.get.ts +44 -0
- package/server/api/db-config.post.ts +73 -0
- package/server/api/export.get.ts +64 -0
- package/server/api/formats/datetime/[id].delete.ts +8 -0
- package/server/api/formats/datetime/[id].put.ts +15 -0
- package/server/api/formats/datetime.get.ts +11 -0
- package/server/api/formats/datetime.post.ts +16 -0
- package/server/api/formats/modifiers/[id].delete.ts +8 -0
- package/server/api/formats/modifiers/[id].put.ts +10 -0
- package/server/api/formats/modifiers.get.ts +10 -0
- package/server/api/formats/modifiers.post.ts +14 -0
- package/server/api/formats/number/[id].delete.ts +8 -0
- package/server/api/formats/number/[id].put.ts +15 -0
- package/server/api/formats/number.get.ts +11 -0
- package/server/api/formats/number.post.ts +16 -0
- package/server/api/formats/snippet.get.ts +87 -0
- package/server/api/fs/browse.get.ts +50 -0
- package/server/api/history/[translationId].get.ts +13 -0
- package/server/api/keys/[id].delete.ts +14 -0
- package/server/api/keys/[id].get.ts +41 -0
- package/server/api/keys/[id].patch.ts +20 -0
- package/server/api/keys/index.get.ts +98 -0
- package/server/api/keys/index.post.ts +17 -0
- package/server/api/languages/[code].delete.ts +15 -0
- package/server/api/languages/[id].put.ts +24 -0
- package/server/api/languages/index.get.ts +13 -0
- package/server/api/languages/index.post.ts +42 -0
- package/server/api/onboarding.post.ts +56 -0
- package/server/api/profile.get.ts +81 -0
- package/server/api/project-snapshot.get.ts +73 -0
- package/server/api/project-snapshot.post.ts +160 -0
- package/server/api/projects/[id].delete.ts +13 -0
- package/server/api/projects/[id].put.ts +40 -0
- package/server/api/projects/index.get.ts +19 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/scan.post.ts +165 -0
- package/server/api/settings/index.get.ts +9 -0
- package/server/api/settings/index.post.ts +20 -0
- package/server/api/setup.post.ts +39 -0
- package/server/api/stats/global.get.ts +126 -0
- package/server/api/stats.get.ts +70 -0
- package/server/api/sync.post.ts +179 -0
- package/server/api/translate.post.ts +52 -0
- package/server/api/translations/batch-translate.post.ts +121 -0
- package/server/api/translations/bulk-status.post.ts +24 -0
- package/server/api/translations/index.post.ts +62 -0
- package/server/api/translations/job/[id].get.ts +23 -0
- package/server/api/translations/status.post.ts +30 -0
- package/server/api/translations/translate-all.post.ts +18 -0
- package/server/api/ui-locale.get.ts +39 -0
- package/server/api/users/[id]/profile.get.ts +107 -0
- package/server/api/users/[id]/roles.put.ts +67 -0
- package/server/api/users/[id].delete.ts +36 -0
- package/server/api/users/[id].put.ts +43 -0
- package/server/api/users/index.get.ts +49 -0
- package/server/api/users/index.post.ts +89 -0
- package/server/consts/auto-translate.const.ts +2 -0
- package/server/consts/commons.const.ts +10 -0
- package/server/consts/db.const.ts +3 -0
- package/server/consts/scanner.const.ts +4 -0
- package/server/consts/translation-job.const.ts +8 -0
- package/server/db/index.ts +672 -0
- package/server/enums/auth.enum.ts +5 -0
- package/server/enums/translation.enum.ts +6 -0
- package/server/interfaces/profile.interface.ts +48 -0
- package/server/interfaces/project-config.interface.ts +9 -0
- package/server/interfaces/scanner.interface.ts +18 -0
- package/server/interfaces/translation-job.interface.ts +13 -0
- package/server/middleware/auth.ts +32 -0
- package/server/plugins/db.ts +6 -0
- package/server/routes/locale/[lang].get.ts +179 -0
- package/server/types/auth.type.ts +3 -0
- package/server/utils/auth.util.ts +89 -0
- package/server/utils/auto-translate.util.ts +112 -0
- package/server/utils/lang-api.util.ts +24 -0
- package/server/utils/mailer.util.ts +80 -0
- package/server/utils/project-config.util.ts +37 -0
- package/server/utils/scanner.uti.ts +307 -0
- package/server/utils/translation-job.util.ts +142 -0
- package/services/auth.service.ts +31 -0
- package/services/base.service.ts +140 -0
- package/services/job.service.ts +10 -0
- package/services/key.service.ts +26 -0
- package/services/language.service.ts +26 -0
- package/services/profile.service.ts +14 -0
- package/services/project.service.ts +23 -0
- package/services/scan.service.ts +14 -0
- package/services/settings.service.ts +14 -0
- package/services/stats.service.ts +11 -0
- package/services/translation.service.ts +36 -0
- package/services/user.service.ts +28 -0
- package/tsconfig.json +3 -0
- package/types/commons.type.ts +3 -0
- package/types/dashboard.type.ts +26 -0
- 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
|
+
}
|