cinqcinqdev-seo 0.1.8 → 0.1.10

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 (68) hide show
  1. package/dist/module.d.mts +17 -1
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +19 -11
  4. package/dist/runtime/assets/admin-tw.css +1 -1
  5. package/dist/runtime/components/admin/ContentCalendar.d.vue.ts +3 -0
  6. package/dist/runtime/components/admin/ContentCalendar.vue +196 -0
  7. package/dist/runtime/components/admin/ContentCalendar.vue.d.ts +3 -0
  8. package/dist/runtime/components/admin/DashboardLayout.d.vue.ts +13 -0
  9. package/dist/runtime/components/admin/DashboardLayout.vue +12 -0
  10. package/dist/runtime/components/admin/DashboardLayout.vue.d.ts +13 -0
  11. package/dist/runtime/components/admin/NavItem.d.vue.ts +3 -0
  12. package/dist/runtime/components/admin/NavItem.vue +46 -0
  13. package/dist/runtime/components/admin/NavItem.vue.d.ts +3 -0
  14. package/dist/runtime/components/admin/Navbar.vue +1 -1
  15. package/dist/runtime/components/admin/Sidebar.d.vue.ts +3 -0
  16. package/dist/runtime/components/admin/Sidebar.vue +115 -0
  17. package/dist/runtime/components/admin/Sidebar.vue.d.ts +3 -0
  18. package/dist/runtime/composables/useAdminBranding.d.ts +4 -0
  19. package/dist/runtime/composables/useAdminBranding.js +22 -0
  20. package/dist/runtime/composables/useAdminContent.d.ts +9 -0
  21. package/dist/runtime/composables/useAdminContent.js +46 -0
  22. package/dist/runtime/composables/useAdminPersonas.d.ts +8 -0
  23. package/dist/runtime/composables/useAdminPersonas.js +31 -0
  24. package/dist/runtime/composables/useAdminProducts.d.ts +6 -0
  25. package/dist/runtime/composables/useAdminProducts.js +24 -0
  26. package/dist/runtime/composables/useAdminSections.js +1 -0
  27. package/dist/runtime/composables/useAdminStrategies.d.ts +10 -0
  28. package/dist/runtime/composables/useAdminStrategies.js +57 -0
  29. package/dist/runtime/composables/useAdminTodos.d.ts +10 -0
  30. package/dist/runtime/composables/useAdminTodos.js +55 -0
  31. package/dist/runtime/middleware/admin-auth.js +2 -0
  32. package/dist/runtime/pages/admin/account.vue +1 -0
  33. package/dist/runtime/pages/admin/branding.d.vue.ts +3 -0
  34. package/dist/runtime/pages/admin/branding.vue +276 -0
  35. package/dist/runtime/pages/admin/branding.vue.d.ts +3 -0
  36. package/dist/runtime/pages/admin/calendar.d.vue.ts +3 -0
  37. package/dist/runtime/pages/admin/calendar.vue +169 -0
  38. package/dist/runtime/pages/admin/calendar.vue.d.ts +3 -0
  39. package/dist/runtime/pages/admin/content-library.d.vue.ts +3 -0
  40. package/dist/runtime/pages/admin/content-library.vue +359 -0
  41. package/dist/runtime/pages/admin/content-library.vue.d.ts +3 -0
  42. package/dist/runtime/pages/admin/editor/[id].vue +5 -2
  43. package/dist/runtime/pages/admin/index.vue +81 -22
  44. package/dist/runtime/pages/admin/pages/[type].vue +1 -0
  45. package/dist/runtime/pages/admin/personas/[id].d.vue.ts +3 -0
  46. package/dist/runtime/pages/admin/personas/[id].vue +170 -0
  47. package/dist/runtime/pages/admin/personas/[id].vue.d.ts +3 -0
  48. package/dist/runtime/pages/admin/personas/index.d.vue.ts +3 -0
  49. package/dist/runtime/pages/admin/personas/index.vue +266 -0
  50. package/dist/runtime/pages/admin/personas/index.vue.d.ts +3 -0
  51. package/dist/runtime/pages/admin/products.d.vue.ts +3 -0
  52. package/dist/runtime/pages/admin/products.vue +300 -0
  53. package/dist/runtime/pages/admin/products.vue.d.ts +3 -0
  54. package/dist/runtime/pages/admin/strategies/[id].d.vue.ts +3 -0
  55. package/dist/runtime/pages/admin/strategies/[id].vue +237 -0
  56. package/dist/runtime/pages/admin/strategies/[id].vue.d.ts +3 -0
  57. package/dist/runtime/pages/admin/strategies/index.d.vue.ts +3 -0
  58. package/dist/runtime/pages/admin/strategies/index.vue +232 -0
  59. package/dist/runtime/pages/admin/strategies/index.vue.d.ts +3 -0
  60. package/dist/runtime/pages/admin/todos.d.vue.ts +3 -0
  61. package/dist/runtime/pages/admin/todos.vue +251 -0
  62. package/dist/runtime/pages/admin/todos.vue.d.ts +3 -0
  63. package/dist/runtime/plugins/admin-font.client.d.ts +0 -5
  64. package/dist/runtime/plugins/admin-font.client.js +1 -0
  65. package/dist/runtime/stores/adminUser.d.ts +29 -1
  66. package/dist/runtime/stores/adminUser.js +4 -2
  67. package/dist/types.d.mts +1 -1
  68. package/package.json +6 -3
