nodebb-plugin-calendar-onekite 11.1.25 → 11.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js CHANGED
@@ -101,6 +101,7 @@ Plugin.init = async function (params) {
101
101
 
102
102
  // Add to admin navigation
103
103
  Plugin.addAdminNavigation = async function (data) {
104
+ data.plugins = data.plugins || [];
104
105
  data.plugins.push({
105
106
  route: '/plugins/calendar-onekite',
106
107
  icon: 'fa-calendar',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.25",
3
+ "version": "11.1.26",
4
4
  "description": "Calendar reservations with HelloAsso integration for OneKite",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/public/client.js CHANGED
@@ -1,265 +1,139 @@
1
1
  'use strict';
2
-
3
2
  /* global FullCalendar */
4
3
 
5
- define('forum/calendar-onekite', ['api', 'alerts', 'hooks', 'bootbox'], function (api, alerts, hooks, bootbox) {
4
+ define('forum/calendar-onekite', ['api', 'alerts'], function (api, alerts) {
6
5
  let calendar;
7
6
 
8
- function ymdToTs(ymd) {
9
- // ymd in YYYY-MM-DD, treat as UTC midnight
10
- const [y, m, d] = ymd.split('-').map(Number);
11
- return Date.UTC(y, m - 1, d, 0, 0, 0, 0);
12
- }
13
-
14
- function daysBetween(startYmd, endYmd) {
15
- const start = ymdToTs(startYmd);
16
- const end = ymdToTs(endYmd);
17
- const dayMs = 24 * 3600 * 1000;
18
- return Math.max(1, Math.round((end - start) / dayMs));
19
- }
20
-
21
- async function fetchJson(url, opts) {
22
- try {
23
- return await api[opts && opts.method === 'POST' ? 'post' : 'get'](url, opts && opts.body ? opts.body : undefined);
24
- } catch (e) {
25
- // fallback to fetch if api module not compatible
26
- const res = await fetch(url, {
27
- method: (opts && opts.method) || 'GET',
28
- headers: { 'Content-Type': 'application/json' },
29
- body: opts && opts.body ? JSON.stringify(opts.body) : undefined,
30
- credentials: 'same-origin',
31
- });
32
- const j = await res.json().catch(() => ({}));
33
- if (!res.ok) throw Object.assign(new Error('http'), { status: res.status, body: j });
34
- return j;
35
- }
36
- }
37
-
38
- async function loadItems() {
39
- const r = await fetchJson('/plugins/calendar-onekite/items');
40
- return (r && r.items) || [];
7
+ function showError(msg) {
8
+ try { alerts.error(msg); } catch (e) { console.error(msg); }
41
9
  }
42
10
 
43
- function formatEuro(cents) {
44
- return (Number(cents || 0) / 100).toFixed(2) + ' €';
11
+ function daysBetween(startStr, endStr) {
12
+ // start/end are YYYY-MM-DD strings, end is exclusive
13
+ const s = new Date(startStr + 'T00:00:00Z');
14
+ const e = new Date(endStr + 'T00:00:00Z');
15
+ const ms = e.getTime() - s.getTime();
16
+ return Math.max(1, Math.round(ms / (24 * 3600 * 1000)));
45
17
  }
46
18
 
47
- function buildItemsList(items) {
48
- const rows = items.map(it => {
49
- return `<label style="display:flex;gap:8px;align-items:center;cursor:pointer;margin:6px 0;">
50
- <input type="checkbox" class="onekite-item" value="${String(it.id).replace(/"/g, '&quot;')}">
51
- <span style="flex:1;">${String(it.name)}</span>
52
- <span>${formatEuro(it.priceCents)}</span>
53
- </label>`;
54
- }).join('');
55
- return `<div id="onekite-items">${rows}</div>`;
19
+ async function fetchCatalog() {
20
+ const res = await api.get('/plugins/calendar-onekite/items');
21
+ return (res && res.items) || [];
56
22
  }
57
23
 
58
- function getSelectedIds(root) {
59
- return Array.from(root.querySelectorAll('input.onekite-item:checked')).map(el => el.value);
60
- }
61
-
62
- function sumSelected(items, ids) {
63
- const map = new Map(items.map(i => [String(i.id), i]));
64
- return ids.reduce((s, id) => s + Number((map.get(String(id)) || {}).priceCents || 0), 0);
24
+ function formatEuroFromCents(cents) {
25
+ const v = (Number(cents || 0) / 100).toFixed(2).replace('.', ',');
26
+ return v + ' €';
65
27
  }
66
28
 
67
29
  async function openRequestModal(selectionInfo) {
68
- const startYmd = selectionInfo.startStr;
69
- const endYmd = selectionInfo.endStr; // exclusive
70
- const days = daysBetween(startYmd, endYmd);
71
-
72
- let items = [];
73
30
  try {
74
- items = await loadItems();
75
- } catch (e) {
76
- alerts.error('Impossible de charger le matériel (HelloAsso).');
77
- calendar.unselect();
78
- return;
79
- }
80
-
81
- if (!items.length) {
82
- alerts.error('Aucun matériel disponible (catalogue HelloAsso vide).');
83
- calendar.unselect();
84
- return;
85
- }
86
-
87
- const body = document.createElement('div');
88
- body.innerHTML = `
89
- <p><strong>Période :</strong> ${startYmd} → ${endYmd} (${days} jour(s))</p>
90
- <p><strong>Matériel :</strong></p>
91
- ${buildItemsList(items)}
92
- <hr>
93
- <p><strong>Total estimé :</strong> <span id="onekite-total">${formatEuro(0)}</span></p>
94
- <p class="help-block">Astuce : coche plusieurs matériels. Ctrl/Cmd+clic sur le libellé fonctionne aussi.</p>
95
- `;
96
-
97
- const updateTotal = () => {
98
- const ids = getSelectedIds(body);
99
- const perDay = sumSelected(items, ids);
100
- body.querySelector('#onekite-total').textContent = formatEuro(perDay * days);
101
- };
102
-
103
- body.addEventListener('change', (e) => {
104
- if (e.target && e.target.classList.contains('onekite-item')) updateTotal();
105
- });
106
- body.addEventListener('click', (e) => {
107
- const label = e.target.closest('label');
108
- if (!label) return;
109
- const cb = label.querySelector('input.onekite-item');
110
- if (!cb) return;
111
- if (e.ctrlKey || e.metaKey) {
112
- cb.checked = !cb.checked;
113
- updateTotal();
31
+ const items = await fetchCatalog();
32
+ if (!items.length) {
33
+ showError('Aucun matériel disponible (catalogue HelloAsso non chargé).');
34
+ return;
114
35
  }
115
- });
116
-
117
- updateTotal();
118
36
 
119
- bootbox.dialog({
120
- title: 'Demande de réservation',
121
- message: body,
122
- buttons: {
123
- cancel: {
124
- label: 'Annuler',
125
- className: 'btn-default',
126
- callback: function () {
127
- calendar.unselect();
128
- },
129
- },
130
- ok: {
131
- label: 'Envoyer la demande',
132
- className: 'btn-primary',
133
- callback: async function () {
134
- const ids = getSelectedIds(body);
135
- if (!ids.length) {
136
- alerts.error('Sélectionne au moins un matériel.');
137
- return false;
138
- }
139
- try {
140
- await fetchJson('/plugins/calendar-onekite/reservations', {
141
- method: 'POST',
142
- body: {
143
- startTs: ymdToTs(startYmd),
144
- endTs: ymdToTs(endYmd),
145
- itemIds: ids,
146
- },
147
- });
148
- alerts.success('Demande envoyée.');
149
- calendar.refetchEvents();
150
- calendar.unselect();
151
- } catch (err) {
152
- if (err && err.status === 409) {
153
- alerts.error('Conflit : un des matériels est déjà réservé sur cette période.');
154
- } else if (err && err.body && err.body.error) {
155
- alerts.error(err.body.error);
156
- } else {
157
- alerts.error('Erreur lors de la demande.');
158
- }
159
- calendar.unselect();
160
- return false;
161
- }
162
- },
163
- },
164
- },
165
- });
166
- }
37
+ const start = selectionInfo.startStr;
38
+ const end = selectionInfo.endStr;
39
+ const nbDays = daysBetween(start, end);
40
+
41
+ // Build simple modal UI (no jquery)
42
+ const modal = document.createElement('div');
43
+ modal.className = 'onekite-modal';
44
+ modal.innerHTML = `
45
+ <div class="onekite-modal__backdrop"></div>
46
+ <div class="onekite-modal__panel">
47
+ <h4>Nouvelle demande</h4>
48
+ <div style="margin-bottom:8px;">Du <strong>${start}</strong> au <strong>${end}</strong> (<strong>${nbDays}</strong> jour(s))</div>
49
+ <div style="max-height:240px; overflow:auto; border:1px solid #ddd; padding:8px; border-radius:6px;">
50
+ ${items.map((it, idx) => `
51
+ <label style="display:flex; align-items:center; gap:8px; margin:6px 0; cursor:pointer;">
52
+ <input type="checkbox" class="onekite-item" data-id="${it.id}" data-price="${it.priceCents || 0}">
53
+ <span>${it.name}</span>
54
+ <span style="margin-left:auto; opacity:.8;">${formatEuroFromCents(it.priceCents || 0)}/jour</span>
55
+ </label>
56
+ `).join('')}
57
+ </div>
58
+ <div style="margin-top:10px; display:flex; justify-content:space-between; align-items:center;">
59
+ <div>Estimation total : <strong id="onekite-total">0 €</strong></div>
60
+ <div style="display:flex; gap:8px;">
61
+ <button class="btn btn-default" id="onekite-cancel">Annuler</button>
62
+ <button class="btn btn-primary" id="onekite-submit">Envoyer</button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ `;
67
+ document.body.appendChild(modal);
68
+
69
+ const updateTotal = () => {
70
+ const checks = Array.from(modal.querySelectorAll('.onekite-item')).filter(i => i.checked);
71
+ const daily = checks.reduce((sum, el) => sum + Number(el.dataset.price || 0), 0);
72
+ const total = daily * nbDays;
73
+ modal.querySelector('#onekite-total').textContent = formatEuroFromCents(total);
74
+ };
75
+ modal.querySelectorAll('.onekite-item').forEach(el => el.addEventListener('change', updateTotal));
76
+ updateTotal();
167
77
 
168
- async function openEventModal(info) {
169
- const ev = info.event;
170
- const props = ev.extendedProps || {};
171
- const status = props.status || 'pending';
172
- const items = (props.items || []).map(i => i.name).join(', ');
173
- const days = props.days || '';
174
- const total = props.totalCents != null ? formatEuro(props.totalCents) : '';
78
+ const close = () => { modal.remove(); calendar && calendar.unselect(); };
175
79
 
176
- const body = document.createElement('div');
177
- body.innerHTML = `
178
- <p><strong>Statut :</strong> ${status}</p>
179
- <p><strong>Matériel :</strong> ${items}</p>
180
- ${days ? `<p><strong>Jours :</strong> ${days}</p>` : ''}
181
- ${total ? `<p><strong>Total :</strong> ${total}</p>` : ''}
182
- `;
80
+ modal.querySelector('#onekite-cancel').addEventListener('click', close);
81
+ modal.querySelector('.onekite-modal__backdrop').addEventListener('click', close);
183
82
 
184
- const buttons = {
185
- close: {
186
- label: 'Fermer',
187
- className: 'btn-default',
188
- },
189
- };
190
-
191
- // Show approve/refuse buttons if server allows (we try; if 403, show error)
192
- if (status === 'pending') {
193
- buttons.refuse = {
194
- label: 'Refuser',
195
- className: 'btn-danger',
196
- callback: async function () {
197
- try {
198
- await fetchJson(`/plugins/calendar-onekite/reservations/${encodeURIComponent(ev.id)}/refuse`, { method: 'POST', body: {} });
199
- alerts.success('Réservation refusée.');
200
- calendar.refetchEvents();
201
- } catch (e) {
202
- alerts.error("Impossible de refuser (droits requis).");
203
- }
204
- },
205
- };
206
- buttons.approve = {
207
- label: 'Valider',
208
- className: 'btn-success',
209
- callback: async function () {
210
- try {
211
- const r = await fetchJson(`/plugins/calendar-onekite/reservations/${encodeURIComponent(ev.id)}/approve`, { method: 'POST', body: {} });
212
- alerts.success('Réservation validée.');
213
- if (r && r.paymentUrl) {
214
- alerts.success('Lien de paiement envoyé.');
215
- }
216
- calendar.refetchEvents();
217
- } catch (e) {
218
- alerts.error("Impossible de valider (droits requis).");
219
- }
220
- },
221
- };
83
+ modal.querySelector('#onekite-submit').addEventListener('click', async () => {
84
+ const selected = Array.from(modal.querySelectorAll('.onekite-item')).filter(i => i.checked).map(i => i.dataset.id);
85
+ if (!selected.length) {
86
+ showError('Sélectionne au moins un matériel.');
87
+ return;
88
+ }
89
+ try {
90
+ await api.post('/plugins/calendar-onekite/reservations', { start, end, itemIds: selected });
91
+ try { alerts.success('Demande envoyée'); } catch {}
92
+ close();
93
+ await calendar.refetchEvents();
94
+ } catch (err) {
95
+ const msg = (err && err.message) ? err.message : 'Impossible de créer la demande';
96
+ showError(msg);
97
+ }
98
+ });
99
+ } catch (e) {
100
+ showError('Impossible de charger le catalogue.');
101
+ console.error(e);
222
102
  }
223
-
224
- bootbox.dialog({
225
- title: 'Réservation',
226
- message: body,
227
- buttons,
228
- });
229
103
  }
230
104
 
231
- function initCalendar() {
105
+ async function initCalendar() {
232
106
  const el = document.getElementById('onekite-calendar');
233
- if (!el || !window.FullCalendar) return;
107
+ if (!el || typeof FullCalendar === 'undefined') {
108
+ return;
109
+ }
234
110
 
235
111
  calendar = new FullCalendar.Calendar(el, {
236
- locale: 'fr',
237
112
  initialView: 'dayGridMonth',
113
+ locale: 'fr',
238
114
  selectable: true,
239
115
  selectMirror: true,
240
116
  displayEventTime: false,
241
- height: 'auto',
242
- select: function (info) {
243
- openRequestModal(info);
244
- },
245
- eventClick: function (info) {
246
- info.jsEvent.preventDefault();
247
- openEventModal(info);
248
- },
249
- events: async function (fetchInfo, success, failure) {
117
+ eventTimeFormat: { hour: '2-digit', minute: '2-digit' },
118
+ select: (info) => openRequestModal(info),
119
+ events: async (fetchInfo, successCallback, failureCallback) => {
250
120
  try {
251
- const startTs = ymdToTs(fetchInfo.startStr);
252
- const endTs = ymdToTs(fetchInfo.endStr);
253
- const r = await fetchJson(`/plugins/calendar-onekite/events?startTs=${startTs}&endTs=${endTs}`);
254
- success((r && r.events) || []);
121
+ const qs = new URLSearchParams({ start: fetchInfo.startStr, end: fetchInfo.endStr });
122
+ const res = await fetch('/api/v3/plugins/calendar-onekite/events?' + qs.toString(), { credentials: 'same-origin' });
123
+ if (!res.ok) throw new Error('events-fetch-failed');
124
+ const json = await res.json();
125
+ const events = (json.events || []).map(ev => ({
126
+ id: ev.id,
127
+ title: ev.title,
128
+ start: ev.start,
129
+ end: ev.end,
130
+ allDay: true,
131
+ extendedProps: ev.extendedProps || {},
132
+ }));
133
+ successCallback(events);
255
134
  } catch (e) {
256
- failure(e);
257
- }
258
- },
259
- eventDidMount: function (arg) {
260
- const st = (arg.event.extendedProps && arg.event.extendedProps.status) || '';
261
- if (st === 'pending' || st === 'awaiting_payment') {
262
- arg.el.title = 'En attente';
135
+ console.error(e);
136
+ failureCallback(e);
263
137
  }
264
138
  },
265
139
  });
@@ -267,19 +141,22 @@ define('forum/calendar-onekite', ['api', 'alerts', 'hooks', 'bootbox'], function
267
141
  calendar.render();
268
142
  }
269
143
 
270
- function maybeInit() {
271
- // Ensure we only init on our template
272
- try {
273
- if (ajaxify && ajaxify.data && ajaxify.data.template && ajaxify.data.template.name !== 'calendar-onekite') return;
274
- } catch (e) { /* ignore */ }
275
- initCalendar();
144
+ function onAjaxifyEnd(_ev, data) {
145
+ if (data && data.tpl === 'calendar-onekite') {
146
+ initCalendar().catch(console.error);
147
+ }
276
148
  }
277
149
 
278
- hooks.on('action:ajaxify.end', function () {
279
- maybeInit();
280
- });
150
+ // Auto-init on page load & ajaxify
151
+ if (window && window.addEventListener) {
152
+ window.addEventListener('action:ajaxify.end', (ev) => onAjaxifyEnd(ev, ev && ev.detail ? ev.detail : undefined));
153
+ }
154
+ // NodeBB uses jQuery-triggered events too, but jQuery isn't guaranteed. If present, hook it.
155
+ if (typeof window !== 'undefined' && window.jQuery) {
156
+ window.jQuery(window).on('action:ajaxify.end', function (_ev, data) { onAjaxifyEnd(_ev, data); });
157
+ }
281
158
 
282
159
  return {
283
- init: maybeInit,
160
+ init: initCalendar,
284
161
  };
285
162
  });
@@ -11,6 +11,3 @@
11
11
  <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
12
12
  <script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.11/locales-all.global.min.js"></script>
13
13
 
14
- <script>
15
- require(['forum/calendar-onekite'], function (mod) { mod.init(); });
16
- </script>
package/README.md DELETED
@@ -1,38 +0,0 @@
1
- # nodebb-plugin-calendar-onekite
2
-
3
- Plugin NodeBB (v4.7.x) : calendrier de réservation de matériel basé sur FullCalendar, workflow de validation via ACP, et génération de lien de paiement HelloAsso.
4
-
5
- ## Installation
6
-
7
- Dans le dossier NodeBB :
8
-
9
- ```bash
10
- npm install /path/to/nodebb-plugin-calendar-onekite
11
- # ou si vous avez un zip, dézippez dans node_modules puis :
12
- ./nodebb build
13
- ./nodebb restart
14
- ```
15
-
16
- Activez ensuite le plugin dans l’ACP.
17
-
18
- ## Page
19
-
20
- - URL : `/calendar` (ex: https://www.onekite.com/calendar)
21
-
22
- ## Paramètres ACP
23
-
24
- ACP → Plugins → Calendar OneKite
25
-
26
- - `allowedGroups` : groupes autorisés à créer une demande (séparés par virgules)
27
- - `notifyGroups` : groupes notifiés par email
28
- - `pendingHoldMinutes` : durée de blocage d’une demande en attente
29
- - HelloAsso :
30
- - `helloassoEnv` : sandbox/prod
31
- - `helloassoClientId` / `helloassoClientSecret`
32
- - `helloassoOrganizationSlug` / `helloassoFormType` / `helloassoFormSlug`
33
- - `helloassoReturnUrl` (optionnel)
34
-
35
- ## Notes
36
-
37
- - Les demandes sont créées en statut `pending` puis expirent automatiquement après `pendingHoldMinutes`.
38
- - La validation admin crée un checkout-intent HelloAsso et envoie le lien de paiement au demandeur.
@@ -1,11 +0,0 @@
1
- 'use strict';
2
-
3
- const controllers = {};
4
-
5
- controllers.renderCalendar = async function (req, res) {
6
- res.render('calendar-onekite', {
7
- title: 'Calendar',
8
- });
9
- };
10
-
11
- module.exports = controllers;
package/lib/scheduler.js DELETED
@@ -1,50 +0,0 @@
1
- 'use strict';
2
-
3
- const meta = require.main.require('./src/meta');
4
- const dbLayer = require('./db');
5
-
6
- let timer = null;
7
-
8
- async function expirePending() {
9
- const settings = await meta.settings.get('calendar-onekite');
10
- const holdMins = parseInt(settings.pendingHoldMinutes || '5', 10) || 5;
11
- const now = Date.now();
12
-
13
- const ids = await dbLayer.listAllReservationIds(5000);
14
- if (!ids || !ids.length) {
15
- return;
16
- }
17
-
18
- for (const rid of ids) {
19
- const resv = await dbLayer.getReservation(rid);
20
- if (!resv || resv.status !== 'pending') {
21
- continue;
22
- }
23
- const createdAt = parseInt(resv.createdAt, 10) || 0;
24
- const expiresAt = createdAt + holdMins * 60 * 1000;
25
- if (now > expiresAt) {
26
- // Expire (remove from calendar)
27
- await dbLayer.removeReservation(rid);
28
- }
29
- }
30
- }
31
-
32
- function start() {
33
- if (timer) return;
34
- timer = setInterval(() => {
35
- expirePending().catch(() => {});
36
- }, 60 * 1000);
37
- }
38
-
39
- function stop() {
40
- if (timer) {
41
- clearInterval(timer);
42
- timer = null;
43
- }
44
- }
45
-
46
- module.exports = {
47
- start,
48
- stop,
49
- expirePending,
50
- };
package/lib/settings.js DELETED
@@ -1,36 +0,0 @@
1
- 'use strict';
2
-
3
- const Settings = require.main.require('./src/settings');
4
-
5
- const defaults = {
6
- // Who can create reservations (comma-separated group names)
7
- allowedGroups: 'registered-users',
8
- // Who receives email notifications for new pending reservations (comma-separated group names)
9
- notifyGroups: 'administrators',
10
-
11
- // How long a "pending" reservation blocks equipment (minutes)
12
- pendingHoldMinutes: 5,
13
-
14
- // Default reservation status for newly created (pending)
15
- // Also used by cleanup job
16
- cleanupIntervalSeconds: 60,
17
-
18
- // HelloAsso
19
- helloassoEnv: 'sandbox', // sandbox | prod
20
- helloassoClientId: '',
21
- helloassoClientSecret: '',
22
- helloassoOrganizationSlug: '',
23
- helloassoFormType: 'event', // event | membership | donation | crowdfunding | paymentform
24
- helloassoFormSlug: '',
25
- helloassoReturnUrl: '',
26
-
27
- // UX
28
- showUsernamesOnCalendar: false,
29
- };
30
-
31
- const settings = new Settings('calendar-onekite', '1.0.0', defaults);
32
-
33
- module.exports = {
34
- settings,
35
- defaults,
36
- };
@@ -1,8 +0,0 @@
1
- #onekite-calendar {
2
- margin-top: 1rem;
3
- .fc {
4
- .fc-event {
5
- cursor: pointer;
6
- }
7
- }
8
- }
@@ -1,29 +0,0 @@
1
- <div class="container">
2
- <div class="row">
3
- <div class="col-12">
4
- <h1>Calendrier des réservations</h1>
5
- <p class="text-muted">Cliquez sur une date, ou sélectionnez une plage, pour faire une demande de réservation.</p>
6
- <div id="onekite-calendar"></div>
7
- </div>
8
- </div>
9
- </div>
10
-
11
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.css" />
12
-
13
- <script defer src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
14
- <script defer src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.11/locales-all.global.min.js"></script>
15
-
16
- <script>
17
- (function () {
18
- function boot() {
19
- if (!window.require) return setTimeout(boot, 50);
20
- window.require(['forum/calendar-onekite'], function (CalendarOneKite) {
21
- CalendarOneKite.init({
22
- el: '#onekite-calendar',
23
- locale: (ajaxify && ajaxify.data && ajaxify.data.config && ajaxify.data.config.userLang) || 'fr',
24
- });
25
- });
26
- }
27
- boot();
28
- })();
29
- </script>
@@ -1,10 +0,0 @@
1
- <p>Bonjour {username},</p>
2
- <p>Votre réservation a été validée.</p>
3
- <ul>
4
- <li>Matériel : {itemName}</li>
5
- <li>Début : {start}</li>
6
- <li>Fin : {end}</li>
7
- </ul>
8
- <!-- IF paymentUrl -->
9
- <p>Lien de paiement : <a href="{paymentUrl}">{paymentUrl}</a></p>
10
- <!-- ENDIF paymentUrl -->
@@ -1,10 +0,0 @@
1
- <p>Bonjour {username},</p>
2
- <p>Nouvelle demande de réservation :</p>
3
- <ul>
4
- <li>Demandeur : {requester}</li>
5
- <li>Matériel : {itemName}</li>
6
- <li>Début : {start}</li>
7
- <li>Fin : {end}</li>
8
- <li>ID : {rid}</li>
9
- </ul>
10
- <p>Validation/refus via l’ACP.</p>
@@ -1,7 +0,0 @@
1
- <p>Bonjour {username},</p>
2
- <p>Votre demande de réservation a été refusée.</p>
3
- <ul>
4
- <li>Matériel : {itemName}</li>
5
- <li>Début : {start}</li>
6
- <li>Fin : {end}</li>
7
- </ul>