@saltcorn/reservable 0.2.2 → 0.3.0

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/README.md CHANGED
@@ -18,11 +18,7 @@ for a single resource, you can leave this out. On the other hand, if you are bui
18
18
  need to be tracked for whether they are available, use this to denote the resources that can be taken/available.
19
19
  To do this, create a foreign key field on your reservations Table to the reservable resource table.
20
20
 
21
- You should then create a Reserve view on the reservations table. In the configuration, you specify the fields
22
- As explained above. You will also need to configure:
23
21
 
24
- - What services are available. Each service has a name and a duration.
25
- - What the availability is, your opening hours. You specify these in blocks of time and the day of the week.
26
22
 
27
23
  ### Example
28
24
 
@@ -33,3 +29,31 @@ and a full restyle taking 45 minutes. You can book either Goldilocks or Rapunzel
33
29
 
34
30
  Here, the reservable resource is the hairdresser with two rows: Goldilocks and Rapunzel. They have two services,
35
31
  haircut and full restyle. The availability can be specified as: Mon-Fri 9-12, Mon-Fri 13-17 and Saturday 10-13.
32
+
33
+ ### Using Reserve view
34
+
35
+ The reserve view is a quick way to get started but offers limited control over the look and feel and workflow of the reservation process.
36
+
37
+ You should create a Reserve view on the reservations table. In the configuration, you specify the fields
38
+ as explained above. You will also need to configure:
39
+
40
+ - What services are available. Each service has a name and a duration.
41
+ - What the availability is, your opening hours. You specify these in blocks of time and the day of the week.
42
+
43
+ ### Using the "Reservation availabilites" table provider
44
+
45
+ Using the table provider gives you full control over the workflow and the look and feel of your reservation process.
46
+
47
+ First, define a standard database table for reservations as described above. This table should also have an Edit view for adding new reservations.
48
+
49
+ Now define a new table with the reservation availabilities table provider. in the configuration, pick the table for the reservations and fill out the field choices. You also set up the different services offered and the times you are available.
50
+
51
+ You can now create lists or feed views on this provided table in order to display your availability to the user.
52
+
53
+ In order to let the user perform a reservation you should link to the edit view on the reservations table. Create a viewlink and pick this edit view on the reservations table with no relation. you need to use the extra state formula to identify the reservation time and duration. If you start tiem field name is `startat` and the duration field is `duration`, use this as the extra state formula: `{startat: start_date, duration: service_duration}` - `start_date` and `service_duration` are fields defined by the table provider as you can see in the field list. You can also use other Fields such as the service title to fill information in the edit view.
54
+
55
+ You may also want to use the validate_reservation action to ensure that reservation is still available at the time of the booking (that is, it was not reserved by another user while the user was looking at the edit view). In the reservations table add a trigger with when=Validate and action=validate_reservation. This is simply configured by selecting the provided table.
56
+
57
+ ### Multiday bookings
58
+
59
+ The Available Resources Feed view is used to make multi-day bookings for instance for hotel or car rental.
package/common.js ADDED
@@ -0,0 +1,83 @@
1
+ const gcd = function (a, b) {
2
+ if (!b) {
3
+ return a;
4
+ }
5
+
6
+ return gcd(b, a % b);
7
+ };
8
+
9
+ let gcdArr = function (arr) {
10
+ let gcdres = gcd(arr[0], arr[1]);
11
+ for (let i = 2; i < arr.length; i++) {
12
+ gcdres = gcd(gcdres, arr[i]);
13
+ }
14
+ return gcdres;
15
+ };
16
+
17
+ function range(size, startAt = 0) {
18
+ return [...Array(size).keys()].map((i) => i + startAt);
19
+ }
20
+
21
+ const get_available_slots = async ({
22
+ table,
23
+ availability,
24
+ date,
25
+ entity_wanted,
26
+ reservable_entity_key,
27
+ services,
28
+ start_field,
29
+ duration_field,
30
+ }) => {
31
+ const from = new Date(date);
32
+ from.setHours(0, 0, 0, 0);
33
+ const to = new Date(date);
34
+ to.setHours(23, 59, 59, 999);
35
+ const q = {};
36
+ q[start_field] = [{ gt: from }, { lt: to }];
37
+ if (reservable_entity_key && entity_wanted)
38
+ q[reservable_entity_key] = entity_wanted;
39
+ //console.log(JSON.stringify({ date, q }, null, 2));
40
+ const taken_slots = await table.getRows(q);
41
+
42
+ // figure out regular availability for this day
43
+ const dayOfWeek = [
44
+ "Sunday",
45
+ "Monday",
46
+ "Tuesday",
47
+ "Wednesday",
48
+ "Thursday",
49
+ "Friday",
50
+ "Saturday",
51
+ ][date.getDay()];
52
+ const relevant_availabilities = availability.filter(
53
+ ({ day }) =>
54
+ day === dayOfWeek ||
55
+ (day === "Mon-Fri" && !["Saturday", "Sunday"].includes(dayOfWeek))
56
+ );
57
+
58
+ const available_slots = [];
59
+ const durGCD = gcdArr(services.map((s) => s.duration));
60
+ relevant_availabilities.forEach(({ from, to }) => {
61
+ for (let i = (from * 60) / durGCD; i < (to * 60) / durGCD; i++) {
62
+ available_slots[i] = true;
63
+ }
64
+ });
65
+ //console.log({ taken_slots });
66
+ taken_slots.forEach((slot) => {
67
+ /* console.log(
68
+ "taken slot",
69
+ slot,
70
+ slot[start_field].getHours(),
71
+ slot[start_field].getTimezoneOffset()
72
+ ); */
73
+ const from =
74
+ slot[start_field].getHours() * 60 + slot[start_field].getMinutes();
75
+ const to = from + slot[duration_field];
76
+ for (let i = from / durGCD; i < to / durGCD; i++) {
77
+ available_slots[i] = false;
78
+ }
79
+ });
80
+ return { available_slots, from, durGCD, taken_slots };
81
+ };
82
+
83
+ module.exports = { get_available_slots, range };
package/index.js CHANGED
@@ -26,6 +26,8 @@ const { InvalidConfiguration } = require("@saltcorn/data/utils");
26
26
  const {
27
27
  getForm,
28
28
  } = require("@saltcorn/data/base-plugin/viewtemplates/viewable_fields");
