@saltcorn/reservable 0.1.2 → 0.1.4
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/available-feed.js +99 -8
- package/index.js +1 -0
- package/package.json +1 -1
- package/validate.js +73 -0
package/available-feed.js
CHANGED
|
@@ -56,6 +56,8 @@ const configuration_workflow = (req) =>
|
|
|
56
56
|
|
|
57
57
|
const reservable_entity_fields = fields.filter((f) => f.is_fkey);
|
|
58
58
|
const show_view_opts = {};
|
|
59
|
+
const slots_available_field = {};
|
|
60
|
+
const distinct_slot_fields = new Set();
|
|
59
61
|
for (const rfield of reservable_entity_fields) {
|
|
60
62
|
const retable = Table.findOne(rfield.reftable_name);
|
|
61
63
|
const show_views = await View.find_table_views_where(
|
|
@@ -66,6 +68,13 @@ const configuration_workflow = (req) =>
|
|
|
66
68
|
state_fields.some((sf) => sf.name === "id")
|
|
67
69
|
);
|
|
68
70
|
show_view_opts[rfield.name] = show_views.map((v) => v.name);
|
|
71
|
+
slots_available_field[rfield.name] = retable.fields
|
|
72
|
+
.filter((f) => f.type?.name === "Integer")
|
|
73
|
+
.map((f) => f.name);
|
|
74
|
+
slots_available_field[rfield.name].forEach((v) =>
|
|
75
|
+
distinct_slot_fields.add(v)
|
|
76
|
+
);
|
|
77
|
+
slots_available_field[rfield.name].unshift("");
|
|
69
78
|
}
|
|
70
79
|
return new Form({
|
|
71
80
|
fields: [
|
|
@@ -78,6 +87,17 @@ const configuration_workflow = (req) =>
|
|
|
78
87
|
options: reservable_entity_fields.map((f) => f.name),
|
|
79
88
|
},
|
|
80
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: "valid_field",
|
|
92
|
+
label: "Valid reservation field",
|
|
93
|
+
sublabel: "Only consider reservations with this field valid",
|
|
94
|
+
type: "String",
|
|
95
|
+
attributes: {
|
|
96
|
+
options: fields
|
|
97
|
+
.filter((f) => f.type.name === "Bool")
|
|
98
|
+
.map((f) => f.name),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
81
101
|
{
|
|
82
102
|
name: "start_field",
|
|
83
103
|
label: "Start date field",
|
|
@@ -93,13 +113,14 @@ const configuration_workflow = (req) =>
|
|
|
93
113
|
name: "end_field",
|
|
94
114
|
label: "End date field",
|
|
95
115
|
type: "String",
|
|
116
|
+
required: true,
|
|
96
117
|
attributes: {
|
|
97
118
|
options: fields
|
|
98
119
|
.filter((f) => f.type.name === "Date")
|
|
99
120
|
.map((f) => f.name),
|
|
100
121
|
},
|
|
101
122
|
},
|
|
102
|
-
{
|
|
123
|
+
/*{
|
|
103
124
|
name: "duration_field",
|
|
104
125
|
label: "Duration field",
|
|
105
126
|
sublabel: "Integer field holding booked duration in minutes",
|
|
@@ -109,7 +130,7 @@ const configuration_workflow = (req) =>
|
|
|
109
130
|
.filter((f) => f.type.name === "Integer")
|
|
110
131
|
.map((f) => f.name),
|
|
111
132
|
},
|
|
112
|
-
},
|
|
133
|
+
}, */
|
|
113
134
|
{
|
|
114
135
|
name: "show_view",
|
|
115
136
|
label: req.__("Single item view"),
|
|
@@ -129,6 +150,29 @@ const configuration_workflow = (req) =>
|
|
|
129
150
|
calcOptions: ["reservable_entity_key", show_view_opts],
|
|
130
151
|
},
|
|
131
152
|
},
|
|
153
|
+
{
|
|
154
|
+
name: "slots_available_field",
|
|
155
|
+
label: "Slots available field",
|
|
156
|
+
sublabel:
|
|
157
|
+
"Field with a number of available instances of the reservable entity. If blank, treat as one per entity.",
|
|
158
|
+
type: "String",
|
|
159
|
+
attributes: {
|
|
160
|
+
calcOptions: ["reservable_entity_key", slots_available_field],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "slot_count_field",
|
|
165
|
+
label: "Slots taken field",
|
|
166
|
+
sublabel:
|
|
167
|
+
"Field with the number of entities reserved. If blank, treat as one per entity.",
|
|
168
|
+
type: "String",
|
|
169
|
+
showIf: { slots_available_field: [...distinct_slot_fields] },
|
|
170
|
+
attributes: {
|
|
171
|
+
options: fields
|
|
172
|
+
.filter((f) => f.type.name === "Integer")
|
|
173
|
+
.map((f) => f.name),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
132
176
|
],
|
|
133
177
|
});
|
|
134
178
|
},
|
|
@@ -136,10 +180,20 @@ const configuration_workflow = (req) =>
|
|
|
136
180
|
],
|
|
137
181
|
});
|
|
138
182
|
|
|
183
|
+
const first = (xs) => (Array.isArray(xs) ? xs[0] : xs);
|
|
184
|
+
|
|
139
185
|
const run = async (
|
|
140
186
|
table_id,
|
|
141
187
|
viewname,
|
|
142
|
-
{
|
|
188
|
+
{
|
|
189
|
+
reservable_entity_key,
|
|
190
|
+
valid_field,
|
|
191
|
+
slot_count_field,
|
|
192
|
+
slots_available_field,
|
|
193
|
+
show_view,
|
|
194
|
+
start_field,
|
|
195
|
+
end_field,
|
|
196
|
+
},
|
|
143
197
|
state,
|
|
144
198
|
extraArgs
|
|
145
199
|
) => {
|
|
@@ -160,9 +214,9 @@ const run = async (
|
|
|
160
214
|
table: restable,
|
|
161
215
|
});
|
|
162
216
|
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
217
|
+
if (valid_field) reswhere[valid_field] = true;
|
|
218
|
+
const reservations = await restable.getRows(reswhere);
|
|
219
|
+
const use_slots = slot_count_field || slots_available_field;
|
|
166
220
|
const sview = await View.findOne({ name: show_view });
|
|
167
221
|
if (!sview)
|
|
168
222
|
throw new InvalidConfiguration(
|
|
@@ -170,8 +224,45 @@ const run = async (
|
|
|
170
224
|
);
|
|
171
225
|
const srespAll = await sview.runMany(state, extraArgs);
|
|
172
226
|
const srespsAvailable = [];
|
|
173
|
-
|
|
174
|
-
|
|
227
|
+
|
|
228
|
+
if (!use_slots) {
|
|
229
|
+
const resEnts = new Set(reservations.map((r) => r[reservable_entity_key]));
|
|
230
|
+
for (const sresp of srespAll) {
|
|
231
|
+
if (!resEnts.has(sresp.row[retable.pk_name])) srespsAvailable.push(sresp);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
//console.log("state_res", state_res);
|
|
235
|
+
//console.log("reswhere", reswhere);
|
|
236
|
+
const to = new Date(first(reswhere[start_field])?.lt);
|
|
237
|
+
const from = new Date(first(reswhere[end_field])?.gt);
|
|
238
|
+
if (!from || !to) srespsAvailable.push(...srespAll);
|
|
239
|
+
else
|
|
240
|
+
for (const sresp of srespAll) {
|
|
241
|
+
const myreservations = reservations.filter(
|
|
242
|
+
(r) => r[reservable_entity_key] === sresp.row[retable.pk_name]
|
|
243
|
+
);
|
|
244
|
+
/*console.log({
|
|
245
|
+
taken: resEnts[sresp.row[retable.pk_name]] || 0,
|
|
246
|
+
available: sresp.row[slots_available_field],
|
|
247
|
+
});*/
|
|
248
|
+
const to = new Date(first(reswhere[start_field])?.lt);
|
|
249
|
+
const from = new Date(first(reswhere[end_field])?.gt);
|
|
250
|
+
let maxAvailable = sresp.row[slots_available_field];
|
|
251
|
+
for (let day = from; day <= to; day.setDate(day.getDate() + 1)) {
|
|
252
|
+
const active = myreservations.filter(
|
|
253
|
+
(r) => r[start_field] <= day && r[end_field] >= day
|
|
254
|
+
);
|
|
255
|
+
const taken = active
|
|
256
|
+
.map((r) => r[slot_count_field] || 1)
|
|
257
|
+
.reduce((a, b) => a + b, 0);
|
|
258
|
+
maxAvailable = Math.min(
|
|
259
|
+
maxAvailable,
|
|
260
|
+
sresp.row[slots_available_field] - taken
|
|
261
|
+
);
|
|
262
|
+
//console.log({ car: sresp.row.name, day, maxAvailable });
|
|
263
|
+
}
|
|
264
|
+
if (maxAvailable > 0) srespsAvailable.push(sresp);
|
|
265
|
+
}
|
|
175
266
|
}
|
|
176
267
|
const showRow = (r) => r.html;
|
|
177
268
|
return div(srespsAvailable.map(showRow));
|
package/index.js
CHANGED
package/package.json
CHANGED
package/validate.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const View = require("@saltcorn/data/models/view");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
configFields: async ({ table }) => {
|
|
6
|
+
const views = await View.find({
|
|
7
|
+
viewtemplate: "Available Resources Feed",
|
|
8
|
+
table_id: table.id,
|
|
9
|
+
});
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
name: "feedview",
|
|
13
|
+
label: "View",
|
|
14
|
+
sublabel: `A view on table ${table.name} with pattern: Available Resources Feed`,
|
|
15
|
+
type: "String",
|
|
16
|
+
required: true,
|
|
17
|
+
attributes: {
|
|
18
|
+
options: views.map((f) => f.name),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
},
|
|
23
|
+
requireRow: true,
|
|
24
|
+
run: async ({ table, req, row, configuration: { feedview } }) => {
|
|
25
|
+
const view = View.findOne({ name: feedview });
|
|
26
|
+
const {
|
|
27
|
+
reservable_entity_key,
|
|
28
|
+
valid_field,
|
|
29
|
+
slot_count_field,
|
|
30
|
+
slots_available_field,
|
|
31
|
+
show_view,
|
|
32
|
+
start_field,
|
|
33
|
+
end_field,
|
|
34
|
+
} = view.configuration;
|
|
35
|
+
//get all relevant reservations
|
|
36
|
+
|
|
37
|
+
const ress = await table.getRows({
|
|
38
|
+
[reservable_entity_key]:
|
|
39
|
+
row[reservable_entity_key]?.id || row[reservable_entity_key],
|
|
40
|
+
[start_field]: { lt: row[end_field], equal: true, day_only: true },
|
|
41
|
+
[end_field]: { gt: row[start_field], equal: true, day_only: true },
|
|
42
|
+
...(valid_field ? { [valid_field]: true } : {}),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
//get entity
|
|
46
|
+
const refield = table.getField(reservable_entity_key);
|
|
47
|
+
const retable = Table.findOne(refield.reftable_name);
|
|
48
|
+
const entity = await retable.getRow({
|
|
49
|
+
[retable.pk_name]:
|
|
50
|
+
row[reservable_entity_key]?.id || row[reservable_entity_key],
|
|
51
|
+
});
|
|
52
|
+
//check that for every day, there is availablity
|
|
53
|
+
const from = new Date(row[start_field]);
|
|
54
|
+
const to = new Date(row[end_field]);
|
|
55
|
+
let maxAvailable = entity[slots_available_field];
|
|
56
|
+
// loop for every day
|
|
57
|
+
for (let day = from; day <= to; day.setDate(day.getDate() + 1)) {
|
|
58
|
+
// your day is here
|
|
59
|
+
const active = ress.filter(
|
|
60
|
+
(r) => r[start_field] <= day && r[end_field] >= day
|
|
61
|
+
);
|
|
62
|
+
const taken = active
|
|
63
|
+
.map((r) => r[slot_count_field])
|
|
64
|
+
.reduce((a, b) => a + b, 0);
|
|
65
|
+
maxAvailable = Math.min(
|
|
66
|
+
maxAvailable,
|
|
67
|
+
entity[slots_available_field] - taken
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (maxAvailable < row[slot_count_field])
|
|
71
|
+
return { error: `Only ${maxAvailable} are available` };
|
|
72
|
+
},
|
|
73
|
+
};
|