{{ message }}
Repository navigation
Expand file tree
/
Copy pathkitchen-call.js
More file actions
237 lines (220 loc) · 8.87 KB
/
Copy pathkitchen-call.js
File metadata and controls
237 lines (220 loc) · 8.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
"use strict";
/*
* THE KITCHEN HEARS A TICKET ARRIVE.
*
* Owner: "whenever new KOT received one tink sound with full sound i want.
* need to place in kitchen. if possible read the items. Example Table 5 new
* order. Ting! one chicken briyani, one chicken tikka masala."
*
* A printer in a kitchen is silent and a ticket is small. A cook with their
* hands in a pan does not see one arrive, and the first anybody knows is a
* waiter asking where the food is. A speaker says it across the room without
* anybody looking at anything.
*
* THE TING COMES FIRST, THEN THE WORDS. A speaker that starts talking into a
* working kitchen loses its first two words, and the first two words are the
* table number - the one part nobody can guess from the rest.
*
* WHAT IS SAID, AND WHAT IS NOT. Names and counts. Not notes, not the dish's
* description, not the price. A ticket read aloud for ninety seconds is one a
* kitchen stops listening to, and what makes announcements fail is never that
* they said too little.
*
* ENGLISH, by the owner's choice: "All english as of now." The one place that
* decides is `say()` below.
*
* PER MACHINE, NOT PER SHOP. Only the machine in the kitchen has the speaker.
* A counter till that started talking would be a till somebody mutes, and then
* the kitchen's own speaker is muted too.
*/
/*
* Counts as words, up to the point where words stop helping.
*
* "One chicken biryani" is how somebody would say it; "1 chicken biryani" is
* how a speech engine says "one chicken biryani" anyway, but pronounces badly
* in the middle of a sentence on some voices. Past twelve, digits are clearer
* than "seventeen" spoken quickly.
*/
const WORDS = [
"",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
];
const countWord = (n) => {
const count = Number(n) || 0;
if (count < 1) return "";
return count < WORDS.length ? WORDS[count] : String(count);
};
/*
* HOW MANY LINES GET READ.
*
* A table of six sending six courses arrives within seconds - kot-manager
* already debounces printing for exactly that reason. Read in full, that is a
* minute of talking, and a kitchen that has stopped listening does not hear
* the next one either. Six lines, then how many are left, and the ticket
* itself is still on the spike for the detail.
*/
const READ_AT_MOST = Infinity;
/*
* The name as the shop typed it, tidied of whitespace and nothing else.
*
* An earlier version stripped anything in brackets, meaning to drop decoration
* like "(Half)". It turned "Gobi (65)" into "Gobi" - a different dish. What is
* in brackets is usually the part that tells a cook which one: half, full,
* boneless. The shop's name is the shop's.
*/
function spokenName(item) {
return String((item && (item.item_name || item.name || item.product_name)) || "")
.replace(/\s+/g, " ")
.trim();
}
const quantityOf = (item) =>
Number((item && (item.item_quantity || item.quantity)) || 0) || 0;
/**
* What the speaker should say about one ticket, ONE LINE AT A TIME.
*
* Separate lines rather than one sentence, because the renderer speaks each as
* its own utterance and a speech engine leaves a real gap between them. A full
* stop inside one sentence is a shorter pause than a kitchen needs: the point
* of the gap is that somebody can hold one dish in their head before the next
* arrives.
*
* Returns an empty list when there is nothing worth saying, so a caller can
* stay quiet rather than announce a ticket with no dishes on it.
*
* @param {object} ticket
* @param {string|number} ticket.table where it is for
* @param {Array} ticket.items what is on it
* @param {boolean} [ticket.changed] an amendment rather than a new order
* @param {boolean} [ticket.cancelled] dishes coming OFF, not on
* @param {boolean} [ticket.whole] the whole order is going, not some of it
*/
function script({ table, items, changed, cancelled, whole } = {}) {
const said = (Array.isArray(items) ? items : [])
.filter(item => !item.held)
.map((item) => ({ name: spokenName(item), count: quantityOf(item), instruction:item.instruction_only, allergies:[...(item.allergies || []),item.allergy_note || ''].filter(Boolean), seat:item.seat }))
.filter((line) => line.name && line.count > 0);
if (!said.length) return { head: [], items: [] };
const where = String(table == null ? "" : table).trim();
/*
* "Table 5" when there is one, "Takeaway" when the shop calls it that, and
* neither invented when the ticket does not say. A speaker announcing
* "Table undefined" is worse than one that just reads the food.
*/
/*
* A CANCELLATION IS NOT AN ORDER, AND IT USED TO BE READ AS ONE.
*
* Owner, hearing it: "one mistake for cancelled order it reads as new order.
* item cancelled or order cancelled clearly need to do."
*
* The till writes three kinds of ticket - new, modified and cancel - and the
* announcer only asked whether it was modified. Everything else opened with
* "new order", so a table cancelling two biryanis was read to the kitchen as
* a table ordering two biryanis, and somebody cooked them.
*
* The whole order going and two dishes coming off it are different sentences
* because they are different jobs: one clears a table, the other changes
* what is already on the pass.
*/
const what = cancelled
? whole
? "order cancelled"
: said.length === 1
? "item cancelled"
: "items cancelled"
: changed
? "order changed"
: "new order";
const opening = where
? `${/^\d+$/.test(where) ? `Table ${where}` : where}, ${what}.`
: `${what.charAt(0).toUpperCase()}${what.slice(1)}.`;
/*
* HOW MANY PLATES ARE COMING, said before the list.
*
* Owner: "KOT total items also print and voice read please. so that chef's
* can hear well."
*
* Before rather than after, because a number heard first is a number you can
* count against. A chef who knows three plates are coming notices when they
* have heard two, which is the whole use of it - after the list it is a fact
* nobody can act on.
*
* PLATES, not lines. One biryani and two naan is three things to cook and
* two lines on a ticket, and a kitchen works in plates.
*/
const plates = said.reduce((sum, line) => sum + line.count, 0);
const counted = countWord(plates);
const howMany = cancelled
? plates === 1
? "One item off."
: `${counted.charAt(0).toUpperCase()}${counted.slice(1)} items off.`
: plates === 1
? "One item."
: `${counted.charAt(0).toUpperCase()}${counted.slice(1)} items.`;
const read = said.slice(0, READ_AT_MOST);
/* Capitalised, because each of these is a sentence once the full stops go
in, and a log or a test reading "one Chicken Biryani" mid-line looks like
a bug even where a speech engine does not care. */
const spoken = read.map((line) => {
const count = countWord(line.count);
/*
* EVERY CANCELLED LINE SAYS SO, not only the opening.
*
* A cook who walks up half way through hears one dish and a number, and
* nothing else. "Cancel two naan" cannot be misheard as an order for two
* naan; "two naan" after an opening they missed can.
*/
const allergy=line.allergies.length ? '. Allergy: ' + line.allergies.join(', ') : '';
const seat=line.seat ? ', seat ' + line.seat : '';
if(line.instruction)return `Preparation update for ${count} ${line.name}${seat}. Do not add another item${allergy}`;
if (cancelled) return `Cancel ${count} ${line.name}${seat}${allergy}`;
return `${count.charAt(0).toUpperCase()}${count.slice(1)} ${line.name}${seat}${allergy}`;
});
const rest = said.length - read.length;
if (rest > 0) spoken.push(`And ${countWord(rest)} more`);
/*
* SPLIT, BECAUSE A BELL GOES BETWEEN THEM.
*
* Owner: "First bell is we got new order. I want one bell for each line item
* before read it."
*
* The opening and the plate count are about the ticket; everything after is
* a dish. Only the dishes get their own bell, so the caller has to be able
* to tell them apart. Where that line falls is decided here, beside the
* words, rather than by a renderer counting sentences and guessing.
*/
return {
head: [opening, howMany],
items: spoken.map((line) => `${line}.`),
};
}
/**
* The same announcement as a flat list, opening first.
*
* Kept because most callers want the words and not the shape of them, and
* because it is what every existing test and log line reads.
*/
function lines(ticket) {
const said = script(ticket);
return said.head.concat(said.items);
}
/*
* The same announcement as one string.
*
* Kept because a sentence is easier to read in a log and in a test than an
* array, and because a caller that cannot queue utterances can still say it.
*/
function say(ticket) {
return lines(ticket).join(" ");
}
module.exports = { say, lines, script, countWord, spokenName, READ_AT_MOST };
You can’t perform that action at this time.