@@ -0,0 +1,300 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, useHead } from "#imports";
3
+ import { useAdminProducts } from "#imports";
4
+ useHead({ title: "Produits - Admin" });
5
+ const { fetchProducts, createProduct, updateProduct, deleteProduct } = useAdminProducts();
6
+ const products = ref([]);
7
+ const loading = ref(true);
8
+ const showModal = ref(false);
9
+ const editingProduct = ref(null);
10
+ const saving = ref(false);
11
+ const featuresText = ref("");
12
+ const defaultForm = () => ({
13
+ name: "",
14
+ description: "",
15
+ short_description: "",
16
+ type: "product",
17
+ category: "",
18
+ price: 0,
19
+ currency: "EUR",
20
+ pricing_model: "one-time",
21
+ is_offer: false,
22
+ original_price: 0,
23
+ discount_percentage: 0,
24
+ features: [],
25
+ status: "active",
26
+ stock_quantity: 0,
27
+ is_unlimited_stock: true
28
+ });
29
+ const formData = ref(defaultForm());
30
+ onMounted(async () => {
31
+ loading.value = true;
32
+ try {
33
+ products.value = await fetchProducts();
34
+ } catch {
35
+ } finally {
36
+ loading.value = false;
37
+ }
38
+ });
39
+ const activeProducts = computed(() => products.value.filter((p) => p.status === "active").length);
40
+ const offerProducts = computed(() => products.value.filter((p) => p.is_offer).length);
41
+ const totalRevenue = computed(() => products.value.reduce((s, p) => s + (p.revenue_generated || 0), 0).toLocaleString("fr-FR"));
42
+ const openCreate = () => {
43
+ editingProduct.value = null;
44
+ formData.value = defaultForm();
45
+ featuresText.value = "";
46
+ showModal.value = true;
47
+ };
48
+ const editProduct = (p) => {
49
+ editingProduct.value = p;
50
+ formData.value = { ...p };
51
+ featuresText.value = p.features?.join("\n") || "";
52
+ showModal.value = true;
53
+ };
54
+ const closeModal = () => {
55
+ showModal.value = false;
56
+ editingProduct.value = null;
57
+ };
58
+ const saveProduct = async () => {
59
+ saving.value = true;
60
+ try {
61
+ formData.value.features = featuresText.value.split("\n").map((f) => f.trim()).filter((f) => f);
62
+ if (editingProduct.value) await updateProduct(editingProduct.value.id, formData.value);
63
+ else await createProduct(formData.value);
64
+ products.value = await fetchProducts();
65
+ closeModal();
66
+ } catch {
67
+ } finally {
68
+ saving.value = false;
69
+ }
70
+ };
71
+ const duplicateProduct = async (p) => {
72
+ const d = { ...p };
73
+ delete d.id;
74
+ d.name = `${p.name} (Copie)`;
75
+ d.units_sold = 0;
76
+ d.revenue_generated = 0;
77
+ await createProduct(d);
78
+ products.value = await fetchProducts();
79
+ };
80
+ const handleDelete = async (id) => {
81
+ if (confirm("Supprimer ce produit ?")) {
82
+ await deleteProduct(id);
83
+ products.value = await fetchProducts();
84
+ }
85
+ };
86
+ const formatPrice = (n) => new Intl.NumberFormat("fr-FR").format(n);
87
+ const statusClass = (s) => ({
88
+ active: "px-3 py-1 bg-green-100 text-green-700 rounded-lg text-sm font-semibold",
89
+ inactive: "px-3 py-1 bg-gray-100 text-gray-700 rounded-lg text-sm font-semibold",
90
+ draft: "px-3 py-1 bg-yellow-100 text-yellow-700 rounded-lg text-sm font-semibold",
91
+ archived: "px-3 py-1 bg-red-100 text-red-700 rounded-lg text-sm font-semibold"
92
+ })[s] || "px-3 py-1 bg-green-100 text-green-700 rounded-lg text-sm font-semibold";
93
+ const statusLabel = (s) => ({ active: "Actif", inactive: "Inactif", draft: "Brouillon", archived: "Archive" })[s] || s;
94
+ </script>
95
+
96
+ <template>
97
+ <AdminDashboardLayout>
98
+ <div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
99
+ <div class="flex items-center justify-between mb-8">
100
+ <div>
101
+ <h1 class="text-4xl font-bold text-slate-900">Produits & Offres</h1>
102
+ <p class="text-slate-500 mt-1">Gerez votre catalogue de produits et services</p>
103
+ </div>
104
+ <button @click="openCreate" class="px-6 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 shadow-lg flex items-center gap-2">
105
+ <Icon name="mdi:plus" class="inline" />
106
+ Nouveau Produit
107
+ </button>
108
+ </div>
109
+
110
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
111
+ <div class="bg-white rounded-2xl p-6 border border-slate-200">
112
+ <div class="flex items-center justify-between mb-2">
113
+ <Icon name="mdi:package-variant" class="text-3xl text-blue-600" />
114
+ <span class="px-3 py-1 bg-blue-100 text-blue-700 text-xs font-bold rounded-full">Total</span>
115
+ </div>
116
+ <p class="text-3xl font-black text-slate-900">{{ products.length }}</p>
117
+ <p class="text-sm text-slate-500">Produits</p>
118
+ </div>
119
+ <div class="bg-white rounded-2xl p-6 border border-slate-200">
120
+ <div class="flex items-center justify-between mb-2">
121
+ <Icon name="mdi:check-circle" class="text-3xl text-green-600" />
122
+ <span class="px-3 py-1 bg-green-100 text-green-700 text-xs font-bold rounded-full">Actifs</span>
123
+ </div>
124
+ <p class="text-3xl font-black text-slate-900">{{ activeProducts }}</p>
125
+ <p class="text-sm text-slate-500">En vente</p>
126
+ </div>
127
+ <div class="bg-white rounded-2xl p-6 border border-slate-200">
128
+ <div class="flex items-center justify-between mb-2">
129
+ <Icon name="mdi:tag-multiple" class="text-3xl text-orange-600" />
130
+ <span class="px-3 py-1 bg-orange-100 text-orange-700 text-xs font-bold rounded-full">Promos</span>
131
+ </div>
132
+ <p class="text-3xl font-black text-slate-900">{{ offerProducts }}</p>
133
+ <p class="text-sm text-slate-500">Offres actives</p>
134
+ </div>
135
+ <div class="bg-white rounded-2xl p-6 border border-slate-200">
136
+ <div class="flex items-center justify-between mb-2">
137
+ <Icon name="mdi:cash-multiple" class="text-3xl text-purple-600" />
138
+ <span class="px-3 py-1 bg-purple-100 text-purple-700 text-xs font-bold rounded-full">Revenue</span>
139
+ </div>
140
+ <p class="text-3xl font-black text-slate-900">{{ totalRevenue }}</p>
141
+ <p class="text-sm text-slate-500">Total</p>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden">
146
+ <div class="overflow-x-auto">
147
+ <table class="w-full">
148
+ <thead>
149
+ <tr class="bg-slate-50 border-b border-slate-200">
150
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Produit</th>
151
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Type</th>
152
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Prix</th>
153
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Statut</th>
154
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Ventes</th>
155
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Actions</th>
156
+ </tr>
157
+ </thead>
158
+ <tbody class="divide-y divide-slate-200">
159
+ <tr v-for="product in products" :key="product.id" class="hover:bg-slate-50 transition-colors">
160
+ <td class="px-6 py-4">
161
+ <div class="flex items-center gap-4">
162
+ <div class="w-14 h-14 rounded-xl overflow-hidden bg-slate-100 flex items-center justify-center flex-shrink-0">
163
+ <img v-if="product.image_url" :src="product.image_url" :alt="product.name" class="w-full h-full object-cover" />
164
+ <Icon v-else name="mdi:package-variant" class="text-2xl text-slate-400" />
165
+ </div>
166
+ <div>
167
+ <p class="font-bold text-slate-900">{{ product.name }}</p>
168
+ <p class="text-sm text-slate-500 line-clamp-1">{{ product.short_description }}</p>
169
+ <span v-if="product.is_offer" class="px-2 py-0.5 bg-orange-100 text-orange-700 text-xs font-bold rounded">-{{ product.discount_percentage }}% OFF</span>
170
+ </div>
171
+ </div>
172
+ </td>
173
+ <td class="px-6 py-4"><span class="px-3 py-1 bg-blue-100 text-blue-700 rounded-lg text-sm font-semibold capitalize">{{ product.type }}</span></td>
174
+ <td class="px-6 py-4">
175
+ <p class="font-bold text-slate-900">{{ formatPrice(product.price) }} {{ product.currency }}</p>
176
+ <p v-if="product.is_offer && product.original_price" class="text-sm text-slate-400 line-through">{{ formatPrice(product.original_price) }}</p>
177
+ <p v-if="product.pricing_model" class="text-xs text-slate-500">{{ product.pricing_model }}</p>
178
+ </td>
179
+ <td class="px-6 py-4"><span :class="statusClass(product.status)">{{ statusLabel(product.status) }}</span></td>
180
+ <td class="px-6 py-4">
181
+ <p class="font-bold text-slate-900">{{ product.units_sold || 0 }} unites</p>
182
+ <p class="text-sm text-slate-500">{{ formatPrice(product.revenue_generated || 0) }}</p>
183
+ </td>
184
+ <td class="px-6 py-4">
185
+ <div class="flex items-center gap-2">
186
+ <button @click="editProduct(product)" class="p-2 hover:bg-blue-100 text-blue-600 rounded-lg transition-all"><Icon name="mdi:pencil" class="text-lg" /></button>
187
+ <button @click="duplicateProduct(product)" class="p-2 hover:bg-green-100 text-green-600 rounded-lg transition-all"><Icon name="mdi:content-copy" class="text-lg" /></button>
188
+ <button @click="handleDelete(product.id)" class="p-2 hover:bg-red-100 text-red-600 rounded-lg transition-all"><Icon name="mdi:delete" class="text-lg" /></button>
189
+ </div>
190
+ </td>
191
+ </tr>
192
+ </tbody>
193
+ </table>
194
+ </div>
195
+ <div v-if="products.length === 0" class="text-center py-16 text-slate-400">
196
+ <Icon name="mdi:package-variant-closed" class="text-6xl mb-4 mx-auto" />
197
+ <p class="text-lg font-bold mb-2">Aucun produit</p>
198
+ <button @click="openCreate" class="px-6 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 mt-2">
199
+ <Icon name="mdi:plus" class="inline mr-2" /> Creer mon premier produit
200
+ </button>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <Teleport to="body">
206
+ <div v-if="showModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-6" @click.self="closeModal">
207
+ <div class="bg-white rounded-3xl p-8 max-w-4xl w-full max-h-[90vh] overflow-y-auto shadow-2xl">
208
+ <div class="flex items-center justify-between mb-6">
209
+ <h3 class="text-2xl font-bold text-slate-900">{{ editingProduct ? "Modifier le Produit" : "Nouveau Produit" }}</h3>
210
+ <button @click="closeModal" class="p-2 hover:bg-slate-100 rounded-xl transition-colors"><Icon name="mdi:close" class="text-2xl text-slate-400" /></button>
211
+ </div>
212
+ <form @submit.prevent="saveProduct" class="space-y-6">
213
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
214
+ <div class="md:col-span-2">
215
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Nom *</label>
216
+ <input v-model="formData.name" required class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
217
+ </div>
218
+ <div class="md:col-span-2">
219
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Description courte</label>
220
+ <input v-model="formData.short_description" maxlength="500" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
221
+ </div>
222
+ <div class="md:col-span-2">
223
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Description complete</label>
224
+ <textarea v-model="formData.description" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none"></textarea>
225
+ </div>
226
+ <div>
227
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Type *</label>
228
+ <select v-model="formData.type" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none">
229
+ <option value="product">Produit</option>
230
+ <option value="service">Service</option>
231
+ <option value="subscription">Abonnement</option>
232
+ <option value="package">Package</option>
233
+ </select>
234
+ </div>
235
+ <div>
236
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Categorie</label>
237
+ <input v-model="formData.category" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
238
+ </div>
239
+ </div>
240
+ <div class="border-t pt-6">
241
+ <h4 class="text-lg font-bold text-slate-900 mb-4">Tarification</h4>
242
+ <div class="grid grid-cols-3 gap-4">
243
+ <div>
244
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Prix *</label>
245
+ <input v-model.number="formData.price" type="number" step="0.01" required class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
246
+ </div>
247
+ <div>
248
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Devise</label>
249
+ <select v-model="formData.currency" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl">
250
+ <option value="EUR">EUR</option><option value="USD">USD</option><option value="DZD">DZD</option>
251
+ </select>
252
+ </div>
253
+ <div>
254
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Modele</label>
255
+ <select v-model="formData.pricing_model" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl">
256
+ <option value="one-time">Paiement unique</option>
257
+ <option value="monthly">Mensuel</option>
258
+ <option value="yearly">Annuel</option>
259
+ <option value="custom">Personnalise</option>
260
+ </select>
261
+ </div>
262
+ </div>
263
+ <div class="mt-4 p-4 bg-orange-50 rounded-xl border-2 border-orange-200">
264
+ <label class="flex items-center gap-3 cursor-pointer">
265
+ <input v-model="formData.is_offer" type="checkbox" class="w-5 h-5 rounded" />
266
+ <span class="font-bold text-slate-900">Offre promotionnelle</span>
267
+ </label>
268
+ <div v-if="formData.is_offer" class="grid grid-cols-2 gap-4 mt-4">
269
+ <div>
270
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Prix original</label>
271
+ <input v-model.number="formData.original_price" type="number" step="0.01" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
272
+ </div>
273
+ <div>
274
+ <label class="block text-sm font-semibold text-slate-700 mb-2">Reduction (%)</label>
275
+ <input v-model.number="formData.discount_percentage" type="number" min="0" max="100" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl" />
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ <div class="border-t pt-6">
281
+ <h4 class="text-lg font-bold text-slate-900 mb-4">Caracteristiques</h4>
282
+ <textarea v-model="featuresText" rows="4" placeholder="Une par ligne..." class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl"></textarea>
283
+ </div>
284
+ <div class="border-t pt-6">
285
+ <h4 class="text-lg font-bold text-slate-900 mb-4">Statut</h4>
286
+ <select v-model="formData.status" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl">
287
+ <option value="active">Actif</option><option value="inactive">Inactif</option>
288
+ <option value="draft">Brouillon</option><option value="archived">Archive</option>
289
+ </select>
290
+ </div>
291
+ <div class="flex gap-3 pt-4 border-t">
292
+ <button type="submit" :disabled="saving" class="flex-1 px-6 py-3 bg-blue-600 text-white font-bold rounded-xl hover:bg-blue-700 disabled:opacity-50">{{ saving ? "Enregistrement..." : "Enregistrer" }}</button>
293
+ <button type="button" @click="closeModal" class="px-6 py-3 bg-slate-100 text-slate-700 font-bold rounded-xl hover:bg-slate-200">Annuler</button>
294
+ </div>
295
+ </form>
296
+ </div>
297
+ </div>
298
+ </Teleport>
299
+ </AdminDashboardLayout>
300
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -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,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;