@motor-cms/ui-admin 1.1.0-alpha.4 → 1.1.0-alpha.5
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/app/assets/css/v-onboarding.css +64 -0
- package/app/components/OnboardingStep.vue +42 -0
- package/app/components/UsersOnboarding.vue +84 -0
- package/app/components/client/FooterSlotCard.vue +313 -0
- package/app/components/client/GlobalComponentsSection.vue +65 -0
- package/app/components/dashboard/DashboardActivity.vue +71 -0
- package/app/components/dashboard/DashboardActivityItem.vue +96 -0
- package/app/components/dashboard/DashboardAnnouncementModal.vue +327 -0
- package/app/components/dashboard/DashboardAnnouncements.vue +93 -0
- package/app/components/dashboard/DashboardOnboarding.vue +285 -0
- package/app/components/dashboard/DashboardPublishingQueue.vue +47 -0
- package/app/components/dashboard/DashboardQuickActions.vue +44 -0
- package/app/components/dashboard/DashboardStats.vue +63 -0
- package/app/components/form/inputs/CategoryTreePicker.vue +265 -109
- package/app/components/form/inputs/EntityConfigurationsPanel.vue +235 -0
- package/app/composables/useClientFormExtensions.ts +89 -0
- package/app/composables/useClientLanguages.ts +81 -0
- package/app/composables/useDashboardData.ts +169 -0
- package/app/composables/useOnboardingState.ts +151 -0
- package/app/data/footerTemplate.ts +283 -0
- package/app/lang/de/motor-admin/ai_system_prompts.json +1 -0
- package/app/lang/de/motor-admin/categories.json +1 -0
- package/app/lang/de/motor-admin/category_trees.json +2 -1
- package/app/lang/de/motor-admin/clients.json +17 -1
- package/app/lang/de/motor-admin/config_variables.json +1 -0
- package/app/lang/de/motor-admin/dashboard.json +83 -0
- package/app/lang/de/motor-admin/domains.json +6 -1
- package/app/lang/de/motor-admin/email_templates.json +1 -0
- package/app/lang/de/motor-admin/entity_configurations.json +12 -0
- package/app/lang/de/motor-admin/languages.json +1 -0
- package/app/lang/de/motor-admin/onboarding.json +60 -0
- package/app/lang/de/motor-admin/permissions.json +1 -0
- package/app/lang/de/motor-admin/roles.json +1 -0
- package/app/lang/de/motor-admin/users.json +1 -0
- package/app/lang/en/motor-admin/ai_system_prompts.json +1 -0
- package/app/lang/en/motor-admin/categories.json +1 -0
- package/app/lang/en/motor-admin/category_trees.json +2 -1
- package/app/lang/en/motor-admin/clients.json +17 -1
- package/app/lang/en/motor-admin/config_variables.json +1 -0
- package/app/lang/en/motor-admin/dashboard.json +83 -0
- package/app/lang/en/motor-admin/domains.json +6 -1
- package/app/lang/en/motor-admin/email_templates.json +1 -0
- package/app/lang/en/motor-admin/entity_configurations.json +12 -0
- package/app/lang/en/motor-admin/languages.json +1 -0
- package/app/lang/en/motor-admin/onboarding.json +60 -0
- package/app/lang/en/motor-admin/permissions.json +1 -0
- package/app/lang/en/motor-admin/roles.json +1 -0
- package/app/lang/en/motor-admin/users.json +1 -0
- package/app/pages/index.vue +119 -22
- package/app/pages/login.vue +6 -0
- package/app/pages/motor-admin/ai-system-prompts/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/category-trees/[id]/categories/[categoryId]/edit.vue +4 -3
- package/app/pages/motor-admin/category-trees/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/clients/[id]/edit.vue +146 -6
- package/app/pages/motor-admin/clients/create.vue +34 -2
- package/app/pages/motor-admin/config-variables/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/domains/[id]/edit.vue +18 -5
- package/app/pages/motor-admin/email-templates/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/email-templates/index.vue +36 -25
- package/app/pages/motor-admin/languages/[id]/edit.vue +17 -4
- package/app/pages/motor-admin/languages/create.vue +13 -0
- package/app/pages/motor-admin/permission-groups/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/roles/[id]/edit.vue +4 -4
- package/app/pages/motor-admin/roles/create.vue +4 -1
- package/app/pages/motor-admin/users/[id]/edit.vue +4 -3
- package/app/pages/motor-admin/users/index.vue +1 -0
- package/app/pages/profile.vue +47 -1
- package/app/pages/search.vue +13 -3
- package/app/types/generated/form-meta.ts +24 -20
- package/app/types/generated/grid-meta.ts +5 -3
- package/nuxt.config.ts +15 -1
- package/package.json +6 -2
- package/app/pages/dashboard.vue +0 -5
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
configurableType: string
|
|
4
|
+
configurableId: number | string
|
|
5
|
+
}>()
|
|
6
|
+
|
|
7
|
+
const { t } = useI18n()
|
|
8
|
+
const client = useSanctumClient()
|
|
9
|
+
const { success: notifySuccess, error: notifyError } = useNotify()
|
|
10
|
+
|
|
11
|
+
interface ConfigVariable {
|
|
12
|
+
id: number
|
|
13
|
+
package: string
|
|
14
|
+
group: string
|
|
15
|
+
name: string
|
|
16
|
+
value: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface EntityConfiguration {
|
|
20
|
+
id: number
|
|
21
|
+
configurable_type: string
|
|
22
|
+
configurable_id: number
|
|
23
|
+
config_variable_id: number
|
|
24
|
+
config_variable?: ConfigVariable
|
|
25
|
+
value: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const configurations = ref<EntityConfiguration[]>([])
|
|
29
|
+
const configVariables = ref<ConfigVariable[]>([])
|
|
30
|
+
const loading = ref(false)
|
|
31
|
+
const saving = ref(false)
|
|
32
|
+
const removing = ref<number | null>(null)
|
|
33
|
+
|
|
34
|
+
const newConfigVariableId = ref<number | null>(null)
|
|
35
|
+
const newValue = ref('')
|
|
36
|
+
|
|
37
|
+
async function loadConfigurations() {
|
|
38
|
+
loading.value = true
|
|
39
|
+
try {
|
|
40
|
+
const response = await client<{ data: EntityConfiguration[] }>('/api/v2/entity-configurations', {
|
|
41
|
+
query: {
|
|
42
|
+
configurable_type: props.configurableType,
|
|
43
|
+
configurable_id: props.configurableId,
|
|
44
|
+
per_page: 100
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
configurations.value = response.data ?? []
|
|
48
|
+
} catch {
|
|
49
|
+
configurations.value = []
|
|
50
|
+
} finally {
|
|
51
|
+
loading.value = false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadConfigVariables() {
|
|
56
|
+
try {
|
|
57
|
+
const response = await client<{ data: ConfigVariable[] }>('/api/v2/config-variables', {
|
|
58
|
+
query: { per_page: 200 }
|
|
59
|
+
})
|
|
60
|
+
configVariables.value = response.data ?? []
|
|
61
|
+
} catch {
|
|
62
|
+
configVariables.value = []
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const availableConfigVariables = computed(() => {
|
|
67
|
+
const usedIds = new Set(configurations.value.map(c => c.config_variable_id))
|
|
68
|
+
return configVariables.value
|
|
69
|
+
.filter(v => !usedIds.has(v.id))
|
|
70
|
+
.map(v => ({
|
|
71
|
+
label: `${v.package}.${v.group}.${v.name}`,
|
|
72
|
+
value: v.id
|
|
73
|
+
}))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const canAdd = computed(() => newConfigVariableId.value !== null)
|
|
77
|
+
|
|
78
|
+
async function addConfiguration() {
|
|
79
|
+
if (!canAdd.value) return
|
|
80
|
+
saving.value = true
|
|
81
|
+
try {
|
|
82
|
+
await client('/api/v2/entity-configurations', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body: {
|
|
85
|
+
configurable_type: props.configurableType,
|
|
86
|
+
configurable_id: props.configurableId,
|
|
87
|
+
config_variable_id: newConfigVariableId.value,
|
|
88
|
+
value: newValue.value || null
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
newConfigVariableId.value = null
|
|
92
|
+
newValue.value = ''
|
|
93
|
+
await loadConfigurations()
|
|
94
|
+
notifySuccess(
|
|
95
|
+
t('motor-admin.entity_configurations.title'),
|
|
96
|
+
t('motor-admin.entity_configurations.saved')
|
|
97
|
+
)
|
|
98
|
+
} catch (e) {
|
|
99
|
+
notifyError(
|
|
100
|
+
t('motor-admin.entity_configurations.title'),
|
|
101
|
+
(e as Error).message
|
|
102
|
+
)
|
|
103
|
+
} finally {
|
|
104
|
+
saving.value = false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function removeConfiguration(config: EntityConfiguration) {
|
|
109
|
+
removing.value = config.id
|
|
110
|
+
try {
|
|
111
|
+
await client(`/api/v2/entity-configurations/${config.id}`, {
|
|
112
|
+
method: 'DELETE'
|
|
113
|
+
})
|
|
114
|
+
await loadConfigurations()
|
|
115
|
+
notifySuccess(
|
|
116
|
+
t('motor-admin.entity_configurations.title'),
|
|
117
|
+
t('motor-admin.entity_configurations.removed')
|
|
118
|
+
)
|
|
119
|
+
} catch (e) {
|
|
120
|
+
notifyError(
|
|
121
|
+
t('motor-admin.entity_configurations.title'),
|
|
122
|
+
(e as Error).message
|
|
123
|
+
)
|
|
124
|
+
} finally {
|
|
125
|
+
removing.value = null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatVariableName(cv: ConfigVariable | undefined): string {
|
|
130
|
+
if (!cv) return '—'
|
|
131
|
+
return `${cv.package}.${cv.group}.${cv.name}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onMounted(() => {
|
|
135
|
+
loadConfigurations()
|
|
136
|
+
loadConfigVariables()
|
|
137
|
+
})
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<UCard>
|
|
142
|
+
<template #header>
|
|
143
|
+
<div class="flex items-center justify-between">
|
|
144
|
+
<h3 class="text-base font-semibold">
|
|
145
|
+
{{ t('motor-admin.entity_configurations.title') }}
|
|
146
|
+
</h3>
|
|
147
|
+
<span
|
|
148
|
+
v-if="configurations.length > 0"
|
|
149
|
+
class="text-xs text-[var(--ui-text-muted)]"
|
|
150
|
+
>
|
|
151
|
+
{{ configurations.length }}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
</template>
|
|
155
|
+
|
|
156
|
+
<div
|
|
157
|
+
v-if="loading"
|
|
158
|
+
class="flex items-center justify-center py-6"
|
|
159
|
+
>
|
|
160
|
+
<UIcon
|
|
161
|
+
name="i-lucide-loader-2"
|
|
162
|
+
class="size-5 animate-spin text-[var(--ui-text-muted)]"
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div v-else>
|
|
167
|
+
<!-- Existing configurations -->
|
|
168
|
+
<div
|
|
169
|
+
v-if="configurations.length > 0"
|
|
170
|
+
class="mb-4 space-y-2"
|
|
171
|
+
>
|
|
172
|
+
<div
|
|
173
|
+
v-for="config in configurations"
|
|
174
|
+
:key="config.id"
|
|
175
|
+
class="flex items-center gap-4 py-2.5 px-3 rounded-lg ring-1 ring-[var(--ui-border)] group"
|
|
176
|
+
>
|
|
177
|
+
<UIcon
|
|
178
|
+
name="i-lucide-link"
|
|
179
|
+
class="size-3.5 text-emerald-500 shrink-0"
|
|
180
|
+
/>
|
|
181
|
+
<div class="min-w-0 flex-1">
|
|
182
|
+
<p class="text-sm font-medium truncate">
|
|
183
|
+
{{ formatVariableName(config.config_variable) }}
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="shrink-0 max-w-xs">
|
|
187
|
+
<span class="text-sm text-[var(--ui-text-muted)] font-mono truncate">
|
|
188
|
+
{{ config.value ?? '—' }}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
<UButton
|
|
192
|
+
icon="i-lucide-unlink"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
color="error"
|
|
195
|
+
size="xs"
|
|
196
|
+
:loading="removing === config.id"
|
|
197
|
+
class="opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
|
198
|
+
@click="removeConfiguration(config)"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<p
|
|
204
|
+
v-else
|
|
205
|
+
class="text-sm text-[var(--ui-text-muted)] mb-4"
|
|
206
|
+
>
|
|
207
|
+
{{ t('motor-admin.entity_configurations.no_configurations') }}
|
|
208
|
+
</p>
|
|
209
|
+
|
|
210
|
+
<!-- Add new configuration -->
|
|
211
|
+
<div class="flex items-start gap-3">
|
|
212
|
+
<USelect
|
|
213
|
+
v-model="newConfigVariableId"
|
|
214
|
+
:items="availableConfigVariables"
|
|
215
|
+
:placeholder="t('motor-admin.entity_configurations.select_variable')"
|
|
216
|
+
class="flex-1"
|
|
217
|
+
/>
|
|
218
|
+
<UInput
|
|
219
|
+
v-model="newValue"
|
|
220
|
+
:placeholder="t('motor-admin.entity_configurations.value')"
|
|
221
|
+
class="flex-1"
|
|
222
|
+
@keydown.enter.prevent="addConfiguration"
|
|
223
|
+
/>
|
|
224
|
+
<UButton
|
|
225
|
+
icon="i-lucide-plus"
|
|
226
|
+
variant="outline"
|
|
227
|
+
:disabled="!canAdd"
|
|
228
|
+
:loading="saving"
|
|
229
|
+
class="shrink-0"
|
|
230
|
+
@click="addConfiguration"
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</UCard>
|
|
235
|
+
</template>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// app/composables/useClientFormExtensions.ts
|
|
2
|
+
import type { Component, Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ClientFormExtension {
|
|
5
|
+
key: string
|
|
6
|
+
order: number
|
|
7
|
+
component: Component
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const registry: ClientFormExtension[] = reactive([])
|
|
11
|
+
const validityState = reactive<Record<string, boolean>>({})
|
|
12
|
+
|
|
13
|
+
const callbackRefs = new Map<string, {
|
|
14
|
+
validate: Ref<() => boolean | Promise<boolean>>
|
|
15
|
+
getSubmitData: Ref<() => Record<string, unknown>>
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
function ensureCallbackRefs(key: string) {
|
|
19
|
+
if (!callbackRefs.has(key)) {
|
|
20
|
+
callbackRefs.set(key, {
|
|
21
|
+
validate: ref(() => true),
|
|
22
|
+
getSubmitData: ref(() => ({})),
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
return callbackRefs.get(key)!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useClientFormExtensions() {
|
|
29
|
+
function register(ext: ClientFormExtension) {
|
|
30
|
+
if (registry.some(e => e.key === ext.key)) return
|
|
31
|
+
registry.push(ext)
|
|
32
|
+
registry.sort((a, b) => a.order - b.order)
|
|
33
|
+
validityState[ext.key] = false
|
|
34
|
+
ensureCallbackRefs(ext.key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setValidity(key: string, isValid: boolean) {
|
|
38
|
+
validityState[key] = isValid
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function validateAll(): Promise<boolean> {
|
|
42
|
+
for (const ext of registry) {
|
|
43
|
+
const refs = callbackRefs.get(ext.key)
|
|
44
|
+
if (refs) {
|
|
45
|
+
const result = await refs.validate.value()
|
|
46
|
+
if (!result) return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getAllSubmitData(): Record<string, unknown> {
|
|
53
|
+
let merged: Record<string, unknown> = {}
|
|
54
|
+
for (const ext of registry) {
|
|
55
|
+
const refs = callbackRefs.get(ext.key)
|
|
56
|
+
if (refs) {
|
|
57
|
+
merged = { ...merged, ...refs.getSubmitData.value() }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return merged
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const extensions = computed(() => [...registry])
|
|
64
|
+
|
|
65
|
+
const allExtensionsValid = computed(() =>
|
|
66
|
+
registry.length === 0 || registry.every(ext => validityState[ext.key] === true)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return { extensions, register, setValidity, allExtensionsValid, validateAll, getAllSubmitData }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useExtensionCallbacks(key: string) {
|
|
73
|
+
const refs = ensureCallbackRefs(key)
|
|
74
|
+
|
|
75
|
+
function setValidate(fn: () => boolean | Promise<boolean>) {
|
|
76
|
+
refs.validate.value = fn
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setGetSubmitData(fn: () => Record<string, unknown>) {
|
|
80
|
+
refs.getSubmitData.value = fn
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onUnmounted(() => {
|
|
84
|
+
refs.validate.value = () => true
|
|
85
|
+
refs.getSubmitData.value = () => ({})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return { setValidate, setGetSubmitData }
|
|
89
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Ref, ComputedRef } from 'vue'
|
|
2
|
+
import type { components } from '@motor-cms/ui-core/app/types/generated/api'
|
|
3
|
+
|
|
4
|
+
type NavigationTreeResource = components['schemas']['NavigationTreeResource']
|
|
5
|
+
type LanguageResource = components['schemas']['LanguageResource']
|
|
6
|
+
|
|
7
|
+
interface PaginatedResponse<T> {
|
|
8
|
+
data: T[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ClientLanguage {
|
|
12
|
+
id: number
|
|
13
|
+
name: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseClientLanguagesReturn {
|
|
17
|
+
languages: Ref<ClientLanguage[]>
|
|
18
|
+
isMultiLanguage: ComputedRef<boolean>
|
|
19
|
+
loading: Ref<boolean>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useClientLanguages(clientId: Ref<string | number>): UseClientLanguagesReturn {
|
|
23
|
+
const client = useSanctumClient()
|
|
24
|
+
|
|
25
|
+
const languages = ref<ClientLanguage[]>([])
|
|
26
|
+
const loading = ref(false)
|
|
27
|
+
|
|
28
|
+
const isMultiLanguage = computed(() => languages.value.length > 1)
|
|
29
|
+
|
|
30
|
+
watch(
|
|
31
|
+
clientId,
|
|
32
|
+
async (id) => {
|
|
33
|
+
if (!id) {
|
|
34
|
+
languages.value = []
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
loading.value = true
|
|
39
|
+
try {
|
|
40
|
+
const treesResponse = await client<PaginatedResponse<NavigationTreeResource>>(
|
|
41
|
+
`/api/v2/navigation-trees?filter[client_id]=${id}&per_page=100`
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const trees = treesResponse.data ?? []
|
|
45
|
+
|
|
46
|
+
const distinctLanguageIds = [...new Set(
|
|
47
|
+
trees
|
|
48
|
+
.map((tree) => Number(tree.language_id))
|
|
49
|
+
.filter((langId) => !isNaN(langId) && langId > 0)
|
|
50
|
+
)]
|
|
51
|
+
|
|
52
|
+
if (distinctLanguageIds.length === 0) {
|
|
53
|
+
languages.value = []
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const languagesResponse = await client<PaginatedResponse<LanguageResource>>(
|
|
58
|
+
'/api/v2/languages?per_page=100'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const allLanguages = languagesResponse.data ?? []
|
|
62
|
+
|
|
63
|
+
languages.value = allLanguages
|
|
64
|
+
.filter((lang) => distinctLanguageIds.includes(lang.id))
|
|
65
|
+
.map((lang) => ({ id: lang.id, name: lang.english_name }))
|
|
66
|
+
.sort((a, b) => a.id - b.id)
|
|
67
|
+
} catch {
|
|
68
|
+
languages.value = []
|
|
69
|
+
} finally {
|
|
70
|
+
loading.value = false
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{ immediate: true }
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
languages,
|
|
78
|
+
isMultiLanguage,
|
|
79
|
+
loading,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export interface DashboardStats {
|
|
2
|
+
pages_total: number
|
|
3
|
+
pages_draft: number
|
|
4
|
+
pages_published: number
|
|
5
|
+
pages_scheduled: number
|
|
6
|
+
media_total: number
|
|
7
|
+
navigation_trees: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ActivityItem {
|
|
11
|
+
id: number
|
|
12
|
+
description: string
|
|
13
|
+
subject_type: string
|
|
14
|
+
subject_id: number
|
|
15
|
+
subject_name: string | null
|
|
16
|
+
subject_exists: boolean
|
|
17
|
+
causer_name: string | null
|
|
18
|
+
created_at: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PublishingQueueItem {
|
|
22
|
+
id: number
|
|
23
|
+
name: string
|
|
24
|
+
to_be_published_at: string
|
|
25
|
+
publishable_type: string
|
|
26
|
+
publishable_id: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AnnouncementItem {
|
|
30
|
+
id: number
|
|
31
|
+
title: string
|
|
32
|
+
body: string | null
|
|
33
|
+
type: 'info' | 'warning' | 'error'
|
|
34
|
+
audience: 'self' | 'users' | 'client'
|
|
35
|
+
linkable_type: string | null
|
|
36
|
+
linkable_id: number | null
|
|
37
|
+
linkable_name: string | null
|
|
38
|
+
linkable_url: string | null
|
|
39
|
+
created_by_name: string | null
|
|
40
|
+
starts_at: string | null
|
|
41
|
+
created_at: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ActivityMeta {
|
|
45
|
+
current_page: number
|
|
46
|
+
last_page: number
|
|
47
|
+
per_page: number
|
|
48
|
+
total: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DashboardResponse {
|
|
52
|
+
data: {
|
|
53
|
+
stats: DashboardStats
|
|
54
|
+
activity: ActivityItem[]
|
|
55
|
+
activity_meta: ActivityMeta
|
|
56
|
+
publishing_queue: PublishingQueueItem[]
|
|
57
|
+
announcements: AnnouncementItem[]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const NOTIFIED_STORAGE_KEY = 'dashboard-announcements-notified'
|
|
62
|
+
|
|
63
|
+
function getNotifiedIds(): Set<number> {
|
|
64
|
+
try {
|
|
65
|
+
const raw = localStorage.getItem(NOTIFIED_STORAGE_KEY)
|
|
66
|
+
return new Set(raw ? JSON.parse(raw) : [])
|
|
67
|
+
} catch {
|
|
68
|
+
return new Set()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function persistNotifiedIds(ids: Set<number>) {
|
|
73
|
+
localStorage.setItem(NOTIFIED_STORAGE_KEY, JSON.stringify([...ids]))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function useDashboardData() {
|
|
77
|
+
const client = useSanctumClient()
|
|
78
|
+
const { notify } = useNotify()
|
|
79
|
+
|
|
80
|
+
const stats = ref<DashboardStats>({
|
|
81
|
+
pages_total: 0,
|
|
82
|
+
pages_draft: 0,
|
|
83
|
+
pages_published: 0,
|
|
84
|
+
pages_scheduled: 0,
|
|
85
|
+
media_total: 0,
|
|
86
|
+
navigation_trees: 0,
|
|
87
|
+
})
|
|
88
|
+
const activity = ref<ActivityItem[]>([])
|
|
89
|
+
const activityMeta = ref<ActivityMeta>({ current_page: 1, last_page: 1, per_page: 10, total: 0 })
|
|
90
|
+
const activityLoadingMore = ref(false)
|
|
91
|
+
const publishingQueue = ref<PublishingQueueItem[]>([])
|
|
92
|
+
const announcements = ref<AnnouncementItem[]>([])
|
|
93
|
+
const loading = ref(true)
|
|
94
|
+
|
|
95
|
+
const hasMoreActivity = computed(() => activityMeta.value.current_page < activityMeta.value.last_page)
|
|
96
|
+
|
|
97
|
+
async function refresh() {
|
|
98
|
+
loading.value = true
|
|
99
|
+
try {
|
|
100
|
+
const response = await client<DashboardResponse>('/api/v2/dashboard')
|
|
101
|
+
stats.value = response.data.stats
|
|
102
|
+
activity.value = response.data.activity
|
|
103
|
+
activityMeta.value = response.data.activity_meta
|
|
104
|
+
publishingQueue.value = response.data.publishing_queue
|
|
105
|
+
announcements.value = response.data.announcements
|
|
106
|
+
|
|
107
|
+
const notified = getNotifiedIds()
|
|
108
|
+
for (const a of response.data.announcements) {
|
|
109
|
+
if (!notified.has(a.id)) {
|
|
110
|
+
notify({
|
|
111
|
+
title: a.title,
|
|
112
|
+
description: a.body ?? undefined,
|
|
113
|
+
color: a.type === 'error' ? 'error' : a.type === 'warning' ? 'warning' : 'info',
|
|
114
|
+
icon: a.type === 'error' ? 'i-lucide-alert-circle' : a.type === 'warning' ? 'i-lucide-alert-triangle' : 'i-lucide-megaphone',
|
|
115
|
+
})
|
|
116
|
+
notified.add(a.id)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
persistNotifiedIds(notified)
|
|
120
|
+
} finally {
|
|
121
|
+
loading.value = false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function loadMoreActivity() {
|
|
126
|
+
if (!hasMoreActivity.value || activityLoadingMore.value) return
|
|
127
|
+
activityLoadingMore.value = true
|
|
128
|
+
try {
|
|
129
|
+
const nextPage = activityMeta.value.current_page + 1
|
|
130
|
+
const response = await client<DashboardResponse>(`/api/v2/dashboard?activity_page=${nextPage}`)
|
|
131
|
+
activity.value.push(...response.data.activity)
|
|
132
|
+
activityMeta.value = response.data.activity_meta
|
|
133
|
+
} finally {
|
|
134
|
+
activityLoadingMore.value = false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function dismissAnnouncement(id: number) {
|
|
139
|
+
await client(`/api/v2/dashboard/announcements/${id}/dismiss`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
})
|
|
142
|
+
announcements.value = announcements.value.filter(a => a.id !== id)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function createAnnouncement(data: Record<string, unknown>) {
|
|
146
|
+
await client('/api/v2/dashboard/announcements', {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
body: data,
|
|
149
|
+
})
|
|
150
|
+
await refresh()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
refresh().catch(() => {})
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
stats,
|
|
157
|
+
activity,
|
|
158
|
+
activityMeta,
|
|
159
|
+
activityLoadingMore,
|
|
160
|
+
hasMoreActivity,
|
|
161
|
+
publishingQueue,
|
|
162
|
+
announcements,
|
|
163
|
+
loading,
|
|
164
|
+
refresh,
|
|
165
|
+
loadMoreActivity,
|
|
166
|
+
dismissAnnouncement,
|
|
167
|
+
createAnnouncement,
|
|
168
|
+
}
|
|
169
|
+
}
|