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.
Files changed (64) 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 -2
  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/useAdminStrategies.d.ts +10 -0
  27. package/dist/runtime/composables/useAdminStrategies.js +57 -0
  28. package/dist/runtime/composables/useAdminTodos.d.ts +10 -0
  29. package/dist/runtime/composables/useAdminTodos.js +55 -0
  30. package/dist/runtime/pages/admin/account.vue +59 -51
  31. package/dist/runtime/pages/admin/branding.d.vue.ts +3 -0
  32. package/dist/runtime/pages/admin/branding.vue +276 -0
  33. package/dist/runtime/pages/admin/branding.vue.d.ts +3 -0
  34. package/dist/runtime/pages/admin/calendar.d.vue.ts +3 -0
  35. package/dist/runtime/pages/admin/calendar.vue +169 -0
  36. package/dist/runtime/pages/admin/calendar.vue.d.ts +3 -0
  37. package/dist/runtime/pages/admin/content-library.d.vue.ts +3 -0
  38. package/dist/runtime/pages/admin/content-library.vue +359 -0
  39. package/dist/runtime/pages/admin/content-library.vue.d.ts +3 -0
  40. package/dist/runtime/pages/admin/editor/[id].vue +5 -3
  41. package/dist/runtime/pages/admin/index.vue +81 -23
  42. package/dist/runtime/pages/admin/pages/[type].vue +267 -83
  43. package/dist/runtime/pages/admin/personas/[id].d.vue.ts +3 -0
  44. package/dist/runtime/pages/admin/personas/[id].vue +170 -0
  45. package/dist/runtime/pages/admin/personas/[id].vue.d.ts +3 -0
  46. package/dist/runtime/pages/admin/personas/index.d.vue.ts +3 -0
  47. package/dist/runtime/pages/admin/personas/index.vue +266 -0
  48. package/dist/runtime/pages/admin/personas/index.vue.d.ts +3 -0
  49. package/dist/runtime/pages/admin/products.d.vue.ts +3 -0
  50. package/dist/runtime/pages/admin/products.vue +300 -0
  51. package/dist/runtime/pages/admin/products.vue.d.ts +3 -0
  52. package/dist/runtime/pages/admin/strategies/[id].d.vue.ts +3 -0
  53. package/dist/runtime/pages/admin/strategies/[id].vue +237 -0
  54. package/dist/runtime/pages/admin/strategies/[id].vue.d.ts +3 -0
  55. package/dist/runtime/pages/admin/strategies/index.d.vue.ts +3 -0
  56. package/dist/runtime/pages/admin/strategies/index.vue +232 -0
  57. package/dist/runtime/pages/admin/strategies/index.vue.d.ts +3 -0
  58. package/dist/runtime/pages/admin/todos.d.vue.ts +3 -0
  59. package/dist/runtime/pages/admin/todos.vue +251 -0
  60. package/dist/runtime/pages/admin/todos.vue.d.ts +3 -0
  61. package/dist/runtime/stores/adminUser.d.ts +29 -1
  62. package/dist/runtime/stores/adminUser.js +2 -2
  63. package/dist/types.d.mts +1 -1
  64. package/package.json +6 -3