29
+ const { get_available_slots, range } = require("./common.js");
30
+
29
31
  const configuration_workflow = () =>
30
32
  new Workflow({
31
33
  steps: [
@@ -181,87 +183,6 @@ const get_state_fields = async (table_id, viewname, { show_view }) => {
181
183
  return sf;
182
184
  });
183
185
  };
184
- const gcd = function (a, b) {
185
- if (!b) {
186
- return a;
187
- }
188
-
189
- return gcd(b, a % b);
190
- };
191
-
192
- let gcdArr = function (arr) {
193
- let gcdres = gcd(arr[0], arr[1]);
194
- for (let i = 2; i < arr.length; i++) {
195
- gcdres = gcd(gcdres, arr[i]);
196
- }
197
- return gcdres;
198
- };
199
-
200
- function range(size, startAt = 0) {
201
- return [...Array(size).keys()].map((i) => i + startAt);
202
- }
203
-
204
- const get_available_slots = async ({
205
- table,
206
- availability,
207
- date,
208
- entity_wanted,
209
- reservable_entity_key,
210
- services,
211
- start_field,
212
- duration_field,
213
- }) => {
214
- const from = new Date(date);
215
- from.setHours(0, 0, 0, 0);
216
- const to = new Date(date);
217
- to.setHours(23, 59, 59, 999);
218
- const q = {};
219
- q[start_field] = [{ gt: from }, { lt: to }];
220
- if (reservable_entity_key && entity_wanted)
221
- q[reservable_entity_key] = entity_wanted;
222
- //console.log({ date, q });
223
- const taken_slots = await table.getRows(q);
224
-
225
- // figure out regular availability for this day
226
- const dayOfWeek = [
227
- "Sunday",
228
- "Monday",
229
- "Tuesday",
230
- "Wednesday",
231
- "Thursday",
232
- "Friday",
233
- "Saturday",
234
- ][date.getDay()];
235
- const relevant_availabilities = availability.filter(
236
- ({ day }) =>
237
- day === dayOfWeek ||
238
- (day === "Mon-Fri" && !["Saturday", "Sunday"].includes(dayOfWeek))
239
- );
240
-
241
- const available_slots = [];
242
- const durGCD = gcdArr(services.map((s) => s.duration));
243
- relevant_availabilities.forEach(({ from, to }) => {
244
- for (let i = (from * 60) / durGCD; i < (to * 60) / durGCD; i++) {
245
- available_slots[i] = true;
246
- }
247
- });
248
- //console.log({ taken_slots });
249
- taken_slots.forEach((slot) => {
250
- /* console.log(
251
- "taken slot",
252
- slot,
253
- slot[start_field].getHours(),
254
- slot[start_field].getTimezoneOffset()
255
- ); */
256
- const from =
257
- slot[start_field].getHours() * 60 + slot[start_field].getMinutes();
258
- const to = from + slot[duration_field];
259
- for (let i = from / durGCD; i < to / durGCD; i++) {
260
- available_slots[i] = false;
261
- }
262
- });
263
- return { available_slots, from, durGCD, taken_slots };
264
- };
265
186
 
266
187
  const run = async (
267
188
  table_id,
@@ -589,6 +510,9 @@ module.exports = {
589
510
  require("./available-feed"),
590
511
  ],
591
512
  actions: { validate_reservation: require("./validate") },
513
+ table_providers: {
514
+ "Reservation availabilites": require("./table-provider.js"),
515
+ },
592
516
  };
593
517
 
594
518
  /*
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@saltcorn/reservable",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Reservable resources",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "@saltcorn/markup": "^0.5.1",
8
- "@saltcorn/data": "^0.5.1"
8
+ "@saltcorn/data": "^0.5.1",
9
+ "@saltcorn/plain-date": "0.2.5"
9
10
  },
10
11
  "author": "Tom Nielsen",
11
12
  "license": "MIT",
@@ -0,0 +1,298 @@
1
+ const { eval_expression } = require("@saltcorn/data/models/expression");
2
+ const Workflow = require("@saltcorn/data/models/workflow");
3
+ const Form = require("@saltcorn/data/models/form");
4
+ const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
5
+ const Field = require("@saltcorn/data/models/field");
6
+ const Table = require("@saltcorn/data/models/table");
7
+ const { getState } = require("@saltcorn/data/db/state");
8
+ const { mkTable } = require("@saltcorn/markup");
9
+ const { pre, code } = require("@saltcorn/markup/tags");
10
+ const PlainDate = require("@saltcorn/plain-date");
11
+
12
+ const { get_available_slots, range } = require("./common");
13
+
14
+ const configuration_workflow = (req) =>
15
+ new Workflow({
16
+ steps: [
17
+ {
18
+ name: "table",
19
+ form: async () => {
20
+ const tables = await Table.find({});
21
+ return new Form({
22
+ fields: [
23
+ {
24
+ name: "table_name",
25
+ label: "Reservations Table",
26
+ type: "String",
27
+ required: true,
28
+ attributes: {
29
+ options: tables.map((t) => t.name),
30
+ },
31
+ sublabel: "Select a table with reservations",
32
+ },
33
+ ],
34
+ });
35
+ },
36
+ },
37
+ {
38
+ name: "fields",
39
+ form: async (context) => {
40
+ const table = await Table.findOne({ name: context.table_name });
41
+ const fields = await table.getFields();
42
+
43
+ return new Form({
44
+ fields: [
45
+ {
46
+ name: "reservable_entity_key",
47
+ label: "Key to reservable entity",
48
+ type: "String",
49
+ attributes: {
50
+ options: fields.filter((f) => f.is_fkey).map((f) => f.name),
51
+ },
52
+ },
53
+ {
54
+ name: "start_field",
55
+ label: "Start date field",
56
+ type: "String",
57
+ required: true,
58
+ attributes: {
59
+ options: fields
60
+ .filter((f) => f.type.name === "Date")
61
+ .map((f) => f.name),
62
+ },
63
+ },
64
+ {
65
+ name: "duration_field",
66
+ label: "Duration field",
67
+ sublabel: "Integer field holding booked duration in minutes",
68
+ type: "String",
69
+ required: true,
70
+ attributes: {
71
+ options: fields
72
+ .filter((f) => f.type.name === "Integer")
73
+ .map((f) => f.name),
74
+ },
75
+ },
76
+ ],
77
+ });
78
+ },
79
+ },
80
+ {
81
+ name: "services",
82
+ form: async () => {
83
+ return new Form({
84
+ fields: [
85
+ new FieldRepeat({
86
+ name: "services",
87
+ fields: [
88
+ {
89
+ name: "title",
90
+ label: "Title",
91
+ sublabel: "Optional name of this service",
92
+ type: "String",
93
+ },
94
+ {
95
+ name: "duration",
96
+ label: "Duration (minutes)",
97
+ type: "Integer",
98
+ attributes: {
99
+ min: 0,
100
+ },
101
+ },
102
+ ],
103
+ }),
104
+ ],
105
+ });
106
+ },
107
+ },
108
+ {
109
+ name: "availability",
110
+ form: async () => {
111
+ return new Form({
112
+ fields: [
113
+ new FieldRepeat({
114
+ name: "availability",
115
+ fields: [
116
+ {
117
+ name: "day",
118
+ label: "Day of week",
119
+ type: "String",
120
+ required: true,
121
+ attributes: {
122
+ options:
123
+ "Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday,Mon-Fri",
124
+ },
125
+ },
126
+ {
127
+ name: "from",
128
+ label: "Start hour",
129
+ type: "Integer",
130
+ required: true,
131
+ attributes: {
132
+ min: 0,
133
+ max: 23,
134
+ },
135
+ },
136
+ {
137
+ name: "to",
138
+ label: "End hour",
139
+ type: "Integer",
140
+ required: true,
141
+ attributes: {
142
+ min: 0,
143
+ max: 24,
144
+ },
145
+ },
146
+ ],
147
+ }),
148
+ ],
149
+ });
150
+ },
151
+ },
152
+ ],
153
+ });
154
+
155
+ module.exports = {
156
+ configuration_workflow,
157
+ fields: (cfg) => {
158
+ if (!cfg?.table_name) return [];
159
+
160
+ const table = Table.findOne({ name: cfg.table_name });
161
+ let entity_key;
162
+ if (cfg.reservable_entity_key) {
163
+ entity_key = table.getField(cfg.reservable_entity_key);
164
+ }
165
+ return [
166
+ {
167
+ name: "reserve_ident",
168
+ type: "String",
169
+ primary_key: true,
170
+ is_unique: true,
171
+ },
172
+ {
173
+ name: "start_day",
174
+ label: "Start day",
175
+ type: "Date",
176
+ attributes: { day_only: true },
177
+ },
178
+ {
179
+ name: "start_date",
180
+ label: "Start date",
181
+ type: "Date",
182
+ },
183
+ {
184
+ name: "start_hour",
185
+ label: "Start hour",
186
+ type: "Integer",
187
+ attributes: { min: 0, max: 23 },
188
+ },
189
+ {
190
+ name: "start_minute",
191
+ label: "Start mintute",
192
+ type: "Integer",
193
+ attributes: { min: 0, max: 59 },
194
+ },
195
+ {
196
+ name: "service",
197
+ label: "Service",
198
+ type: "String",
199
+ attributes: { options: cfg.services.map((s) => s.title) },
200
+ },
201
+ {
202
+ name: "service_duration",
203
+ label: "Duration",
204
+ type: "Integer",
205
+ },
206
+ ...(entity_key
207
+ ? [
208
+ {
209
+ name: "entity",
210
+ label: "Entity",
211
+ type: `Key to ${entity_key.reftable_name}`,
212
+ attributes: {
213
+ summary_field: entity_key.attributes.summary_field,
214
+ },
215
+ },
216
+ ]
217
+ : []),
218
+ ];
219
+ },
220
+ get_table: (cfg) => {
221
+ return {
222
+ disableFiltering: true,
223
+ getRows: async (where, opts) => {
224
+ const table = Table.findOne({ name: cfg.table_name });
225
+ const date = !where?.start_day
226
+ ? new Date()
227
+ : where?.start_day.constructor.name === "PlainDate"
228
+ ? where.start_day.toDate()
229
+ : new Date(where?.start_day);
230
+
231
+ const services = where?.service
232
+ ? cfg.services.filter((s) => s.title === where.service)
233
+ : cfg.services;
234
+ const { available_slots, from, durGCD, taken_slots } =
235
+ await get_available_slots({
236
+ table,
237
+ date,
238
+ availability: cfg.availability,
239
+ entity_wanted: where?.entity || undefined,
240
+ reservable_entity_key: cfg.reservable_entity_key,
241
+ start_field: cfg.start_field,
242
+ duration_field: cfg.duration_field,
243
+ services,
244
+ });
245
+ const minSlot = Math.min(...Object.keys(available_slots));
246
+ const maxSlot = Math.max(...Object.keys(available_slots));
247
+ const service_availabilities = services.map((service, serviceIx) => {
248
+ const nslots = service.duration / durGCD;
249
+ const availabilities = [];
250
+ for (let i = minSlot; i <= maxSlot; i++) {
251
+ const mins_since_midnight = i * durGCD;
252
+ const hour = Math.floor(mins_since_midnight / 60);
253
+
254
+ const minute = mins_since_midnight - hour * 60;
255
+ const date1 = new Date(date);
256
+ date1.setHours(hour);
257
+ date1.setMinutes(minute);
258
+ date1.setSeconds(0);
259
+ date1.setMilliseconds(0);
260
+ if (date1 > new Date())
261
+ if (range(nslots, i).every((j) => available_slots[j])) {
262
+ availabilities.push({
263
+ date: date1,
264
+ available: true,
265
+ });
266
+ } else {
267
+ availabilities.push({
268
+ date: date1,
269
+ available: false,
270
+ });
271
+ }
272
+ }
273
+ //console.log({ availabilities, service });
274
+ return { availabilities, service, serviceIx };
275
+ });
276
+
277
+ const rows = service_availabilities
278
+ .map(({ availabilities, service, serviceIx }) =>
279
+ availabilities
280
+ .filter((a) => a.available)
281
+ .map(({ date }) => {
282
+ return {
283
+ reserve_ident: `${date.toISOString()}//${service.title}`,
284
+ service: service.title,
285
+ service_duration: service.duration,
286
+ start_day: new PlainDate(date),
287
+ start_date: date,
288
+ start_hour: date.getHours(),
289
+ start_minute: date.getMinutes(),
290
+ };
291
+ })
292
+ )
293
+ .flat();
294
+ return rows;
295
+ },
296
+ };
297
+ },
298
+ };
package/validate.js CHANGED
@@ -7,67 +7,147 @@ module.exports = {
7
7
  viewtemplate: "Available Resources Feed",
8
8
  table_id: table.id,
9
9
  });
10
- return [
10
+ const tables = await Table.find(
11
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
- },
12
+ provider_name: "Reservation availabilites",
20
13
  },
14
+ {
15
+ cached: true,
16
+ }
17
+ );
18
+ const neither = views.length === 0 && tables.length === 0;
19
+ //console.log({ views, tables, neither });
20
+
21
+ return [
22
+ ...(views.length || neither
23
+ ? [
24
+ {
25
+ name: "feedview",
26
+ label: "View",
27
+ sublabel: `A view on table ${table.name} with pattern: Available Resources Feed`,
28
+ type: "String",
29
+ attributes: {
30
+ options: views.map((f) => f.name),
31
+ },
32
+ },
33
+ ]
34
+ : []),
35
+ ...(tables.length || neither
36
+ ? [
37
+ {
38
+ name: "reserve_provided_table",
39
+ label: "Table",
40
+ sublabel: `A table with provider: Reservation availabilites`,
41
+ type: "String",
42
+ attributes: {
43
+ options: tables.map((f) => f.name),
44
+ },
45
+ },
46
+ ]
47
+ : []),
21
48
  ];
22
49
  },
23
50
  requireRow: true,
24
- run: async ({ table, req, row, configuration: { feedview } }) => {
25
- const view = View.findOne({ name: feedview });
51
+ run: async ({
52
+ table,
53
+ req,
54
+ row,
55
+ configuration: { feedview, reserve_provided_table },
56
+ }) => {
57
+ const get_config = () => {
58
+ if (feedview) {
59
+ const view = View.findOne({ name: feedview });
60
+ return view.configuration;
61
+ } else if (reserve_provided_table) {
62
+ const table = Table.findOne({ name: reserve_provided_table });
63
+ return table.provider_cfg;
64
+ }
65
+ };
26
66
  const {
27
67
  reservable_entity_key,
28
68
  valid_field,
29
69
  slot_count_field,
30
70
  slots_available_field,
31
- show_view,
32
71
  start_field,
33
72
  end_field,
34
- } = view.configuration;
73
+ duration_field,
74
+ } = get_config();
35
75
  //get all relevant reservations
36
76
 
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
77
  //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
- );
78
+ let entity;
79
+ if (reservable_entity_key) {
80
+ const refield = table.getField(reservable_entity_key);
81
+ const retable = Table.findOne(refield.reftable_name);
82
+ entity = await retable.getRow({
83
+ [retable.pk_name]:
84
+ row[reservable_entity_key]?.id || row[reservable_entity_key],
85
+ });
86
+ }
87
+ if (end_field) {
88
+ const q = valid_field ? { [valid_field]: true } : {};
89
+ if (end_field) {
90
+ q[start_field] = { lt: row[end_field], equal: true, day_only: true };
91
+ q[end_field] = { gt: row[start_field], equal: true, day_only: true };
92
+ } else {
93
+ q[start_field] = {
94
+ lt: row[start_field],
95
+ gt: row[start_field],
96
+ equal: true,
97
+ day_only: true,
98
+ };
99
+ }
100
+ if (reservable_entity_key)
101
+ q[reservable_entity_key] =
102
+ row[reservable_entity_key]?.id || row[reservable_entity_key];
103
+ //console.log("q", q);
104
+
105
+ const ress = await table.getRows(q);
106
+
107
+ //check that for every day, there is availablity
108
+ const from = new Date(row[start_field]);
109
+ const to = new Date(row[end_field]);
110
+ let maxAvailable =
111
+ slots_available_field && entity ? entity[slots_available_field] : 1;
112
+ // loop for every day
113
+ for (let day = from; day <= to; day.setDate(day.getDate() + 1)) {
114
+ // your day is here
115
+ const active = ress.filter(
116
+ (r) => r[start_field] <= day && r[end_field] >= day
117
+ );
118
+ const taken = active
119
+ .map((r) => (slot_count_field ? r[slot_count_field] : 1))
120
+ .reduce((a, b) => a + b, 0);
121
+ maxAvailable = Math.min(
122
+ maxAvailable,
123
+ (slots_available_field && entity
124
+ ? entity[slots_available_field]
125
+ : 1) - taken
126
+ );
127
+ }
128
+ if (maxAvailable < (slot_count_field ? row[slot_count_field] : 1))
129
+ return maxAvailable === 1
130
+ ? { error: `Not available` }
131
+ : { error: `Only ${maxAvailable} are available` };
132
+ } else if (reserve_provided_table) {
133
+ const { get_table } = require("./table-provider");
134
+ const table = Table.findOne({ name: reserve_provided_table });
135
+ const ptable = get_table(table.provider_cfg);
136
+ const q = {};
137
+ if (entity) q.entity = entity.id; //todo pk_name
138
+ q.start_day = row[start_field];
139
+ const rows = await ptable.getRows(q);
140
+ const start_hr = new Date(row[start_field]).getHours()
141
+ const start_min = new Date(row[start_field]).getMinutes()
142
+ if (
143
+ !rows.find(
144
+ (r) =>
145
+ r.start_hour === start_hr &&
146
+ r.start_minute === start_min &&
147
+ r.service_duration >= row[duration_field]
148
+ )
149
+ )
150
+ return { error: `Not available` };
69
151
  }
70
- if (maxAvailable < row[slot_count_field])
71
- return { error: `Only ${maxAvailable} are available` };
72
152
  },
73
153
  };