cinqcinqdev-seo 0.1.9 → 0.1.11
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/dist/module.d.mts +17 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -11
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin/ContentCalendar.d.vue.ts +3 -0
- package/dist/runtime/components/admin/ContentCalendar.vue +196 -0
- package/dist/runtime/components/admin/ContentCalendar.vue.d.ts +3 -0
- package/dist/runtime/components/admin/DashboardLayout.d.vue.ts +13 -0
- package/dist/runtime/components/admin/DashboardLayout.vue +12 -0
- package/dist/runtime/components/admin/DashboardLayout.vue.d.ts +13 -0
- package/dist/runtime/components/admin/NavItem.d.vue.ts +3 -0
- package/dist/runtime/components/admin/NavItem.vue +46 -0
- package/dist/runtime/components/admin/NavItem.vue.d.ts +3 -0
- package/dist/runtime/components/admin/Navbar.vue +1 -2
- package/dist/runtime/components/admin/Sidebar.d.vue.ts +3 -0
- package/dist/runtime/components/admin/Sidebar.vue +115 -0
- package/dist/runtime/components/admin/Sidebar.vue.d.ts +3 -0
- package/dist/runtime/composables/useAdminBranding.d.ts +4 -0
- package/dist/runtime/composables/useAdminBranding.js +22 -0
- package/dist/runtime/composables/useAdminContent.d.ts +9 -0
- package/dist/runtime/composables/useAdminContent.js +46 -0
- package/dist/runtime/composables/useAdminPersonas.d.ts +8 -0
- package/dist/runtime/composables/useAdminPersonas.js +31 -0
- package/dist/runtime/composables/useAdminProducts.d.ts +6 -0
- package/dist/runtime/composables/useAdminProducts.js +24 -0
- package/dist/runtime/composables/useAdminStrategies.d.ts +10 -0
- package/dist/runtime/composables/useAdminStrategies.js +57 -0
- package/dist/runtime/composables/useAdminTodos.d.ts +10 -0
- package/dist/runtime/composables/useAdminTodos.js +55 -0
- package/dist/runtime/pages/admin/account.vue +59 -51
- package/dist/runtime/pages/admin/branding.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/branding.vue +276 -0
- package/dist/runtime/pages/admin/branding.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/calendar.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/calendar.vue +169 -0
- package/dist/runtime/pages/admin/calendar.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/content-library.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/content-library.vue +359 -0
- package/dist/runtime/pages/admin/content-library.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/editor/[id].vue +5 -3
- package/dist/runtime/pages/admin/index.vue +81 -23
- package/dist/runtime/pages/admin/pages/[type].vue +267 -83
- package/dist/runtime/pages/admin/personas/[id].d.vue.ts +3 -0
- package/dist/runtime/pages/admin/personas/[id].vue +170 -0
- package/dist/runtime/pages/admin/personas/[id].vue.d.ts +3 -0
- package/dist/runtime/pages/admin/personas/index.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/personas/index.vue +266 -0
- package/dist/runtime/pages/admin/personas/index.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/products.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/products.vue +300 -0
- package/dist/runtime/pages/admin/products.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/strategies/[id].d.vue.ts +3 -0
- package/dist/runtime/pages/admin/strategies/[id].vue +237 -0
- package/dist/runtime/pages/admin/strategies/[id].vue.d.ts +3 -0
- package/dist/runtime/pages/admin/strategies/index.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/strategies/index.vue +232 -0
- package/dist/runtime/pages/admin/strategies/index.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/todos.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/todos.vue +251 -0
- package/dist/runtime/pages/admin/todos.vue.d.ts +3 -0
- package/dist/runtime/stores/adminUser.d.ts +29 -1
- package/dist/runtime/stores/adminUser.js +2 -2
- package/dist/types.d.mts +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, onMounted, useHead, useRoute, navigateTo } from "#imports";
|
|
3
|
+
import { useAdminStrategies, useAdminContent, useAdminPersonas } from "#imports";
|
|
4
|
+
const route = useRoute();
|
|
5
|
+
const id = route.params.id;
|
|
6
|
+
useHead({ title: "Strategie - Admin" });
|
|
7
|
+
const { fetchStrategyById, updateStrategy, fetchStrategyContents, fetchStrategyPersonas } = useAdminStrategies();
|
|
8
|
+
const { fetchContent, createContent } = useAdminContent();
|
|
9
|
+
const { fetchPersonas } = useAdminPersonas();
|
|
10
|
+
const strategy = ref(null);
|
|
11
|
+
const contents = ref([]);
|
|
12
|
+
const linkedPersonas = ref([]);
|
|
13
|
+
const allContents = ref([]);
|
|
14
|
+
const allPersonas = ref([]);
|
|
15
|
+
const loading = ref(true);
|
|
16
|
+
const editMode = ref(false);
|
|
17
|
+
const saving = ref(false);
|
|
18
|
+
const formData = ref({});
|
|
19
|
+
const activeTab = ref("overview");
|
|
20
|
+
onMounted(async () => {
|
|
21
|
+
loading.value = true;
|
|
22
|
+
try {
|
|
23
|
+
const [s, c, p, ac, ap] = await Promise.all([
|
|
24
|
+
fetchStrategyById(id),
|
|
25
|
+
fetchStrategyContents(id),
|
|
26
|
+
fetchStrategyPersonas(id),
|
|
27
|
+
fetchContent(),
|
|
28
|
+
fetchPersonas()
|
|
29
|
+
]);
|
|
30
|
+
strategy.value = s;
|
|
31
|
+
contents.value = c || [];
|
|
32
|
+
linkedPersonas.value = p || [];
|
|
33
|
+
allContents.value = ac || [];
|
|
34
|
+
allPersonas.value = ap || [];
|
|
35
|
+
formData.value = { ...s };
|
|
36
|
+
} catch {
|
|
37
|
+
} finally {
|
|
38
|
+
loading.value = false;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
const saveChanges = async () => {
|
|
42
|
+
saving.value = true;
|
|
43
|
+
try {
|
|
44
|
+
const updated = await updateStrategy(id, formData.value);
|
|
45
|
+
strategy.value = updated;
|
|
46
|
+
editMode.value = false;
|
|
47
|
+
} catch {
|
|
48
|
+
} finally {
|
|
49
|
+
saving.value = false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const statusClass = (s) => ({
|
|
53
|
+
draft: "bg-slate-100 text-slate-600",
|
|
54
|
+
active: "bg-emerald-100 text-emerald-700",
|
|
55
|
+
paused: "bg-amber-100 text-amber-700",
|
|
56
|
+
completed: "bg-blue-100 text-blue-700"
|
|
57
|
+
})[s] || "bg-slate-100 text-slate-600";
|
|
58
|
+
const priorityClass = (p) => ({
|
|
59
|
+
Critique: "bg-red-100 text-red-700",
|
|
60
|
+
Haute: "bg-orange-100 text-orange-700",
|
|
61
|
+
Moyenne: "bg-blue-100 text-blue-700",
|
|
62
|
+
Basse: "bg-slate-100 text-slate-600"
|
|
63
|
+
})[p] || "bg-slate-100 text-slate-600";
|
|
64
|
+
const formatDate = (d) => d ? new Date(d).toLocaleDateString("fr-FR") : "-";
|
|
65
|
+
const formatBudget = (n) => n ? new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(n) : "-";
|
|
66
|
+
const contentStatusClass = {
|
|
67
|
+
draft: "bg-slate-100 text-slate-500",
|
|
68
|
+
approved: "bg-blue-100 text-blue-600",
|
|
69
|
+
published: "bg-emerald-100 text-emerald-600",
|
|
70
|
+
archived: "bg-amber-100 text-amber-600"
|
|
71
|
+
};
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<AdminDashboardLayout>
|
|
76
|
+
<div class="max-w-6xl mx-auto px-6 md:px-10 py-10">
|
|
77
|
+
<NuxtLink to="/admin/strategies" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 mb-8 font-medium">
|
|
78
|
+
<Icon name="mdi:arrow-left" />
|
|
79
|
+
Retour aux strategies
|
|
80
|
+
</NuxtLink>
|
|
81
|
+
|
|
82
|
+
<div v-if="loading" class="flex items-center justify-center py-20">
|
|
83
|
+
<Icon name="mdi:loading" class="text-5xl text-blue-600 animate-spin" />
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div v-else-if="strategy">
|
|
87
|
+
<!-- Header -->
|
|
88
|
+
<div class="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm mb-6">
|
|
89
|
+
<div class="flex items-start justify-between">
|
|
90
|
+
<div class="flex-1">
|
|
91
|
+
<div class="flex items-center gap-3 mb-2">
|
|
92
|
+
<h1 class="text-3xl font-bold text-slate-900">{{ strategy.name }}</h1>
|
|
93
|
+
<span :class="['px-3 py-1 rounded-full text-xs font-semibold', statusClass(strategy.status)]">{{ strategy.status }}</span>
|
|
94
|
+
<span v-if="strategy.priority" :class="['px-3 py-1 rounded-full text-xs font-semibold', priorityClass(strategy.priority)]">{{ strategy.priority }}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<p class="text-slate-600 mb-4">{{ strategy.description }}</p>
|
|
97
|
+
<div class="flex flex-wrap items-center gap-6 text-sm text-slate-500">
|
|
98
|
+
<span v-if="strategy.start_date">{{ formatDate(strategy.start_date) }} — {{ formatDate(strategy.end_date) }}</span>
|
|
99
|
+
<span v-if="strategy.budget" class="font-semibold text-emerald-600">{{ formatBudget(strategy.budget) }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div v-if="strategy.completion_percentage !== void 0" class="mt-4 max-w-sm">
|
|
102
|
+
<div class="flex justify-between mb-1 text-xs">
|
|
103
|
+
<span class="text-slate-500">Progression</span>
|
|
104
|
+
<span class="font-semibold text-blue-600">{{ strategy.completion_percentage }}%</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
|
|
107
|
+
<div :style="{ width: `${strategy.completion_percentage || 0}%` }" class="h-full bg-gradient-to-r from-blue-500 to-pink-500 transition-all"></div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<button @click="editMode = !editMode" class="p-2.5 bg-slate-100 text-slate-600 rounded-xl hover:bg-slate-200 transition-all">
|
|
112
|
+
<Icon name="mdi:pencil" />
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Edit Form -->
|
|
118
|
+
<div v-if="editMode" class="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm mb-6">
|
|
119
|
+
<div class="grid grid-cols-2 gap-6 mb-6">
|
|
120
|
+
<div>
|
|
121
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Nom</label>
|
|
122
|
+
<input v-model="formData.name" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Statut</label>
|
|
126
|
+
<select v-model="formData.status" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl">
|
|
127
|
+
<option v-for="s in ['draft', 'active', 'paused', 'completed', 'cancelled']" :key="s" :value="s">{{ s }}</option>
|
|
128
|
+
</select>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="col-span-2">
|
|
131
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Description</label>
|
|
132
|
+
<textarea v-model="formData.description" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl resize-none"></textarea>
|
|
133
|
+
</div>
|
|
134
|
+
<div>
|
|
135
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Progression (%)</label>
|
|
136
|
+
<input v-model.number="formData.completion_percentage" type="number" min="0" max="100" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
137
|
+
</div>
|
|
138
|
+
<div>
|
|
139
|
+
<label class="block text-xs font-black uppercase text-slate-500 mb-2">Budget</label>
|
|
140
|
+
<input v-model.number="formData.budget" type="number" step="0.01" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="flex gap-3">
|
|
144
|
+
<button @click="saveChanges" :disabled="saving" class="px-8 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 disabled:opacity-50">{{ saving ? "Enregistrement..." : "Enregistrer" }}</button>
|
|
145
|
+
<button @click="editMode = false" class="px-8 py-3 bg-slate-100 text-slate-700 font-bold rounded-xl hover:bg-slate-200">Annuler</button>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Tabs -->
|
|
150
|
+
<div class="flex gap-2 bg-white p-2 rounded-2xl border border-slate-200 shadow-sm mb-6 inline-flex">
|
|
151
|
+
<button v-for="tab in [{ v: 'overview', l: 'Apercu' }, { v: 'contents', l: 'Contenus' }, { v: 'personas', l: 'Personas' }]" :key="tab.v"
|
|
152
|
+
@click="activeTab = tab.v"
|
|
153
|
+
:class="[activeTab === tab.v ? 'bg-blue-600 text-white shadow-lg' : 'text-slate-600 hover:bg-slate-50']"
|
|
154
|
+
class="px-5 py-2.5 rounded-xl font-semibold text-sm transition-all">{{ tab.l }}</button>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Overview Tab -->
|
|
158
|
+
<div v-if="activeTab === 'overview'" class="grid grid-cols-2 gap-6">
|
|
159
|
+
<div class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
160
|
+
<h3 class="font-black text-slate-900 mb-4 flex items-center gap-2"><Icon name="mdi:target" class="text-blue-600" /> Objectif</h3>
|
|
161
|
+
<p class="text-slate-600 leading-relaxed">{{ strategy.objective || "Non defini." }}</p>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
|
|
164
|
+
<h3 class="font-black text-slate-900 mb-4">Statistiques</h3>
|
|
165
|
+
<div class="space-y-3">
|
|
166
|
+
<div class="flex justify-between items-center">
|
|
167
|
+
<span class="text-sm text-slate-500">Contenus</span>
|
|
168
|
+
<span class="font-bold text-slate-900">{{ contents.length }}</span>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="flex justify-between items-center">
|
|
171
|
+
<span class="text-sm text-slate-500">Personas</span>
|
|
172
|
+
<span class="font-bold text-slate-900">{{ linkedPersonas.length }}</span>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="flex justify-between items-center">
|
|
175
|
+
<span class="text-sm text-slate-500">Publies</span>
|
|
176
|
+
<span class="font-bold text-emerald-600">{{ contents.filter((c) => c.content?.status === "published").length }}</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Contents Tab -->
|
|
183
|
+
<div v-if="activeTab === 'contents'" class="space-y-4">
|
|
184
|
+
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
185
|
+
<div class="p-6 border-b border-slate-100">
|
|
186
|
+
<h3 class="font-black text-slate-900">Contenus de la Strategie</h3>
|
|
187
|
+
</div>
|
|
188
|
+
<div v-if="contents.length === 0" class="text-center py-12 text-slate-300">
|
|
189
|
+
<Icon name="mdi:file-document-outline" class="text-5xl mb-2" />
|
|
190
|
+
<p class="text-sm">Aucun contenu lie a cette strategie</p>
|
|
191
|
+
</div>
|
|
192
|
+
<div v-else class="divide-y divide-slate-100">
|
|
193
|
+
<div v-for="item in contents" :key="item.id" class="p-4 hover:bg-slate-50 transition-colors">
|
|
194
|
+
<div class="flex items-center gap-4">
|
|
195
|
+
<div class="flex-1">
|
|
196
|
+
<p class="font-bold text-slate-900">{{ item.content?.title || "Sans titre" }}</p>
|
|
197
|
+
<p class="text-xs text-slate-500">{{ item.content?.platform }} • Position: {{ item.position }}</p>
|
|
198
|
+
</div>
|
|
199
|
+
<span :class="['px-2 py-1 rounded-lg text-[10px] font-black uppercase', contentStatusClass[item.content?.status] || contentStatusClass.draft]">{{ item.content?.status }}</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<!-- Personas Tab -->
|
|
207
|
+
<div v-if="activeTab === 'personas'" class="space-y-4">
|
|
208
|
+
<div class="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
|
|
209
|
+
<div class="p-6 border-b border-slate-100">
|
|
210
|
+
<h3 class="font-black text-slate-900">Personas Cibles</h3>
|
|
211
|
+
</div>
|
|
212
|
+
<div v-if="linkedPersonas.length === 0" class="text-center py-12 text-slate-300">
|
|
213
|
+
<Icon name="mdi:account-group" class="text-5xl mb-2" />
|
|
214
|
+
<p class="text-sm">Aucun persona lie a cette strategie</p>
|
|
215
|
+
</div>
|
|
216
|
+
<div v-else class="divide-y divide-slate-100">
|
|
217
|
+
<div v-for="item in linkedPersonas" :key="item.id" class="p-4 hover:bg-slate-50 transition-colors flex items-center gap-4">
|
|
218
|
+
<div class="w-12 h-12 bg-gradient-to-br from-purple-400 to-pink-400 rounded-xl flex items-center justify-center text-white font-black">
|
|
219
|
+
{{ item.persona?.name ? item.persona.name.charAt(0) : "?" }}
|
|
220
|
+
</div>
|
|
221
|
+
<div>
|
|
222
|
+
<p class="font-bold text-slate-900">{{ item.persona?.name }}</p>
|
|
223
|
+
<p class="text-xs text-slate-500">{{ item.persona?.job_title }}</p>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div v-else class="text-center py-20">
|
|
232
|
+
<Icon name="mdi:strategy" class="text-6xl text-slate-200 mb-4" />
|
|
233
|
+
<p class="text-slate-400">Strategie introuvable.</p>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</AdminDashboardLayout>
|
|
237
|
+
</template>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, onMounted, useHead, navigateTo } from "#imports";
|
|
3
|
+
import { useAdminStrategies } from "#imports";
|
|
4
|
+
useHead({ title: "Strategies - Admin" });
|
|
5
|
+
const { fetchStrategies, createStrategy, updateStrategy, deleteStrategy, fetchStrategyContents, fetchStrategyPersonas } = useAdminStrategies();
|
|
6
|
+
const strategies = ref([]);
|
|
7
|
+
const strategyContents = ref({});
|
|
8
|
+
const strategyPersonas = ref({});
|
|
9
|
+
const loading = ref(true);
|
|
10
|
+
const showModal = ref(false);
|
|
11
|
+
const saving = ref(false);
|
|
12
|
+
const editingStrategy = ref(null);
|
|
13
|
+
const defaultForm = () => ({
|
|
14
|
+
name: "",
|
|
15
|
+
description: "",
|
|
16
|
+
objective: "",
|
|
17
|
+
start_date: "",
|
|
18
|
+
end_date: "",
|
|
19
|
+
budget: 0,
|
|
20
|
+
status: "draft",
|
|
21
|
+
priority: "Moyenne",
|
|
22
|
+
completion_percentage: 0
|
|
23
|
+
});
|
|
24
|
+
const formData = ref(defaultForm());
|
|
25
|
+
onMounted(async () => {
|
|
26
|
+
loading.value = true;
|
|
27
|
+
try {
|
|
28
|
+
strategies.value = await fetchStrategies() || [];
|
|
29
|
+
for (const s of strategies.value) {
|
|
30
|
+
const [c, p] = await Promise.all([fetchStrategyContents(s.id), fetchStrategyPersonas(s.id)]);
|
|
31
|
+
strategyContents.value[s.id] = c;
|
|
32
|
+
strategyPersonas.value[s.id] = p;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
} finally {
|
|
36
|
+
loading.value = false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const stats = computed(() => [
|
|
40
|
+
{ label: "Total", value: strategies.value.length, icon: "mdi:strategy", bg: "bg-blue-50", color: "text-blue-600" },
|
|
41
|
+
{ label: "Actives", value: strategies.value.filter((s) => s.status === "active").length, icon: "mdi:rocket-launch", bg: "bg-emerald-50", color: "text-emerald-600" },
|
|
42
|
+
{ label: "Completees", value: strategies.value.filter((s) => s.status === "completed").length, icon: "mdi:check-circle", bg: "bg-blue-50", color: "text-blue-600" },
|
|
43
|
+
{ label: "En pause", value: strategies.value.filter((s) => s.status === "paused").length, icon: "mdi:pause-circle", bg: "bg-amber-50", color: "text-amber-600" }
|
|
44
|
+
]);
|
|
45
|
+
const statusClass = (s) => ({
|
|
46
|
+
draft: "bg-slate-100 text-slate-600",
|
|
47
|
+
active: "bg-emerald-100 text-emerald-700",
|
|
48
|
+
paused: "bg-amber-100 text-amber-700",
|
|
49
|
+
completed: "bg-blue-100 text-blue-700",
|
|
50
|
+
cancelled: "bg-red-100 text-red-700"
|
|
51
|
+
})[s] || "bg-slate-100 text-slate-600";
|
|
52
|
+
const formatDate = (d) => d ? new Date(d).toLocaleDateString("fr-FR") : "-";
|
|
53
|
+
const formatBudget = (n) => n ? new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(n) : "-";
|
|
54
|
+
const openCreate = () => {
|
|
55
|
+
editingStrategy.value = null;
|
|
56
|
+
formData.value = defaultForm();
|
|
57
|
+
showModal.value = true;
|
|
58
|
+
};
|
|
59
|
+
const openEdit = (s) => {
|
|
60
|
+
editingStrategy.value = s;
|
|
61
|
+
formData.value = { ...s };
|
|
62
|
+
showModal.value = true;
|
|
63
|
+
};
|
|
64
|
+
const closeModal = () => {
|
|
65
|
+
showModal.value = false;
|
|
66
|
+
editingStrategy.value = null;
|
|
67
|
+
};
|
|
68
|
+
const saveStrategy = async () => {
|
|
69
|
+
if (!formData.value.name) return;
|
|
70
|
+
saving.value = true;
|
|
71
|
+
try {
|
|
72
|
+
if (editingStrategy.value) await updateStrategy(editingStrategy.value.id, formData.value);
|
|
73
|
+
else await createStrategy(formData.value);
|
|
74
|
+
strategies.value = await fetchStrategies();
|
|
75
|
+
closeModal();
|
|
76
|
+
} catch {
|
|
77
|
+
alert("Erreur lors de la sauvegarde");
|
|
78
|
+
} finally {
|
|
79
|
+
saving.value = false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const handleDelete = async () => {
|
|
83
|
+
if (!confirm("Supprimer cette strategie ?")) return;
|
|
84
|
+
try {
|
|
85
|
+
await deleteStrategy(editingStrategy.value.id);
|
|
86
|
+
strategies.value = await fetchStrategies();
|
|
87
|
+
closeModal();
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<template>
|
|
94
|
+
<AdminDashboardLayout>
|
|
95
|
+
<div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
|
|
96
|
+
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
|
97
|
+
<div>
|
|
98
|
+
<h1 class="text-4xl font-bold text-slate-900">Strategies <span class="text-blue-600">Marketing</span></h1>
|
|
99
|
+
<p class="text-slate-500 mt-1">Gerez vos campagnes avec contenus et personas</p>
|
|
100
|
+
</div>
|
|
101
|
+
<button @click="openCreate" class="bg-blue-600 text-white px-8 py-4 rounded-2xl font-bold text-sm hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center gap-2">
|
|
102
|
+
<Icon name="mdi:plus-circle" class="text-xl" /> Nouvelle Strategie
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
|
|
107
|
+
<div v-for="stat in stats" :key="stat.label" class="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
|
|
108
|
+
<div :class="[stat.bg, 'w-10 h-10 rounded-xl flex items-center justify-center mb-4']">
|
|
109
|
+
<Icon :name="stat.icon" :class="[stat.color, 'text-xl']" />
|
|
110
|
+
</div>
|
|
111
|
+
<p class="text-xs font-semibold text-slate-400 uppercase">{{ stat.label }}</p>
|
|
112
|
+
<p class="text-2xl font-bold text-slate-900">{{ stat.value }}</p>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="space-y-6">
|
|
117
|
+
<div v-for="strategy in strategies" :key="strategy.id" class="bg-white rounded-[2rem] border border-slate-200 shadow-sm overflow-hidden hover:shadow-xl transition-all">
|
|
118
|
+
<div class="p-6 border-b border-slate-100">
|
|
119
|
+
<div class="flex items-start justify-between mb-4">
|
|
120
|
+
<div class="flex-1">
|
|
121
|
+
<div class="flex items-center gap-3 mb-2">
|
|
122
|
+
<h3 class="text-xl font-bold text-slate-900">{{ strategy.name }}</h3>
|
|
123
|
+
<span :class="['px-3 py-1 rounded-full text-xs font-semibold', statusClass(strategy.status)]">{{ strategy.status }}</span>
|
|
124
|
+
</div>
|
|
125
|
+
<p class="text-sm text-slate-600">{{ strategy.description }}</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="flex gap-2 ml-4">
|
|
128
|
+
<button @click="navigateTo(`/admin/strategies/${strategy.id}`)" class="px-5 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-semibold hover:bg-blue-700 transition-all">Gerer</button>
|
|
129
|
+
<button @click="openEdit(strategy)" class="p-2.5 bg-slate-100 text-slate-600 rounded-xl hover:bg-slate-200 transition-all"><Icon name="mdi:pencil" /></button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="mb-4">
|
|
133
|
+
<div class="flex justify-between mb-1.5 text-xs">
|
|
134
|
+
<span class="font-medium text-slate-500">Progression</span>
|
|
135
|
+
<span class="font-semibold text-blue-600">{{ strategy.completion_percentage || 0 }}%</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
|
|
138
|
+
<div :style="{ width: `${strategy.completion_percentage || 0}%` }" class="h-full bg-gradient-to-r from-blue-500 to-pink-500 transition-all duration-500"></div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="flex items-center gap-4 text-xs text-slate-500">
|
|
142
|
+
<span v-if="strategy.start_date">{{ formatDate(strategy.start_date) }}</span>
|
|
143
|
+
<span v-if="strategy.budget">{{ formatBudget(strategy.budget) }}</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="grid grid-cols-2 gap-px bg-slate-100">
|
|
147
|
+
<div class="bg-white p-4 text-center">
|
|
148
|
+
<p class="text-lg font-bold text-blue-600">{{ strategyContents[strategy.id]?.length || 0 }}</p>
|
|
149
|
+
<p class="text-xs font-medium text-slate-400">Contenus</p>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="bg-white p-4 text-center">
|
|
152
|
+
<p class="text-lg font-bold text-blue-600">{{ strategyPersonas[strategy.id]?.length || 0 }}</p>
|
|
153
|
+
<p class="text-xs font-medium text-slate-400">Personas</p>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div v-if="!loading && strategies.length === 0" class="text-center py-20 bg-white rounded-3xl border border-slate-200">
|
|
159
|
+
<Icon name="mdi:strategy" class="text-6xl text-slate-200 mb-4" />
|
|
160
|
+
<p class="text-slate-400 font-medium mb-4">Aucune strategie. Creez-en une pour commencer!</p>
|
|
161
|
+
<button @click="openCreate" class="px-6 py-3 bg-blue-600 text-white rounded-xl font-bold">Creer ma premiere strategie</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<Teleport to="body">
|
|
167
|
+
<div v-if="showModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-md z-50 flex items-center justify-center p-6" @click.self="closeModal">
|
|
168
|
+
<div class="bg-white rounded-[3rem] max-w-3xl w-full max-h-[90vh] overflow-hidden shadow-2xl">
|
|
169
|
+
<div class="bg-blue-600 p-8 text-white flex justify-between items-center">
|
|
170
|
+
<h2 class="text-2xl font-bold">{{ editingStrategy ? "Modifier Strategie" : "Nouvelle Strategie" }}</h2>
|
|
171
|
+
<button @click="closeModal" class="w-12 h-12 bg-white/10 hover:bg-white/20 rounded-2xl flex items-center justify-center">
|
|
172
|
+
<Icon name="mdi:close" class="text-2xl" />
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="p-10 overflow-y-auto max-h-[calc(90vh-200px)] space-y-6">
|
|
176
|
+
<div>
|
|
177
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Nom *</label>
|
|
178
|
+
<input v-model="formData.name" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-medium" placeholder="Ex: Campagne Instagram Q1 2024" />
|
|
179
|
+
</div>
|
|
180
|
+
<div>
|
|
181
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Description</label>
|
|
182
|
+
<textarea v-model="formData.description" rows="3" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-medium" placeholder="Decrivez votre strategie..."></textarea>
|
|
183
|
+
</div>
|
|
184
|
+
<div>
|
|
185
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Objectif</label>
|
|
186
|
+
<textarea v-model="formData.objective" rows="2" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-medium" placeholder="Quel est l'objectif principal?"></textarea>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="grid grid-cols-2 gap-4">
|
|
189
|
+
<div>
|
|
190
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Date debut</label>
|
|
191
|
+
<input v-model="formData.start_date" type="date" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-bold" />
|
|
192
|
+
</div>
|
|
193
|
+
<div>
|
|
194
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Date fin</label>
|
|
195
|
+
<input v-model="formData.end_date" type="date" class="w-full px-6 py-4 bg-slate-50 border-none rounded-2xl font-bold" />
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
<div>
|
|
199
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Budget (EUR)</label>
|
|
200
|
+
<input v-model.number="formData.budget" type="number" step="0.01" class="w-full px-6 py-4 bg-emerald-50 border-none rounded-2xl font-bold text-emerald-900" />
|
|
201
|
+
</div>
|
|
202
|
+
<div>
|
|
203
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Statut</label>
|
|
204
|
+
<div class="grid grid-cols-4 gap-2">
|
|
205
|
+
<button v-for="s in ['draft', 'active', 'paused', 'completed']" :key="s" type="button" @click="formData.status = s"
|
|
206
|
+
:class="[formData.status === s ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-500']"
|
|
207
|
+
class="py-3 rounded-xl text-xs font-black uppercase transition-all">{{ s }}</button>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div>
|
|
211
|
+
<label class="block text-xs font-black uppercase text-slate-400 mb-2">Priorite</label>
|
|
212
|
+
<div class="grid grid-cols-4 gap-2">
|
|
213
|
+
<button v-for="p in ['Basse', 'Moyenne', 'Haute', 'Critique']" :key="p" type="button" @click="formData.priority = p"
|
|
214
|
+
:class="[formData.priority === p ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-500']"
|
|
215
|
+
class="py-3 rounded-xl text-xs font-black uppercase">{{ p }}</button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="p-8 bg-slate-50 border-t flex justify-between">
|
|
220
|
+
<button v-if="editingStrategy" @click="handleDelete" class="px-6 py-3 bg-red-100 text-red-600 rounded-2xl font-semibold text-sm">Supprimer</button>
|
|
221
|
+
<div class="flex gap-4 ml-auto">
|
|
222
|
+
<button @click="closeModal" class="px-8 py-4 rounded-2xl font-medium text-sm text-slate-500 hover:bg-slate-200">Annuler</button>
|
|
223
|
+
<button @click="saveStrategy" :disabled="saving" class="bg-blue-600 text-white px-10 py-4 rounded-2xl font-semibold text-sm shadow-xl disabled:opacity-50">
|
|
224
|
+
{{ saving ? "Enregistrement..." : "Enregistrer" }}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</Teleport>
|
|
231
|
+
</AdminDashboardLayout>
|
|
232
|
+
</template>
|