@@ -0,0 +1,266 @@
1
+ <script setup>
2
+ import { ref, computed, onMounted, useHead, navigateTo } from "#imports";
3
+ import { useAdminPersonas } from "#imports";
4
+ useHead({ title: "Personas - Admin" });
5
+ const { fetchPersonas, createPersona, updatePersona, deletePersona } = useAdminPersonas();
6
+ const personas = ref([]);
7
+ const loading = ref(true);
8
+ const showModal = ref(false);
9
+ const saving = ref(false);
10
+ const editingPersona = ref(null);
11
+ const currentLayout = ref("table");
12
+ const defaultForm = () => ({
13
+ name: "",
14
+ job_title: "",
15
+ age: null,
16
+ location: "",
17
+ about: "",
18
+ avatar_url: "",
19
+ core_needs: [],
20
+ goals: [],
21
+ pain_points: [],
22
+ frustrations: [],
23
+ preferred_channels: [],
24
+ topics: [],
25
+ gradient_class: ""
26
+ });
27
+ const formData = ref(defaultForm());
28
+ const coreNeedsText = ref("");
29
+ const goalsText = ref("");
30
+ const painPointsText = ref("");
31
+ const layoutIcon = computed(() => currentLayout.value === "table" ? "mdi:table" : "mdi:view-dashboard-variant-outline");
32
+ const layoutLabel = computed(() => currentLayout.value === "table" ? "Tableau" : "Grille");
33
+ onMounted(async () => {
34
+ loading.value = true;
35
+ try {
36
+ personas.value = await fetchPersonas();
37
+ } catch {
38
+ } finally {
39
+ loading.value = false;
40
+ }
41
+ });
42
+ const getInitials = (name) => name ? name.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2) : "?";
43
+ const openCreate = () => {
44
+ editingPersona.value = null;
45
+ formData.value = defaultForm();
46
+ coreNeedsText.value = "";
47
+ goalsText.value = "";
48
+ painPointsText.value = "";
49
+ showModal.value = true;
50
+ };
51
+ const openEdit = (persona) => {
52
+ editingPersona.value = persona;
53
+ formData.value = { ...defaultForm(), ...persona };
54
+ coreNeedsText.value = (persona.core_needs || []).join("\n");
55
+ goalsText.value = (persona.goals || []).join("\n");
56
+ painPointsText.value = (persona.pain_points || []).join("\n");
57
+ showModal.value = true;
58
+ };
59
+ const closeModal = () => {
60
+ showModal.value = false;
61
+ editingPersona.value = null;
62
+ };
63
+ const savePersona = async () => {
64
+ if (!formData.value.name) return;
65
+ saving.value = true;
66
+ try {
67
+ const data = {
68
+ ...formData.value,
69
+ core_needs: coreNeedsText.value.split("\n").map((s) => s.trim()).filter(Boolean),
70
+ goals: goalsText.value.split("\n").map((s) => s.trim()).filter(Boolean),
71
+ pain_points: painPointsText.value.split("\n").map((s) => s.trim()).filter(Boolean)
72
+ };
73
+ if (editingPersona.value) await updatePersona(editingPersona.value.id, data);
74
+ else await createPersona(data);
75
+ personas.value = await fetchPersonas();
76
+ closeModal();
77
+ } catch {
78
+ } finally {
79
+ saving.value = false;
80
+ }
81
+ };
82
+ const handleDelete = async (id) => {
83
+ if (confirm("Supprimer cette persona ?")) {
84
+ try {
85
+ await deletePersona(id);
86
+ personas.value = await fetchPersonas();
87
+ } catch {
88
+ }
89
+ }
90
+ };
91
+ const viewPersona = (persona) => navigateTo(`/admin/personas/${persona.id}`);
92
+ </script>
93
+
94
+ <template>
95
+ <AdminDashboardLayout>
96
+ <div class="max-w-7xl mx-auto px-6 md:px-10 py-10">
97
+ <header class="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-6">
98
+ <div>
99
+ <h1 class="text-5xl font-black text-slate-900 tracking-tight">User <span class="text-blue-600">Personas</span></h1>
100
+ <p class="text-lg text-slate-500 mt-2">Creez et gerez vos personas cibles.</p>
101
+ </div>
102
+ <div class="flex items-center gap-4">
103
+ <div class="relative group">
104
+ <button class="px-5 py-4 bg-white border border-slate-200 rounded-2xl font-bold text-slate-700 flex items-center gap-3 hover:border-blue-300 transition-all shadow-sm">
105
+ <Icon :name="layoutIcon" class="text-xl text-blue-600" />
106
+ <span class="text-sm">{{ layoutLabel }}</span>
107
+ <Icon name="mdi:chevron-down" class="text-slate-300" />
108
+ </button>
109
+ <div class="absolute right-0 mt-3 w-48 bg-white border border-slate-100 rounded-2xl shadow-2xl py-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-30">
110
+ <button @click="currentLayout = 'table'" class="w-full px-4 py-3 text-left hover:bg-blue-50 flex items-center gap-3">
111
+ <Icon name="mdi:table" class="text-slate-400" />
112
+ <span :class="currentLayout === 'table' ? 'text-blue-600 font-black' : 'text-slate-600 font-bold'">Tableau</span>
113
+ </button>
114
+ <button @click="currentLayout = 'bento'" class="w-full px-4 py-3 text-left hover:bg-emerald-50 flex items-center gap-3">
115
+ <Icon name="mdi:view-dashboard-variant" class="text-slate-400" />
116
+ <span :class="currentLayout === 'bento' ? 'text-emerald-600 font-black' : 'text-slate-600 font-bold'">Grille</span>
117
+ </button>
118
+ </div>
119
+ </div>
120
+ <button @click="openCreate" class="px-8 py-4 bg-slate-900 text-white rounded-2xl font-black text-sm uppercase hover:bg-blue-600 transition-all flex items-center gap-3 shadow-xl">
121
+ <Icon name="mdi:plus" class="text-xl" /> Nouveau
122
+ </button>
123
+ </div>
124
+ </header>
125
+
126
+ <div v-if="loading" class="grid grid-cols-3 gap-6">
127
+ <div v-for="i in 3" :key="i" class="h-40 bg-slate-100 animate-pulse rounded-3xl"></div>
128
+ </div>
129
+
130
+ <div v-else-if="personas.length === 0" class="bg-white rounded-[3rem] p-20 text-center border-2 border-dashed border-slate-200">
131
+ <Icon name="mdi:account-question-outline" class="text-6xl text-slate-200 mb-6" />
132
+ <h3 class="text-2xl font-black text-slate-800">Aucune persona</h3>
133
+ <p class="text-slate-400 mt-2 mb-8">Creez vos premiers profils comportementaux.</p>
134
+ <button @click="openCreate" class="px-8 py-3 bg-blue-600 text-white rounded-2xl font-black text-sm uppercase hover:bg-slate-900 transition-all">Creer ma premiere persona</button>
135
+ </div>
136
+
137
+ <div v-else>
138
+ <!-- Table Layout -->
139
+ <div v-if="currentLayout === 'table'" class="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
140
+ <table class="w-full">
141
+ <thead>
142
+ <tr class="bg-slate-50 border-b border-slate-200">
143
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Avatar</th>
144
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Nom</th>
145
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Profession</th>
146
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Age</th>
147
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Ville</th>
148
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Besoins</th>
149
+ <th class="px-6 py-4 text-left text-xs font-black text-slate-600 uppercase">Actions</th>
150
+ </tr>
151
+ </thead>
152
+ <tbody class="divide-y divide-slate-200">
153
+ <tr v-for="persona in personas" :key="persona.id" class="hover:bg-slate-50 transition-colors cursor-pointer" @click="viewPersona(persona)">
154
+ <td class="px-6 py-4">
155
+ <div class="w-12 h-12 rounded-xl overflow-hidden bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-white font-black">
156
+ <img v-if="persona.avatar_url" :src="persona.avatar_url" :alt="persona.name" class="w-full h-full object-cover" />
157
+ <span v-else>{{ getInitials(persona.name) }}</span>
158
+ </div>
159
+ </td>
160
+ <td class="px-6 py-4 font-bold text-slate-900">{{ persona.name }}</td>
161
+ <td class="px-6 py-4 text-sm text-slate-600">{{ persona.job_title || "-" }}</td>
162
+ <td class="px-6 py-4 text-sm text-slate-600">{{ persona.age || "-" }}</td>
163
+ <td class="px-6 py-4 text-sm text-slate-600">{{ persona.location || "-" }}</td>
164
+ <td class="px-6 py-4">
165
+ <div class="flex flex-wrap gap-1">
166
+ <span v-for="need in (persona.core_needs || []).slice(0, 2)" :key="need" class="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs font-semibold">{{ need }}</span>
167
+ <span v-if="(persona.core_needs || []).length > 2" class="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs font-semibold">+{{ persona.core_needs.length - 2 }}</span>
168
+ </div>
169
+ </td>
170
+ <td class="px-6 py-4" @click.stop>
171
+ <div class="flex items-center gap-2">
172
+ <button @click="viewPersona(persona)" class="p-2 hover:bg-blue-100 text-blue-600 rounded-lg"><Icon name="mdi:eye" class="text-lg" /></button>
173
+ <button @click="openEdit(persona)" class="p-2 hover:bg-purple-100 text-purple-600 rounded-lg"><Icon name="mdi:pencil" class="text-lg" /></button>
174
+ <button @click="handleDelete(persona.id)" class="p-2 hover:bg-red-100 text-red-600 rounded-lg"><Icon name="mdi:delete" class="text-lg" /></button>
175
+ </div>
176
+ </td>
177
+ </tr>
178
+ </tbody>
179
+ </table>
180
+ </div>
181
+
182
+ <!-- Bento Layout -->
183
+ <div v-else class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
184
+ <div v-for="persona in personas" :key="persona.id" class="bg-white rounded-[2rem] border border-slate-100 p-8 shadow-sm hover:shadow-xl hover:scale-[1.02] transition-all cursor-pointer group" @click="viewPersona(persona)">
185
+ <div class="flex items-center gap-4 mb-6">
186
+ <div class="w-16 h-16 rounded-2xl shrink-0 overflow-hidden bg-gradient-to-br from-purple-400 to-pink-400 flex items-center justify-center text-white font-black text-xl">
187
+ <img v-if="persona.avatar_url" :src="persona.avatar_url" class="w-full h-full object-cover" />
188
+ <span v-else>{{ getInitials(persona.name) }}</span>
189
+ </div>
190
+ <div>
191
+ <h3 class="font-black text-lg text-slate-800 leading-none">{{ persona.name }}</h3>
192
+ <p class="text-[10px] font-black text-blue-600 uppercase tracking-widest mt-1">{{ persona.job_title }}</p>
193
+ </div>
194
+ </div>
195
+ <div class="p-4 bg-slate-50 rounded-2xl">
196
+ <p class="text-xs font-black text-slate-400 uppercase mb-2">A propos</p>
197
+ <p class="text-sm font-bold text-slate-700 line-clamp-2">"{{ persona.about || "Aucune description..." }}"</p>
198
+ </div>
199
+ <div class="flex items-center justify-between text-[10px] font-black uppercase text-slate-400 px-1 mt-4">
200
+ <span class="flex items-center gap-1"><Icon name="mdi:map-marker" /> {{ persona.location || "-" }}</span>
201
+ <span class="text-blue-600 group-hover:translate-x-1 transition-transform">Details</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Modal -->
209
+ <Teleport to="body">
210
+ <div v-if="showModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" @click.self="closeModal">
211
+ <div class="bg-white rounded-3xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl">
212
+ <div class="p-8">
213
+ <div class="flex items-center justify-between mb-6">
214
+ <h2 class="text-2xl font-black text-slate-900">{{ editingPersona ? "Modifier Persona" : "Nouvelle Persona" }}</h2>
215
+ <button @click="closeModal" class="p-2 hover:bg-slate-100 rounded-xl"><Icon name="mdi:close" class="text-xl text-slate-400" /></button>
216
+ </div>
217
+ <div class="space-y-5">
218
+ <div class="grid grid-cols-2 gap-4">
219
+ <div>
220
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Nom *</label>
221
+ <input v-model="formData.name" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none font-bold" />
222
+ </div>
223
+ <div>
224
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Profession</label>
225
+ <input v-model="formData.job_title" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
226
+ </div>
227
+ <div>
228
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Age</label>
229
+ <input v-model.number="formData.age" type="number" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
230
+ </div>
231
+ <div>
232
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Ville</label>
233
+ <input v-model="formData.location" type="text" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
234
+ </div>
235
+ </div>
236
+ <div>
237
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">A propos</label>
238
+ <textarea v-model="formData.about" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none"></textarea>
239
+ </div>
240
+ <div>
241
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">URL Avatar</label>
242
+ <input v-model="formData.avatar_url" type="url" placeholder="https://..." class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none" />
243
+ </div>
244
+ <div>
245
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Besoins principaux (un par ligne)</label>
246
+ <textarea v-model="coreNeedsText" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none text-sm"></textarea>
247
+ </div>
248
+ <div>
249
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Objectifs (un par ligne)</label>
250
+ <textarea v-model="goalsText" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none text-sm"></textarea>
251
+ </div>
252
+ <div>
253
+ <label class="block text-xs font-black uppercase text-slate-500 mb-2">Points de douleur (un par ligne)</label>
254
+ <textarea v-model="painPointsText" rows="3" class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:border-blue-500 focus:outline-none resize-none text-sm"></textarea>
255
+ </div>
256
+ <div class="flex gap-3 pt-4 border-t">
257
+ <button @click="savePersona" :disabled="saving" class="flex-1 px-6 py-3 bg-blue-600 text-white font-black rounded-xl hover:bg-blue-700 disabled:opacity-50">{{ saving ? "Enregistrement..." : "Enregistrer" }}</button>
258
+ <button @click="closeModal" class="px-6 py-3 bg-slate-100 text-slate-700 font-bold rounded-xl hover:bg-slate-200">Annuler</button>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </Teleport>
265
+ </AdminDashboardLayout>
266
+ </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,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;