@open-commune/web 0.1.3

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 (132) hide show
  1. package/README.md +41 -0
  2. package/app.config.ts +10 -0
  3. package/app.vue +13 -0
  4. package/assets/css/main.css +13 -0
  5. package/components/data/display/row/data-avatar.vue +23 -0
  6. package/components/data/display/row/data-badge.vue +34 -0
  7. package/components/data/display/row/data-color.vue +17 -0
  8. package/components/data/display/row/data-display-badge-publish-status.vue +29 -0
  9. package/components/data/display/row/data-label.vue +37 -0
  10. package/components/data/display/row/data-last-update.vue +45 -0
  11. package/components/data/display/row/data-spacer.vue +4 -0
  12. package/components/data/display/row/data-summary.vue +44 -0
  13. package/components/data/display/row/data-text.vue +14 -0
  14. package/components/data/display/row/data-toggle.vue +16 -0
  15. package/components/data/display/row.vue +111 -0
  16. package/components/data/lists/data-list.vue +85 -0
  17. package/components/data/lists/data-lists-table.vue +155 -0
  18. package/components/elements/avatars/avatar-circular.vue +68 -0
  19. package/components/elements/avatars/avatar-with-text.vue +47 -0
  20. package/components/elements/badges/badge.vue +85 -0
  21. package/components/elements/buttons/primary.vue +32 -0
  22. package/components/elements/buttons/secondary.vue +35 -0
  23. package/components/elements/buttons/tertiary.vue +25 -0
  24. package/components/elements/dropdowns/dots.vue +75 -0
  25. package/components/elements/media/image.vue +82 -0
  26. package/components/elements/notifications/default.vue +90 -0
  27. package/components/forms/handlers/handler-collections.vue +189 -0
  28. package/components/forms/handlers/handler-publication-status.vue +193 -0
  29. package/components/forms/handlers/handler-records.vue +138 -0
  30. package/components/forms/handlers/handler-roles.vue +176 -0
  31. package/components/forms/handlers/handler-schedules.vue +218 -0
  32. package/components/forms/handlers/handler-steps.vue +147 -0
  33. package/components/forms/handlers/handler-tokens.vue +79 -0
  34. package/components/forms/inputs/input-color-picker.vue +159 -0
  35. package/components/forms/inputs/input-editor.vue +363 -0
  36. package/components/forms/inputs/input-files-media.vue +150 -0
  37. package/components/forms/inputs/input-files.vue +98 -0
  38. package/components/forms/inputs/input-json.vue +65 -0
  39. package/components/forms/inputs/input-location.vue +179 -0
  40. package/components/forms/inputs/input-text-area.vue +65 -0
  41. package/components/forms/inputs/input-text.vue +122 -0
  42. package/components/forms/inputs/input-toggle.vue +125 -0
  43. package/components/forms/inputs/media-section/media-editor.vue +95 -0
  44. package/components/forms/layouts/form.vue +203 -0
  45. package/components/forms/layouts/layout.vue +47 -0
  46. package/components/forms/layouts/modal.vue +96 -0
  47. package/components/forms/layouts/navigation.vue +48 -0
  48. package/components/forms/layouts/section.vue +118 -0
  49. package/components/forms/selects/select-collection-enum.vue +195 -0
  50. package/components/forms/selects/select-date.vue +245 -0
  51. package/components/forms/selects/select-enum.vue +186 -0
  52. package/components/forms/selects/select-record.vue +274 -0
  53. package/components/layout/headings/headings-collection.vue +29 -0
  54. package/components/layout/headings/headings-record.vue +33 -0
  55. package/components/layout/navigations/primary/dropdown-publish.vue +147 -0
  56. package/components/layout/navigations/primary.vue +132 -0
  57. package/components/layout/notifications/notifications-list.vue +19 -0
  58. package/composables/core/domains/use-definitions.ts +57 -0
  59. package/composables/core/domains/use-entries.ts +62 -0
  60. package/composables/core/domains/use-media.ts +82 -0
  61. package/composables/core/use-core.ts +12 -0
  62. package/composables/use-ability.js +38 -0
  63. package/composables/use-models.js +27 -0
  64. package/composables/use-session.ts +80 -0
  65. package/composables/use-timer.js +62 -0
  66. package/composables/use-trpc.js +17 -0
  67. package/editor/index.ts +44 -0
  68. package/editor/views/CalloutView.vue +22 -0
  69. package/editor/views/DetailsContentView.vue +11 -0
  70. package/editor/views/DetailsSummaryView.vue +11 -0
  71. package/editor/views/DetailsView.vue +72 -0
  72. package/editor/views/EmbedView.vue +53 -0
  73. package/editor/views/FileView.vue +79 -0
  74. package/editor/views/ImageView.vue +56 -0
  75. package/editor/views/UploadView.vue +71 -0
  76. package/layouts/default.vue +21 -0
  77. package/layouts/entrance.vue +10 -0
  78. package/layouts/record.vue +21 -0
  79. package/middleware/00.auth.global.js +16 -0
  80. package/middleware/01.instance.global.js +21 -0
  81. package/middleware/03.verify-permissions.global.js +11 -0
  82. package/middleware/04.prefetch-page.global.js +18 -0
  83. package/middleware/05.headings-page.global.js +25 -0
  84. package/models/_commun.js +243 -0
  85. package/models/_factory.js +34 -0
  86. package/models/collection.js +87 -0
  87. package/models/entrance.js +42 -0
  88. package/models/media.js +82 -0
  89. package/models/organization.js +92 -0
  90. package/models/record.js +165 -0
  91. package/models/role.js +66 -0
  92. package/models/token.js +56 -0
  93. package/models/user.js +97 -0
  94. package/nuxt.config.ts +151 -0
  95. package/package.json +71 -0
  96. package/pages/[collection]/[record]/index.vue +88 -0
  97. package/pages/[collection]/index.vue +55 -0
  98. package/pages/_lab/editor.vue +149 -0
  99. package/pages/auth/callback.vue +31 -0
  100. package/pages/index/index.vue +85 -0
  101. package/pages/index/password/define/[token].vue +79 -0
  102. package/pages/index/password/recover/index.vue +72 -0
  103. package/pages/index/welcome/[token].vue +90 -0
  104. package/pages/index.vue +44 -0
  105. package/pages/management/index.vue +15 -0
  106. package/pages/management/members.vue +176 -0
  107. package/pages/management/organizations.vue +192 -0
  108. package/pages/management/roles.vue +183 -0
  109. package/pages/overview.vue +15 -0
  110. package/pages/settings.vue +197 -0
  111. package/plugins/01.session.client.ts +5 -0
  112. package/plugins/02.vue-query.ts +25 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/logo-black.svg +1 -0
  115. package/public/logo-white.svg +1 -0
  116. package/server/api/trpc/[...trpc].ts +28 -0
  117. package/server/api/v1/content/deployment.get.ts +45 -0
  118. package/server/api/v1/content/records.get.ts +12 -0
  119. package/server/middleware/0.security.ts +19 -0
  120. package/server/middleware/1.cors.ts +31 -0
  121. package/server/middleware/2.session.ts +20 -0
  122. package/server/middleware/3.api-token.ts +20 -0
  123. package/server/plugins/core.ts +8 -0
  124. package/server/routes/_tasks/[name].post.ts +20 -0
  125. package/server/routes/health.get.ts +6 -0
  126. package/server/tasks/deploy.ts +26 -0
  127. package/server/types.d.ts +8 -0
  128. package/server/utils/core.ts +18 -0
  129. package/store/layout/headings.js +21 -0
  130. package/store/layout/modal.js +80 -0
  131. package/store/layout/notifications.js +52 -0
  132. package/store/layout/workspace.js +80 -0
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div class="mt-12 text-center">
3
+ <h2 class="inline-flex mt-6 text-sm font-semibold">
4
+ Nouveau mot de passe
5
+ </h2>
6
+ <p class="mt-4 font-light text-sm text-gray-600">
7
+ Choisissez un mot de passe robuste (10 caractères minimum).
8
+ </p>
9
+ <form class="mt-12 space-y-6" @submit.prevent="_updatePassword">
10
+ <input-text
11
+ place-holder="Mot de passe"
12
+ name="password"
13
+ type="password"
14
+ autocomplete="new-password"
15
+ :value="password"
16
+ required
17
+ @change="password = $event"
18
+ />
19
+ <input-text
20
+ place-holder="Confirmation"
21
+ name="password-confirmation"
22
+ type="password"
23
+ autocomplete="new-password"
24
+ :value="passwordConfirmation"
25
+ required
26
+ @change="passwordConfirmation = $event"
27
+ />
28
+ <button-primary
29
+ type="submit"
30
+ :label="isLoading ? 'Envoie en cours' : 'Confirmer'"
31
+ :disabled="!password || password !== passwordConfirmation"
32
+ :loading="isLoading"
33
+ class="w-full"
34
+ />
35
+ </form>
36
+ </div>
37
+ </template>
38
+
39
+ <script setup>
40
+ import { useNotificationsStore } from '~/store/layout/notifications'
41
+ import InputText from '~/components/forms/inputs/input-text'
42
+ import ButtonPrimary from '~/components/elements/buttons/primary'
43
+
44
+ // Prepare composables
45
+ const notificationsStore = useNotificationsStore()
46
+ const $route = useRoute()
47
+ const $router = useRouter()
48
+ const { Entrance } = useModels()
49
+
50
+ // Prepare data
51
+ const password = ref('')
52
+ const passwordConfirmation = ref('')
53
+ const isLoading = ref(false)
54
+
55
+ // Prepare methods
56
+ // Cette page ne sert QUE la réinitialisation : le compte existe déjà, son nom
57
+ // est conservé côté serveur. L'invitation, elle, a son propre écran (/welcome)
58
+ // — c'est le seul parcours qui a un nom d'affichage à demander. Les deux
59
+ // partageaient cet écran, et un utilisateur venu du mail « mot de passe
60
+ // oublié » se voyait réclamer un nom sous un titre « Votre compte ».
61
+ const _updatePassword = async () => {
62
+ isLoading.value = true
63
+ try {
64
+ const { token } = $route.params
65
+ await Entrance.updatePassword({ token, password: password.value })
66
+ notificationsStore.add({ icon: 'iconoir:password-cursor', type: 'success', title: 'Mot de passe mis à jour, connectez-vous.' })
67
+ $router.push('/')
68
+ } catch (err) {
69
+ console.log(err)
70
+ notificationsStore.add({
71
+ icon: 'iconoir:password-error',
72
+ type: 'warn',
73
+ title: 'Lien expiré ou déjà utilisé, ou mot de passe trop court (10 caractères minimum).'
74
+ })
75
+ } finally {
76
+ isLoading.value = false
77
+ }
78
+ }
79
+ </script>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <div class="mt-12 text-center">
3
+ <h2 class="inline-flex mt-6 text-sm font-semibold">
4
+ {{ title }}
5
+ </h2>
6
+ <p class="mt-4 font-light text-sm text-gray-600">
7
+ {{ message }}
8
+ </p>
9
+ <form v-if="!isSent" class="mt-12 space-y-6" @submit.prevent="_recoverPassword">
10
+ <input-text
11
+ place-holder="Adresse email"
12
+ name="email"
13
+ type="email"
14
+ autocomplete="email"
15
+ :value="emailAddress"
16
+ :read-only="isLoading"
17
+ required
18
+ @change="emailAddress = $event"
19
+ />
20
+ <button-primary
21
+ type="submit"
22
+ :label="isLoading ? 'Envoie en cours' : 'Continuer'"
23
+ :disabled="!emailAddress"
24
+ :loading="isLoading"
25
+ class="w-full"
26
+ />
27
+ </form>
28
+ <button-primary
29
+ v-if="isSent"
30
+ label="← Revenir à l'espace de connexion"
31
+ class="w-full mt-8"
32
+ @click="$router.push('/')"
33
+ />
34
+ </div>
35
+ </template>
36
+
37
+ <script setup>
38
+ import { useNotificationsStore } from '~/store/layout/notifications'
39
+ import InputText from '~/components/forms/inputs/input-text'
40
+ import ButtonPrimary from '~/components/elements/buttons/primary'
41
+
42
+ // Prepare composables
43
+ const notificationsStore = useNotificationsStore()
44
+ const { Entrance } = useModels()
45
+
46
+ // Prepare data
47
+ const isLoading = ref(false)
48
+ const isSent = ref(false)
49
+ const emailAddress = ref('')
50
+
51
+ // Prepare computed
52
+ const title = computed(() => {
53
+ return isSent.value ? 'Le mail est parti' : 'Réinitialiser votre mot de passe'
54
+ })
55
+ const message = computed(() => {
56
+ return isSent.value ? 'N\'hésitez pas à vérifier vos courriers indesirables.' : 'Entrez votre adresse email et nous vous enverrons un lien pour mettre à jour votre mot de passe.'
57
+ })
58
+
59
+ // Prepare methods
60
+ const _recoverPassword = async () => {
61
+ isLoading.value = true
62
+ try {
63
+ await Entrance.recoverPassword(emailAddress.value)
64
+ notificationsStore.add({ icon: 'iconoir:mail-out', type: 'success', title: 'Email envoyé.' })
65
+ isSent.value = true
66
+ } catch (err) {
67
+ console.log(err) // eslint-disable-line
68
+ } finally {
69
+ setTimeout(() => { isLoading.value = false }, 2 * 1000)
70
+ }
71
+ }
72
+ </script>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <div class="mt-12 text-center">
3
+ <h2 class="inline-flex mt-6 text-sm font-semibold">
4
+ Bienvenue !
5
+ </h2>
6
+ <p class="mt-4 font-light text-sm text-gray-600">
7
+ Vous avez été invité·e à rejoindre l'espace d'administration.
8
+ Choisissez votre nom d'affichage et un mot de passe robuste (10 caractères minimum).
9
+ </p>
10
+ <form class="mt-12 space-y-6" @submit.prevent="_createAccount">
11
+ <input-text
12
+ place-holder="Votre nom"
13
+ name="name"
14
+ autocomplete="name"
15
+ :value="name"
16
+ required
17
+ @change="name = $event"
18
+ />
19
+ <input-text
20
+ place-holder="Mot de passe"
21
+ name="password"
22
+ type="password"
23
+ autocomplete="new-password"
24
+ :value="password"
25
+ required
26
+ @change="password = $event"
27
+ />
28
+ <input-text
29
+ place-holder="Confirmation"
30
+ name="password-confirmation"
31
+ type="password"
32
+ autocomplete="new-password"
33
+ :value="passwordConfirmation"
34
+ required
35
+ @change="passwordConfirmation = $event"
36
+ />
37
+ <button-primary
38
+ type="submit"
39
+ :label="isLoading ? 'Envoie en cours' : 'Créer mon compte'"
40
+ :disabled="!name || !password || password !== passwordConfirmation"
41
+ :loading="isLoading"
42
+ class="w-full"
43
+ />
44
+ </form>
45
+ </div>
46
+ </template>
47
+
48
+ <script setup>
49
+ import { useNotificationsStore } from '~/store/layout/notifications'
50
+ import InputText from '~/components/forms/inputs/input-text'
51
+ import ButtonPrimary from '~/components/elements/buttons/primary'
52
+
53
+ // Flux invitation Commun : la validité du jeton est vérifiée au moment de
54
+ // l'acceptation (auth.acceptInvitation) — plus d'étape « confirmation d'email »
55
+ // préalable.
56
+ //
57
+ // L'écran porte désormais le formulaire au lieu de renvoyer sur
58
+ // /password/define : cette page-là ne sert plus qu'à la réinitialisation, où
59
+ // il n'y a pas de nom à demander.
60
+ const notificationsStore = useNotificationsStore()
61
+ const $route = useRoute()
62
+ const $router = useRouter()
63
+ const { Entrance } = useModels()
64
+
65
+ // Prepare data
66
+ const name = ref('')
67
+ const password = ref('')
68
+ const passwordConfirmation = ref('')
69
+ const isLoading = ref(false)
70
+
71
+ // Prepare methods
72
+ const _createAccount = async () => {
73
+ isLoading.value = true
74
+ try {
75
+ const { token } = $route.params
76
+ await Entrance.acceptInvitation({ token, name: name.value, password: password.value })
77
+ notificationsStore.add({ icon: 'iconoir:password-cursor', type: 'success', title: 'Compte activé, connectez-vous.' })
78
+ $router.push('/')
79
+ } catch (err) {
80
+ console.log(err)
81
+ notificationsStore.add({
82
+ icon: 'iconoir:password-error',
83
+ type: 'warn',
84
+ title: 'Invitation invalide ou mot de passe trop court (10 caractères minimum).'
85
+ })
86
+ } finally {
87
+ isLoading.value = false
88
+ }
89
+ }
90
+ </script>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="min-h-screen bg-white flex">
3
+ <div class="m-auto w-full max-w-lg lg:w-96">
4
+ <div class="flex flex-col items-center space-y-2 mb-4">
5
+ <img
6
+ src="/logo-black.svg"
7
+ alt="Logo"
8
+ class="dark:hidden h-16 w-auto"
9
+ >
10
+ <img
11
+ src="/logo-white.svg"
12
+ alt="Logo"
13
+ class="hidden h-24 w-auto"
14
+ >
15
+ <p v-if="demoEnv" class="text-sm text-gray-400 w-full border-t border-gray-100 text-center pt-2">
16
+ <span class="bg-black rounded-lg text-white px-4 text-xs py-0.5 h-5 mx-auto">
17
+ beta
18
+ </span>
19
+ </p>
20
+ </div>
21
+ <nuxt-page />
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script setup>
27
+ const $config = useRuntimeConfig()
28
+
29
+ definePageMeta({
30
+ layout: 'entrance',
31
+ auth: {
32
+ unauthenticatedOnly: true,
33
+ navigateAuthenticatedTo: '/overview',
34
+ }
35
+ })
36
+
37
+ // Prepare computed
38
+ // L'image d'instance est COMMUNE à tous les clients : rien de spécifique n'y est
39
+ // cuit. L'hôte servi est donc la seule source fiable pour distinguer une démo.
40
+ const demoEnv = computed(() => {
41
+ const host = import.meta.client ? window.location.host : ''
42
+ return host.includes('.beta') || host.includes('localhost')
43
+ })
44
+ </script>
@@ -0,0 +1,15 @@
1
+ <template>
2
+ <div class="relative block w-full">
3
+
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ definePageMeta({
9
+ permissions: ['manage:all'],
10
+ headings: {
11
+ title: 'Bienvenue',
12
+ description: 'Cette section est réservée aux administrateurs.'
13
+ }
14
+ })
15
+ </script>
@@ -0,0 +1,176 @@
1
+ <template>
2
+ <section class="w-full">
3
+ <button-secondary label="Inviter un membre" class="mb-6" @click="_openModalForm('create')" />
4
+ <default-table
5
+ v-if="!$route.params.event"
6
+ :headers="TABLE_HEADERS"
7
+ :filters="filters"
8
+ :items="records"
9
+ :actions="abilities"
10
+ :pagination-size="limit"
11
+ display-search-bar
12
+ paginated-content
13
+ @fetch-more="_fetchMore"
14
+ @update="_openModalForm('update', $event)"
15
+ @remove="_handle('remove', $event)"
16
+ />
17
+ </section>
18
+ </template>
19
+
20
+ <script setup>
21
+ import buttonSecondary from '~/components/elements/buttons/secondary'
22
+ import defaultTable from '~/components/data/lists/data-lists-table'
23
+ import { useModalStore } from '~/store/layout/modal'
24
+ import { useNotificationsStore } from '~/store/layout/notifications'
25
+
26
+
27
+ definePageMeta({
28
+ permissions: ['manage:all'],
29
+ preFetch: ['User'],
30
+ headings: {
31
+ title: 'Membres',
32
+ description: 'Retrouvez & managez ici toutes les membres de la plateforme.'
33
+ }
34
+ })
35
+
36
+ const { User } = useModels()
37
+ const $route = useRoute()
38
+ const $modalStore = useModalStore()
39
+ const notificationsStore = useNotificationsStore()
40
+
41
+ // Prepare constants
42
+ const TABLE_HEADERS = [
43
+ {
44
+ nestedValue: false,
45
+ componentOptions: {
46
+ titleKey: 'fullName',
47
+ descriptionKey: 'emailAddress',
48
+ },
49
+ component: 'data-summary'
50
+ },
51
+ {
52
+ title: 'Rôle',
53
+ property: 'role',
54
+ component: 'data-text'
55
+ },
56
+ {
57
+ component: "data-spacer",
58
+ grow: true
59
+ },
60
+ {
61
+ component: 'actions',
62
+ componentOptions: {
63
+ options: [{ label: 'Voir', action: 'read' }, { label: 'Modifier', action: 'update' }, { label: 'Supprimer', action: 'remove' }]
64
+ }
65
+ },
66
+ ]
67
+
68
+ // Prepare reactive data
69
+ const limit = ref(25)
70
+
71
+ // Prepare computed
72
+ const records = computed(() => User.repo.all())
73
+ const filters = computed(() => [])
74
+ const abilities = computed(() => ['update', 'remove'])
75
+
76
+ // Prepare methods
77
+ const _fetchMore = async (skip = 0) => {
78
+ await User.list(undefined, undefined, { skip, limit: limit.value })
79
+ }
80
+
81
+ // Prepare methods
82
+ const _handle = async (action, data = {}) => {
83
+ const { _id } = data
84
+ switch (action) {
85
+ case 'create': {
86
+ // Une INVITATION est émise : l'email part côté serveur, et le lien est
87
+ // copié au presse-papier comme second recours.
88
+ // L'origine vient du navigateur, jamais d'une URL cuite au build —
89
+ // l'image d'instance est commune à tous les clients.
90
+ const { token } = await User.create(data)
91
+ const link = `${window.location.origin}/welcome/${token}`
92
+ try {
93
+ await navigator.clipboard.writeText(link)
94
+ notificationsStore.add({
95
+ icon: 'iconoir:mail-out',
96
+ type: 'success',
97
+ title: 'Lien d\'invitation copié dans le presse-papier (valable 7 jours).'
98
+ })
99
+ } catch {
100
+ console.log('Lien d\'invitation :', link)
101
+ notificationsStore.add({
102
+ icon: 'iconoir:mail-out',
103
+ type: 'success',
104
+ title: 'Invitation créée — lien affiché dans la console (valable 7 jours).'
105
+ })
106
+ }
107
+ await User.list()
108
+ break
109
+ }
110
+ case 'update':
111
+ await User.update(_id, data)
112
+ break
113
+ case 'remove':
114
+ await User.remove(data._id)
115
+ break
116
+ default:
117
+ throw new Error('E_UNHANDLED')
118
+ }
119
+ }
120
+
121
+ const _openModalForm = (action, record) => {
122
+ $modalStore.open({
123
+ onSubmit: async (data) => _handle(action, data),
124
+ details: {
125
+ title: `${action === 'create' ? 'Inviter' : 'Modifier'} un membre`,
126
+ subTitle: action === 'create'
127
+ ? 'Un lien d\'invitation sera généré pour ce nouveau membre.'
128
+ : 'Modifiez les informations du membre.',
129
+ acceptButton: `${action === 'create' ? 'Inviter' : 'Modifier'}`,
130
+ closeButton: 'Annuler'
131
+ },
132
+ attributes: action === 'create'
133
+ ? [
134
+ {
135
+ property: 'emailAddress',
136
+ componentOptions: {
137
+ label: 'Adresse mail',
138
+ name: 'emailAddress'
139
+ },
140
+ component: 'input-text'
141
+ },
142
+ {
143
+ property: 'role',
144
+ componentOptions: {
145
+ label: 'Rôle',
146
+ name: 'role',
147
+ items: ['admin', 'redacteur'],
148
+ object: false
149
+ },
150
+ component: 'select-enum'
151
+ }
152
+ ]
153
+ : [
154
+ {
155
+ property: 'firstName',
156
+ componentOptions: {
157
+ label: 'Nom d\'affichage',
158
+ name: 'firstName'
159
+ },
160
+ component: 'input-text'
161
+ },
162
+ {
163
+ property: 'role',
164
+ componentOptions: {
165
+ label: 'Rôle',
166
+ name: 'role',
167
+ items: ['admin', 'redacteur'],
168
+ object: false
169
+ },
170
+ component: 'select-enum'
171
+ }
172
+ ],
173
+ record
174
+ })
175
+ }
176
+ </script>
@@ -0,0 +1,192 @@
1
+ <template>
2
+ <div class="relative block w-full">
3
+ <!-- Instance single-tenant : une seule collectivité par déploiement. -->
4
+ <p class="mb-6 rounded-md bg-amber-50 border border-amber-200 px-4 py-3 text-sm text-amber-800">
5
+ Cette instance héberge une seule collectivité — la création d'organisations n'est plus disponible.
6
+ </p>
7
+
8
+ <data-list :items="sortedOrganizations" :density="1" :density-children="2" :group-by="'type'" :item-value="'type'" class="space-y-4 bg-gray-50 p-2 rounded-md">
9
+ <template #item="{ item }">
10
+ <h3 class="text-gray-400 text-sm font-medium mb-2 pl-2">
11
+ @ {{ item.type }}
12
+ </h3>
13
+ </template>
14
+ <template #item-child="{ item: itemChild }">
15
+ <data-list-row
16
+ :item="itemChild"
17
+ :to="`/${itemChild._id}`"
18
+ :headers="ROW_DEFINITION"
19
+ class="border border-gray-100 bg-white shadow-sm rounded-lg"
20
+ @click="_handleRowEvent"
21
+ />
22
+ </template>
23
+ </data-list>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import ButtonSecondary from '~/components/elements/buttons/secondary'
29
+ import dataList from '~/components/data/lists/data-list'
30
+ import dataListRow from '~/components/data/display/row'
31
+ import { useWorkspaceStore } from '~/store/layout/workspace'
32
+ import { useModalStore } from '~/store/layout/modal'
33
+
34
+ definePageMeta({
35
+ permissions: ['manage:all'],
36
+ preFetch: ['Organization'],
37
+ headings: {
38
+ title: 'Organisations',
39
+ description: 'Retrouvez & managez ici toutes les organisations.'
40
+ }
41
+ })
42
+
43
+ const { Organization } = useModels()
44
+ const modalStore = useModalStore()
45
+
46
+ // Prepare constants
47
+ const ORGANIZATIONS_ORDER = ['customer', 'network', 'country', 'bu', 'root'];
48
+ const ROW_DEFINITION = [
49
+ {
50
+ component: 'data-avatar',
51
+ nestedValue: false
52
+ },
53
+ {
54
+ nestedValue: false,
55
+ componentOptions: {
56
+ titleKey: 'name',
57
+ descriptionKey: 'path',
58
+ },
59
+ component: 'data-summary'
60
+ },
61
+ {
62
+ component: "data-spacer",
63
+ grow: true
64
+ },
65
+ {
66
+ component: 'actions',
67
+ componentOptions: {
68
+ options: [{ label: 'Voir', action: 'read' }]
69
+ }
70
+ }
71
+ ]
72
+
73
+ // Prepare data
74
+ const { data: organizations } = await useAsyncData('overview', () => Organization.list())
75
+
76
+ // Prepare computed
77
+ const sortedOrganizations = computed(() => organizations.value.sort((a, b) => ORGANIZATIONS_ORDER.indexOf(a.type) - ORGANIZATIONS_ORDER.indexOf(b.type)))
78
+
79
+ // Prepare methods
80
+ const _handleRowEvent = async (eventName, value) => {
81
+ console.log(eventName, value)
82
+ }
83
+ // import { UPDATE_ORGANIZATION } from '~/store/actions'
84
+ // import buttonSecondary from '~/components/elements/buttons/secondary'
85
+
86
+ // export default {
87
+ // name: 'Organizations',
88
+ // meta: {
89
+ // permissions: ['entrance:self'],
90
+ // preFetch: ['Organization'],
91
+ // headings: {
92
+ // title: 'Organisations',
93
+ // description: 'Retrouvez & managez ici toutes les organisations.'
94
+ // }
95
+ // },
96
+ // components: { buttonSecondary },
97
+ // computed: {
98
+ // organizations () {
99
+ // return this.$api.Organization.all()
100
+ // }
101
+ // },
102
+ // beforeDestroy () {
103
+ // this.$nuxt.$off('modalCreateOrganization')
104
+ // },
105
+ // mounted () {
106
+ // this.$nuxt.$on('modalCreateOrganization', this.createOrganization)
107
+ // },
108
+ // methods: {
109
+ // async updateSelectedOrganization (organizationRecord) {
110
+ // try {
111
+ // await this.$store.dispatch(UPDATE_ORGANIZATION, organizationRecord)
112
+ // this.$router.replace(`${organizationRecord.slug}/`)
113
+ // } catch (err) {
114
+ // console.log(err) // eslint-disable-line
115
+ // }
116
+ // },
117
+ // openModalForm (record) {
118
+ // this.$nuxt.$emit('openModalForm', {
119
+ // submitEvent: 'modalCreateOrganization',
120
+ // details: {
121
+ // title: 'Créer une nouvelle organisation',
122
+ // subTitle: 'Renseignez cette nouvelle entité.'
123
+ // },
124
+ // attributes: [
125
+ // // directParent
126
+ // {
127
+ // property: 'directParent',
128
+ // componentOptions: {
129
+ // placeHolder: 'Parent de l\'organisation',
130
+ // name: 'directParent',
131
+ // model: 'Organization',
132
+ // object: true,
133
+ // itemKey: 'name',
134
+ // itemValue: 'slug'
135
+ // },
136
+ // component: 'select-model'
137
+ // },
138
+ // {
139
+ // property: 'type',
140
+ // componentOptions: {
141
+ // placeHolder: 'Type d\'organisation',
142
+ // name: 'type',
143
+ // items: [
144
+ // 'bu',
145
+ // 'country',
146
+ // 'network',
147
+ // 'customer'
148
+ // ]
149
+ // },
150
+ // component: 'select-enum'
151
+ // },
152
+ // {
153
+ // property: 'slug',
154
+ // componentOptions: {
155
+ // placeHolder: 'Identifiant de l\'organisation (slug)',
156
+ // name: 'slug'
157
+ // },
158
+ // component: 'input-text'
159
+ // },
160
+ // {
161
+ // property: 'name',
162
+ // componentOptions: {
163
+ // placeHolder: 'Nom de l\'organisation',
164
+ // name: 'name'
165
+ // },
166
+ // component: 'input-text'
167
+ // },
168
+ // {
169
+ // property: 'type',
170
+ // componentOptions: {
171
+ // placeHolder: 'Localisation',
172
+ // name: 'organization'
173
+ // },
174
+ // component: 'input-location'
175
+ // }
176
+ // ],
177
+ // record
178
+ // })
179
+ // },
180
+ // async createOrganization (data = {}) {
181
+ // this.$nuxt.$loading.start()
182
+ // try {
183
+ // await this.$api.Organization.create(data)
184
+ // } catch (err) {
185
+ // console.log(err)
186
+ // } finally {
187
+ // this.$nuxt.$loading.finish()
188
+ // }
189
+ // }
190
+ // }
191
+ // }
192
+ </script>