@porulle/plugin-pos-restaurant 0.1.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 +61 -0
- package/dist/hooks/modifier-validation.d.ts +21 -0
- package/dist/hooks/modifier-validation.d.ts.map +1 -0
- package/dist/hooks/modifier-validation.js +40 -0
- package/dist/hooks/table-lifecycle.d.ts +15 -0
- package/dist/hooks/table-lifecycle.d.ts.map +1 -0
- package/dist/hooks/table-lifecycle.js +40 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/dist/routes/kds.d.ts +9 -0
- package/dist/routes/kds.d.ts.map +1 -0
- package/dist/routes/kds.js +112 -0
- package/dist/routes/modifiers.d.ts +15 -0
- package/dist/routes/modifiers.d.ts.map +1 -0
- package/dist/routes/modifiers.js +117 -0
- package/dist/routes/operations.d.ts +34 -0
- package/dist/routes/operations.d.ts.map +1 -0
- package/dist/routes/operations.js +249 -0
- package/dist/routes/tables.d.ts +9 -0
- package/dist/routes/tables.d.ts.map +1 -0
- package/dist/routes/tables.js +110 -0
- package/dist/schema.d.ts +3831 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +418 -0
- package/dist/services/alert-service.d.ts +56 -0
- package/dist/services/alert-service.d.ts.map +1 -0
- package/dist/services/alert-service.js +173 -0
- package/dist/services/analytics-service.d.ts +60 -0
- package/dist/services/analytics-service.d.ts.map +1 -0
- package/dist/services/analytics-service.js +178 -0
- package/dist/services/checklist-service.d.ts +66 -0
- package/dist/services/checklist-service.d.ts.map +1 -0
- package/dist/services/checklist-service.js +106 -0
- package/dist/services/kds-service.d.ts +77 -0
- package/dist/services/kds-service.d.ts.map +1 -0
- package/dist/services/kds-service.js +246 -0
- package/dist/services/modifier-service.d.ts +74 -0
- package/dist/services/modifier-service.d.ts.map +1 -0
- package/dist/services/modifier-service.js +180 -0
- package/dist/services/recipe-deduction-service.d.ts +64 -0
- package/dist/services/recipe-deduction-service.d.ts.map +1 -0
- package/dist/services/recipe-deduction-service.js +144 -0
- package/dist/services/recipe-service.d.ts +43 -0
- package/dist/services/recipe-service.d.ts.map +1 -0
- package/dist/services/recipe-service.js +85 -0
- package/dist/services/table-service.d.ts +61 -0
- package/dist/services/table-service.d.ts.map +1 -0
- package/dist/services/table-service.js +206 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/package.json +61 -0
- package/src/hooks/modifier-validation.ts +60 -0
- package/src/hooks/table-lifecycle.ts +50 -0
- package/src/index.ts +218 -0
- package/src/routes/kds.ts +121 -0
- package/src/routes/modifiers.ts +129 -0
- package/src/routes/operations.ts +276 -0
- package/src/routes/tables.ts +117 -0
- package/src/schema.ts +462 -0
- package/src/services/alert-service.ts +234 -0
- package/src/services/analytics-service.ts +242 -0
- package/src/services/checklist-service.ts +139 -0
- package/src/services/kds-service.ts +340 -0
- package/src/services/modifier-service.ts +251 -0
- package/src/services/recipe-deduction-service.ts +221 -0
- package/src/services/recipe-service.ts +115 -0
- package/src/services/table-service.ts +260 -0
- package/src/types.ts +63 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AlertService — Operational red flags and real-time alerts.
|
|
3
|
+
*
|
|
4
|
+
* URY: Operational Red Flags & Alerts system:
|
|
5
|
+
* - Delayed orders and preparation time breaches
|
|
6
|
+
* - KOT not started after order placement
|
|
7
|
+
* - Unclosed bills and prolonged table occupancy
|
|
8
|
+
* - Excessive KOT cancellations and modifications
|
|
9
|
+
* - Real-time alerts for operational exceptions
|
|
10
|
+
*
|
|
11
|
+
* URY implementation: ury_kot_notification.py polls KOTs and creates
|
|
12
|
+
* system notifications when warning time exceeded. KDS displays timer
|
|
13
|
+
* in red when threshold breached. kotValidationThread runs every minute.
|
|
14
|
+
*
|
|
15
|
+
* Our implementation: AlertService creates persistent alert records,
|
|
16
|
+
* queryable via API. Alert detection runs on demand (called by scheduler
|
|
17
|
+
* or via route). Each alert type has configurable thresholds.
|
|
18
|
+
*/
|
|
19
|
+
import { eq, and, sql, desc } from "@porulle/core/drizzle";
|
|
20
|
+
import { Ok, Err } from "@porulle/core";
|
|
21
|
+
import { posRestaurantAlerts, posAlertConfig, kdsTickets, posTables } from "../schema.js";
|
|
22
|
+
export class AlertService {
|
|
23
|
+
db;
|
|
24
|
+
constructor(db) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
}
|
|
27
|
+
// ─── Alert Configuration ───────────────────────────────────────────
|
|
28
|
+
async setThreshold(orgId, alertType, thresholdMinutes, notifyRoles) {
|
|
29
|
+
// Upsert: try update, then insert
|
|
30
|
+
const existing = await this.db
|
|
31
|
+
.select()
|
|
32
|
+
.from(posAlertConfig)
|
|
33
|
+
.where(and(eq(posAlertConfig.organizationId, orgId), eq(posAlertConfig.alertType, alertType)));
|
|
34
|
+
if (existing.length > 0) {
|
|
35
|
+
await this.db
|
|
36
|
+
.update(posAlertConfig)
|
|
37
|
+
.set({ thresholdMinutes, ...(notifyRoles ? { notifyRoles } : {}), updatedAt: new Date() })
|
|
38
|
+
.where(eq(posAlertConfig.id, existing[0].id));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
await this.db.insert(posAlertConfig).values({
|
|
42
|
+
organizationId: orgId,
|
|
43
|
+
alertType,
|
|
44
|
+
thresholdMinutes,
|
|
45
|
+
notifyRoles: notifyRoles ?? [],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return Ok({ alertType, thresholdMinutes });
|
|
49
|
+
}
|
|
50
|
+
async getConfig(orgId) {
|
|
51
|
+
const rows = await this.db
|
|
52
|
+
.select()
|
|
53
|
+
.from(posAlertConfig)
|
|
54
|
+
.where(eq(posAlertConfig.organizationId, orgId));
|
|
55
|
+
return Ok(rows.map((r) => ({ alertType: r.alertType, thresholdMinutes: r.thresholdMinutes, isEnabled: r.isEnabled })));
|
|
56
|
+
}
|
|
57
|
+
// ─── Alert Detection ──────────────────────────────────────────────
|
|
58
|
+
// Scans for conditions that trigger alerts. Called on demand or by scheduler.
|
|
59
|
+
async detectDelayedOrders(orgId) {
|
|
60
|
+
const config = await this.getThreshold(orgId, "delayed_order");
|
|
61
|
+
if (!config)
|
|
62
|
+
return 0;
|
|
63
|
+
const threshold = new Date(Date.now() - config.thresholdMinutes * 60 * 1000);
|
|
64
|
+
// Find pending tickets older than threshold
|
|
65
|
+
const delayed = await this.db
|
|
66
|
+
.select({ id: kdsTickets.id, transactionId: kdsTickets.transactionId, stationId: kdsTickets.stationId })
|
|
67
|
+
.from(kdsTickets)
|
|
68
|
+
.where(and(eq(kdsTickets.organizationId, orgId), eq(kdsTickets.status, "pending"), sql `${kdsTickets.createdAt} < ${threshold}`));
|
|
69
|
+
let count = 0;
|
|
70
|
+
for (const ticket of delayed) {
|
|
71
|
+
const exists = await this.alertExists(orgId, "delayed_order", ticket.id);
|
|
72
|
+
if (!exists) {
|
|
73
|
+
await this.createAlert(orgId, "delayed_order", "kds_ticket", ticket.id, `KDS ticket for transaction ${ticket.transactionId} has been pending for over ${config.thresholdMinutes} minutes`);
|
|
74
|
+
count++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return count;
|
|
78
|
+
}
|
|
79
|
+
async detectKotNotStarted(orgId) {
|
|
80
|
+
const config = await this.getThreshold(orgId, "kot_not_started");
|
|
81
|
+
if (!config)
|
|
82
|
+
return 0;
|
|
83
|
+
const threshold = new Date(Date.now() - config.thresholdMinutes * 60 * 1000);
|
|
84
|
+
const notStarted = await this.db
|
|
85
|
+
.select({ id: kdsTickets.id, transactionId: kdsTickets.transactionId })
|
|
86
|
+
.from(kdsTickets)
|
|
87
|
+
.where(and(eq(kdsTickets.organizationId, orgId), eq(kdsTickets.status, "pending"), sql `${kdsTickets.firedAt} IS NULL`, sql `${kdsTickets.createdAt} < ${threshold}`));
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const ticket of notStarted) {
|
|
90
|
+
const exists = await this.alertExists(orgId, "kot_not_started", ticket.id);
|
|
91
|
+
if (!exists) {
|
|
92
|
+
await this.createAlert(orgId, "kot_not_started", "kds_ticket", ticket.id, `KOT for transaction ${ticket.transactionId} not started after ${config.thresholdMinutes} minutes`);
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return count;
|
|
97
|
+
}
|
|
98
|
+
async detectProlongedOccupancy(orgId) {
|
|
99
|
+
const config = await this.getThreshold(orgId, "prolonged_occupancy");
|
|
100
|
+
if (!config)
|
|
101
|
+
return 0;
|
|
102
|
+
const threshold = new Date(Date.now() - config.thresholdMinutes * 60 * 1000);
|
|
103
|
+
const prolonged = await this.db
|
|
104
|
+
.select({ id: posTables.id, number: posTables.number })
|
|
105
|
+
.from(posTables)
|
|
106
|
+
.where(and(eq(posTables.organizationId, orgId), eq(posTables.status, "occupied"), sql `${posTables.updatedAt} < ${threshold}`));
|
|
107
|
+
let count = 0;
|
|
108
|
+
for (const table of prolonged) {
|
|
109
|
+
const exists = await this.alertExists(orgId, "prolonged_occupancy", table.id);
|
|
110
|
+
if (!exists) {
|
|
111
|
+
await this.createAlert(orgId, "prolonged_occupancy", "pos_table", table.id, `Table '${table.number}' has been occupied for over ${config.thresholdMinutes} minutes`, "warning");
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return count;
|
|
116
|
+
}
|
|
117
|
+
// ─── Alert Queries ─────────────────────────────────────────────────
|
|
118
|
+
async listAlerts(orgId, options) {
|
|
119
|
+
const conditions = [eq(posRestaurantAlerts.organizationId, orgId)];
|
|
120
|
+
if (options?.type)
|
|
121
|
+
conditions.push(eq(posRestaurantAlerts.type, options.type));
|
|
122
|
+
if (options?.unresolvedOnly)
|
|
123
|
+
conditions.push(eq(posRestaurantAlerts.isResolved, false));
|
|
124
|
+
const rows = await this.db
|
|
125
|
+
.select()
|
|
126
|
+
.from(posRestaurantAlerts)
|
|
127
|
+
.where(and(...conditions))
|
|
128
|
+
.orderBy(desc(posRestaurantAlerts.createdAt))
|
|
129
|
+
.limit(options?.limit ?? 100);
|
|
130
|
+
return Ok(rows);
|
|
131
|
+
}
|
|
132
|
+
async resolveAlert(alertId, resolvedBy) {
|
|
133
|
+
const rows = await this.db
|
|
134
|
+
.update(posRestaurantAlerts)
|
|
135
|
+
.set({ isResolved: true, resolvedBy, resolvedAt: new Date() })
|
|
136
|
+
.where(eq(posRestaurantAlerts.id, alertId))
|
|
137
|
+
.returning();
|
|
138
|
+
if (rows.length === 0)
|
|
139
|
+
return Err("Alert not found");
|
|
140
|
+
return Ok({ resolved: true });
|
|
141
|
+
}
|
|
142
|
+
async runAllDetections(orgId) {
|
|
143
|
+
const delayed = await this.detectDelayedOrders(orgId);
|
|
144
|
+
const notStarted = await this.detectKotNotStarted(orgId);
|
|
145
|
+
const occupancy = await this.detectProlongedOccupancy(orgId);
|
|
146
|
+
return Ok({ delayed, notStarted, occupancy });
|
|
147
|
+
}
|
|
148
|
+
// ─── Helpers ───────────────────────────────────────────────────────
|
|
149
|
+
async getThreshold(orgId, alertType) {
|
|
150
|
+
const rows = await this.db
|
|
151
|
+
.select()
|
|
152
|
+
.from(posAlertConfig)
|
|
153
|
+
.where(and(eq(posAlertConfig.organizationId, orgId), eq(posAlertConfig.alertType, alertType), eq(posAlertConfig.isEnabled, true)));
|
|
154
|
+
return rows[0] ?? null;
|
|
155
|
+
}
|
|
156
|
+
async alertExists(orgId, type, referenceId) {
|
|
157
|
+
const rows = await this.db
|
|
158
|
+
.select({ id: posRestaurantAlerts.id })
|
|
159
|
+
.from(posRestaurantAlerts)
|
|
160
|
+
.where(and(eq(posRestaurantAlerts.organizationId, orgId), eq(posRestaurantAlerts.type, type), eq(posRestaurantAlerts.referenceId, referenceId), eq(posRestaurantAlerts.isResolved, false)));
|
|
161
|
+
return rows.length > 0;
|
|
162
|
+
}
|
|
163
|
+
async createAlert(orgId, type, refType, refId, message, severity = "warning") {
|
|
164
|
+
await this.db.insert(posRestaurantAlerts).values({
|
|
165
|
+
organizationId: orgId,
|
|
166
|
+
type,
|
|
167
|
+
severity,
|
|
168
|
+
referenceType: refType,
|
|
169
|
+
referenceId: refId,
|
|
170
|
+
message,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RestaurantAnalyticsService — Daily P&L, performance reports, and sales analytics.
|
|
3
|
+
*
|
|
4
|
+
* URY: 14 report types + Daily P&L doctype with COGS, direct/indirect expenses,
|
|
5
|
+
* employee costs, gross/net profit calculation.
|
|
6
|
+
*
|
|
7
|
+
* URY reports: Today's Sales, Daywise Sales, Daywise Invoices, Month Wise Sales,
|
|
8
|
+
* Average Bill Value, Cancelled Invoices, Item Wise Sales, Customer Data,
|
|
9
|
+
* Repeated Customers, Daywise Customer Details, Employee Sales,
|
|
10
|
+
* Employee Item Wise Sales, Service Wise Sales, Time Wise Sales.
|
|
11
|
+
*
|
|
12
|
+
* Our implementation: P&L CRUD + SQL-based report queries against core order tables.
|
|
13
|
+
*/
|
|
14
|
+
import type { PluginResult } from "@porulle/core";
|
|
15
|
+
import { posDailyPnl, posPnlExpenses } from "../schema.js";
|
|
16
|
+
import type { Db } from "../types.js";
|
|
17
|
+
export declare class RestaurantAnalyticsService {
|
|
18
|
+
private db;
|
|
19
|
+
constructor(db: Db);
|
|
20
|
+
createDailyPnl(orgId: string, input: {
|
|
21
|
+
date: Date;
|
|
22
|
+
grossSales: number;
|
|
23
|
+
netSales: number;
|
|
24
|
+
costOfGoods: number;
|
|
25
|
+
directExpenses: number;
|
|
26
|
+
indirectExpenses: number;
|
|
27
|
+
employeeCosts: number;
|
|
28
|
+
transactionCount: number;
|
|
29
|
+
expenses?: Array<{
|
|
30
|
+
category: "cogs" | "direct" | "indirect" | "employee";
|
|
31
|
+
name: string;
|
|
32
|
+
amount: number;
|
|
33
|
+
percentage?: number;
|
|
34
|
+
}>;
|
|
35
|
+
}): Promise<PluginResult<typeof posDailyPnl.$inferSelect>>;
|
|
36
|
+
getDailyPnl(orgId: string, date: Date): Promise<PluginResult<{
|
|
37
|
+
pnl: typeof posDailyPnl.$inferSelect;
|
|
38
|
+
expenses: Array<typeof posPnlExpenses.$inferSelect>;
|
|
39
|
+
}>>;
|
|
40
|
+
listDailyPnl(orgId: string, limit?: number): Promise<PluginResult<Array<typeof posDailyPnl.$inferSelect>>>;
|
|
41
|
+
getStationPerformance(orgId: string, stationId: string, dateFrom: Date, dateTo: Date): Promise<PluginResult<{
|
|
42
|
+
totalTickets: number;
|
|
43
|
+
averagePrepSeconds: number;
|
|
44
|
+
ticketsByStatus: Array<{
|
|
45
|
+
status: string;
|
|
46
|
+
count: number;
|
|
47
|
+
}>;
|
|
48
|
+
}>>;
|
|
49
|
+
getCoursePerformance(orgId: string, dateFrom: Date, dateTo: Date): Promise<PluginResult<Array<{
|
|
50
|
+
courseName: string;
|
|
51
|
+
totalItems: number;
|
|
52
|
+
averagePrepSeconds: number;
|
|
53
|
+
}>>>;
|
|
54
|
+
getOperatorPerformance(orgId: string, dateFrom: Date, dateTo: Date): Promise<PluginResult<Array<{
|
|
55
|
+
operatorName: string;
|
|
56
|
+
totalTickets: number;
|
|
57
|
+
averagePrepSeconds: number;
|
|
58
|
+
}>>>;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=analytics-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-service.d.ts","sourceRoot":"","sources":["../../src/services/analytics-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,cAAc,EAA8B,MAAM,cAAc,CAAC;AACvF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,qBAAa,0BAA0B;IACzB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAIpB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACzC,IAAI,EAAE,IAAI,CAAC;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,CAAC;QACzB,aAAa,EAAE,MAAM,CAAC;QACtB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,EAAE,KAAK,CAAC;YAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAChI,GAAG,OAAO,CAAC,YAAY,CAAC,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC;IAqDpD,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;QACjE,GAAG,EAAE,OAAO,WAAW,CAAC,YAAY,CAAC;QACrC,QAAQ,EAAE,KAAK,CAAC,OAAO,cAAc,CAAC,YAAY,CAAC,CAAC;KACrD,CAAC,CAAC;IAwBG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC;IAc1G,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;QAChH,YAAY,EAAE,MAAM,CAAC;QACrB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,eAAe,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC3D,CAAC,CAAC;IA6BG,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC;QAClG,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,CAAC,CAAC,CAAC;IA0CE,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC;QACpG,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,CAAC,CAAC,CAAC;CA6BL"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RestaurantAnalyticsService — Daily P&L, performance reports, and sales analytics.
|
|
3
|
+
*
|
|
4
|
+
* URY: 14 report types + Daily P&L doctype with COGS, direct/indirect expenses,
|
|
5
|
+
* employee costs, gross/net profit calculation.
|
|
6
|
+
*
|
|
7
|
+
* URY reports: Today's Sales, Daywise Sales, Daywise Invoices, Month Wise Sales,
|
|
8
|
+
* Average Bill Value, Cancelled Invoices, Item Wise Sales, Customer Data,
|
|
9
|
+
* Repeated Customers, Daywise Customer Details, Employee Sales,
|
|
10
|
+
* Employee Item Wise Sales, Service Wise Sales, Time Wise Sales.
|
|
11
|
+
*
|
|
12
|
+
* Our implementation: P&L CRUD + SQL-based report queries against core order tables.
|
|
13
|
+
*/
|
|
14
|
+
import { eq, and, sql, desc } from "@porulle/core/drizzle";
|
|
15
|
+
import { Ok, Err } from "@porulle/core";
|
|
16
|
+
import { posDailyPnl, posPnlExpenses, kdsTickets, kdsTicketItems } from "../schema.js";
|
|
17
|
+
export class RestaurantAnalyticsService {
|
|
18
|
+
db;
|
|
19
|
+
constructor(db) {
|
|
20
|
+
this.db = db;
|
|
21
|
+
}
|
|
22
|
+
// ─── Daily P&L ─────────────────────────────────────────────────────
|
|
23
|
+
async createDailyPnl(orgId, input) {
|
|
24
|
+
// Validate P&L inputs
|
|
25
|
+
if (input.grossSales < 0)
|
|
26
|
+
return Err("grossSales cannot be negative");
|
|
27
|
+
if (input.netSales < 0)
|
|
28
|
+
return Err("netSales cannot be negative");
|
|
29
|
+
if (input.costOfGoods < 0)
|
|
30
|
+
return Err("costOfGoods cannot be negative");
|
|
31
|
+
if (input.directExpenses < 0)
|
|
32
|
+
return Err("directExpenses cannot be negative");
|
|
33
|
+
if (input.indirectExpenses < 0)
|
|
34
|
+
return Err("indirectExpenses cannot be negative");
|
|
35
|
+
if (input.employeeCosts < 0)
|
|
36
|
+
return Err("employeeCosts cannot be negative");
|
|
37
|
+
if (input.transactionCount < 0)
|
|
38
|
+
return Err("transactionCount cannot be negative");
|
|
39
|
+
if (input.netSales > input.grossSales)
|
|
40
|
+
return Err("netSales cannot exceed grossSales");
|
|
41
|
+
const grossProfit = input.netSales - input.costOfGoods - input.directExpenses;
|
|
42
|
+
const netProfit = grossProfit - input.indirectExpenses - input.employeeCosts;
|
|
43
|
+
const averageBillValue = input.transactionCount > 0
|
|
44
|
+
? Math.round(input.grossSales / input.transactionCount)
|
|
45
|
+
: 0;
|
|
46
|
+
const rows = await this.db
|
|
47
|
+
.insert(posDailyPnl)
|
|
48
|
+
.values({
|
|
49
|
+
organizationId: orgId,
|
|
50
|
+
date: input.date,
|
|
51
|
+
grossSales: input.grossSales,
|
|
52
|
+
netSales: input.netSales,
|
|
53
|
+
costOfGoods: input.costOfGoods,
|
|
54
|
+
directExpenses: input.directExpenses,
|
|
55
|
+
indirectExpenses: input.indirectExpenses,
|
|
56
|
+
employeeCosts: input.employeeCosts,
|
|
57
|
+
grossProfit,
|
|
58
|
+
netProfit,
|
|
59
|
+
transactionCount: input.transactionCount,
|
|
60
|
+
averageBillValue,
|
|
61
|
+
})
|
|
62
|
+
.returning();
|
|
63
|
+
const pnl = rows[0];
|
|
64
|
+
// Insert expense line items
|
|
65
|
+
if (input.expenses) {
|
|
66
|
+
for (const exp of input.expenses) {
|
|
67
|
+
await this.db.insert(posPnlExpenses).values({
|
|
68
|
+
pnlId: pnl.id,
|
|
69
|
+
category: exp.category,
|
|
70
|
+
name: exp.name,
|
|
71
|
+
amount: exp.amount,
|
|
72
|
+
percentage: exp.percentage,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Ok(pnl);
|
|
77
|
+
}
|
|
78
|
+
async getDailyPnl(orgId, date) {
|
|
79
|
+
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
80
|
+
const endOfDay = new Date(startOfDay.getTime() + 24 * 60 * 60 * 1000);
|
|
81
|
+
const pnls = await this.db
|
|
82
|
+
.select()
|
|
83
|
+
.from(posDailyPnl)
|
|
84
|
+
.where(and(eq(posDailyPnl.organizationId, orgId), sql `${posDailyPnl.date} >= ${startOfDay}`, sql `${posDailyPnl.date} < ${endOfDay}`));
|
|
85
|
+
if (pnls.length === 0)
|
|
86
|
+
return Err("No P&L record for this date");
|
|
87
|
+
const pnl = pnls[0];
|
|
88
|
+
const expenses = await this.db
|
|
89
|
+
.select()
|
|
90
|
+
.from(posPnlExpenses)
|
|
91
|
+
.where(eq(posPnlExpenses.pnlId, pnl.id));
|
|
92
|
+
return Ok({ pnl, expenses });
|
|
93
|
+
}
|
|
94
|
+
async listDailyPnl(orgId, limit) {
|
|
95
|
+
const rows = await this.db
|
|
96
|
+
.select()
|
|
97
|
+
.from(posDailyPnl)
|
|
98
|
+
.where(eq(posDailyPnl.organizationId, orgId))
|
|
99
|
+
.orderBy(desc(posDailyPnl.date))
|
|
100
|
+
.limit(limit ?? 30);
|
|
101
|
+
return Ok(rows);
|
|
102
|
+
}
|
|
103
|
+
// ─── KDS Performance Metrics ──────────────────────────────────────
|
|
104
|
+
// Course-wise and station-wise performance from KDS tickets.
|
|
105
|
+
async getStationPerformance(orgId, stationId, dateFrom, dateTo) {
|
|
106
|
+
const tickets = await this.db
|
|
107
|
+
.select()
|
|
108
|
+
.from(kdsTickets)
|
|
109
|
+
.where(and(eq(kdsTickets.organizationId, orgId), eq(kdsTickets.stationId, stationId), sql `${kdsTickets.createdAt} >= ${dateFrom}`, sql `${kdsTickets.createdAt} < ${dateTo}`));
|
|
110
|
+
const totalTickets = tickets.length;
|
|
111
|
+
const servedTickets = tickets.filter((t) => t.prepDurationSeconds != null);
|
|
112
|
+
const averagePrepSeconds = servedTickets.length > 0
|
|
113
|
+
? Math.round(servedTickets.reduce((sum, t) => sum + (t.prepDurationSeconds ?? 0), 0) / servedTickets.length)
|
|
114
|
+
: 0;
|
|
115
|
+
const statusCounts = new Map();
|
|
116
|
+
for (const t of tickets) {
|
|
117
|
+
statusCounts.set(t.status, (statusCounts.get(t.status) ?? 0) + 1);
|
|
118
|
+
}
|
|
119
|
+
return Ok({
|
|
120
|
+
totalTickets,
|
|
121
|
+
averagePrepSeconds,
|
|
122
|
+
ticketsByStatus: [...statusCounts.entries()].map(([status, count]) => ({ status, count })),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async getCoursePerformance(orgId, dateFrom, dateTo) {
|
|
126
|
+
// Get all ticket items with their parent ticket's prep time
|
|
127
|
+
const tickets = await this.db
|
|
128
|
+
.select()
|
|
129
|
+
.from(kdsTickets)
|
|
130
|
+
.where(and(eq(kdsTickets.organizationId, orgId), sql `${kdsTickets.createdAt} >= ${dateFrom}`, sql `${kdsTickets.createdAt} < ${dateTo}`, eq(kdsTickets.status, "served")));
|
|
131
|
+
const courseStats = new Map();
|
|
132
|
+
for (const ticket of tickets) {
|
|
133
|
+
const items = await this.db
|
|
134
|
+
.select()
|
|
135
|
+
.from(kdsTicketItems)
|
|
136
|
+
.where(eq(kdsTicketItems.ticketId, ticket.id));
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
const course = item.courseName ?? "Uncategorized";
|
|
139
|
+
const existing = courseStats.get(course) ?? { totalItems: 0, totalPrepSeconds: 0, count: 0 };
|
|
140
|
+
existing.totalItems += item.quantity;
|
|
141
|
+
if (ticket.prepDurationSeconds != null) {
|
|
142
|
+
existing.totalPrepSeconds += ticket.prepDurationSeconds;
|
|
143
|
+
existing.count++;
|
|
144
|
+
}
|
|
145
|
+
courseStats.set(course, existing);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return Ok([...courseStats.entries()].map(([courseName, stats]) => ({
|
|
149
|
+
courseName,
|
|
150
|
+
totalItems: stats.totalItems,
|
|
151
|
+
averagePrepSeconds: stats.count > 0 ? Math.round(stats.totalPrepSeconds / stats.count) : 0,
|
|
152
|
+
})));
|
|
153
|
+
}
|
|
154
|
+
// ─── Operator/Staff Performance ───────────────────────────────────
|
|
155
|
+
// URY: Captain and staff performance tracking.
|
|
156
|
+
async getOperatorPerformance(orgId, dateFrom, dateTo) {
|
|
157
|
+
const tickets = await this.db
|
|
158
|
+
.select()
|
|
159
|
+
.from(kdsTickets)
|
|
160
|
+
.where(and(eq(kdsTickets.organizationId, orgId), sql `${kdsTickets.createdAt} >= ${dateFrom}`, sql `${kdsTickets.createdAt} < ${dateTo}`));
|
|
161
|
+
const operatorStats = new Map();
|
|
162
|
+
for (const ticket of tickets) {
|
|
163
|
+
const operator = ticket.operatorName ?? "Unknown";
|
|
164
|
+
const existing = operatorStats.get(operator) ?? { totalTickets: 0, totalPrepSeconds: 0, servedCount: 0 };
|
|
165
|
+
existing.totalTickets++;
|
|
166
|
+
if (ticket.prepDurationSeconds != null) {
|
|
167
|
+
existing.totalPrepSeconds += ticket.prepDurationSeconds;
|
|
168
|
+
existing.servedCount++;
|
|
169
|
+
}
|
|
170
|
+
operatorStats.set(operator, existing);
|
|
171
|
+
}
|
|
172
|
+
return Ok([...operatorStats.entries()].map(([operatorName, stats]) => ({
|
|
173
|
+
operatorName,
|
|
174
|
+
totalTickets: stats.totalTickets,
|
|
175
|
+
averagePrepSeconds: stats.servedCount > 0 ? Math.round(stats.totalPrepSeconds / stats.servedCount) : 0,
|
|
176
|
+
})));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChecklistService — Pre-billing compliance checklists.
|
|
3
|
+
*
|
|
4
|
+
* URY: Pre-billing checklists enforce compliance (stock check, hygiene).
|
|
5
|
+
* validate_invoice_print() blocks billing until checklist complete.
|
|
6
|
+
*
|
|
7
|
+
* Checklists can be of type:
|
|
8
|
+
* - pre_billing: must complete before printing a bill
|
|
9
|
+
* - shift_open: must complete when opening a shift
|
|
10
|
+
* - shift_close: must complete when closing a shift
|
|
11
|
+
*/
|
|
12
|
+
import type { PluginResult } from "@porulle/core";
|
|
13
|
+
import type { Db } from "../types.js";
|
|
14
|
+
type ChecklistType = "pre_billing" | "shift_open" | "shift_close";
|
|
15
|
+
export declare class ChecklistService {
|
|
16
|
+
private db;
|
|
17
|
+
constructor(db: Db);
|
|
18
|
+
createChecklist(orgId: string, input: {
|
|
19
|
+
name: string;
|
|
20
|
+
type: ChecklistType;
|
|
21
|
+
items: Array<{
|
|
22
|
+
label: string;
|
|
23
|
+
isRequired?: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
}): Promise<PluginResult<{
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
type: string;
|
|
29
|
+
items: Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
}>;
|
|
33
|
+
}>>;
|
|
34
|
+
listChecklists(orgId: string, type?: ChecklistType): Promise<PluginResult<Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
isActive: boolean;
|
|
39
|
+
}>>>;
|
|
40
|
+
getChecklistWithItems(checklistId: string, orgId?: string): Promise<PluginResult<{
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
type: string;
|
|
44
|
+
items: Array<{
|
|
45
|
+
id: string;
|
|
46
|
+
label: string;
|
|
47
|
+
isRequired: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
}>>;
|
|
50
|
+
completeChecklist(input: {
|
|
51
|
+
checklistId: string;
|
|
52
|
+
referenceType: "transaction" | "shift";
|
|
53
|
+
referenceId: string;
|
|
54
|
+
operatorId: string;
|
|
55
|
+
completedItems: Array<{
|
|
56
|
+
itemId: string;
|
|
57
|
+
checked: boolean;
|
|
58
|
+
note?: string;
|
|
59
|
+
}>;
|
|
60
|
+
}): Promise<PluginResult<{
|
|
61
|
+
completed: boolean;
|
|
62
|
+
}>>;
|
|
63
|
+
isChecklistCompleted(checklistId: string, referenceType: string, referenceId: string): Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=checklist-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checklist-service.d.ts","sourceRoot":"","sources":["../../src/services/checklist-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,KAAK,aAAa,GAAG,aAAa,GAAG,YAAY,GAAG,aAAa,CAAC;AAElE,qBAAa,gBAAgB;IACf,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEpB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QAC1C,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,aAAa,CAAC;QACpB,KAAK,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KACvD,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IA8B5G,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,CAAC;IAahJ,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QACrF,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,KAAK,CAAC;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KAClE,CAAC,CAAC;IAoBG,iBAAiB,CAAC,KAAK,EAAE;QAC7B,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC;QACvC,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,KAAK,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,OAAO,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC5E,GAAG,OAAO,CAAC,YAAY,CAAC;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IA0B3C,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAW9G"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChecklistService — Pre-billing compliance checklists.
|
|
3
|
+
*
|
|
4
|
+
* URY: Pre-billing checklists enforce compliance (stock check, hygiene).
|
|
5
|
+
* validate_invoice_print() blocks billing until checklist complete.
|
|
6
|
+
*
|
|
7
|
+
* Checklists can be of type:
|
|
8
|
+
* - pre_billing: must complete before printing a bill
|
|
9
|
+
* - shift_open: must complete when opening a shift
|
|
10
|
+
* - shift_close: must complete when closing a shift
|
|
11
|
+
*/
|
|
12
|
+
import { eq, and } from "@porulle/core/drizzle";
|
|
13
|
+
import { Ok, Err } from "@porulle/core";
|
|
14
|
+
import { posChecklists, posChecklistItems, posChecklistCompletions } from "../schema.js";
|
|
15
|
+
export class ChecklistService {
|
|
16
|
+
db;
|
|
17
|
+
constructor(db) {
|
|
18
|
+
this.db = db;
|
|
19
|
+
}
|
|
20
|
+
async createChecklist(orgId, input) {
|
|
21
|
+
const rows = await this.db
|
|
22
|
+
.insert(posChecklists)
|
|
23
|
+
.values({ organizationId: orgId, name: input.name, type: input.type })
|
|
24
|
+
.returning();
|
|
25
|
+
const checklist = rows[0];
|
|
26
|
+
const itemRows = [];
|
|
27
|
+
for (let i = 0; i < input.items.length; i++) {
|
|
28
|
+
const item = input.items[i];
|
|
29
|
+
const inserted = await this.db
|
|
30
|
+
.insert(posChecklistItems)
|
|
31
|
+
.values({
|
|
32
|
+
checklistId: checklist.id,
|
|
33
|
+
label: item.label,
|
|
34
|
+
isRequired: item.isRequired ?? true,
|
|
35
|
+
sortOrder: i,
|
|
36
|
+
})
|
|
37
|
+
.returning();
|
|
38
|
+
itemRows.push(inserted[0]);
|
|
39
|
+
}
|
|
40
|
+
return Ok({
|
|
41
|
+
id: checklist.id,
|
|
42
|
+
name: checklist.name,
|
|
43
|
+
type: checklist.type,
|
|
44
|
+
items: itemRows.map((r) => ({ id: r.id, label: r.label })),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async listChecklists(orgId, type) {
|
|
48
|
+
const conditions = [eq(posChecklists.organizationId, orgId)];
|
|
49
|
+
if (type)
|
|
50
|
+
conditions.push(eq(posChecklists.type, type));
|
|
51
|
+
const rows = await this.db
|
|
52
|
+
.select()
|
|
53
|
+
.from(posChecklists)
|
|
54
|
+
.where(and(...conditions))
|
|
55
|
+
.orderBy(posChecklists.sortOrder);
|
|
56
|
+
return Ok(rows.map((r) => ({ id: r.id, name: r.name, type: r.type, isActive: r.isActive })));
|
|
57
|
+
}
|
|
58
|
+
async getChecklistWithItems(checklistId, orgId) {
|
|
59
|
+
const conditions = [eq(posChecklists.id, checklistId)];
|
|
60
|
+
if (orgId)
|
|
61
|
+
conditions.push(eq(posChecklists.organizationId, orgId));
|
|
62
|
+
const checklists = await this.db.select().from(posChecklists).where(and(...conditions));
|
|
63
|
+
if (checklists.length === 0)
|
|
64
|
+
return Err("Checklist not found");
|
|
65
|
+
const items = await this.db
|
|
66
|
+
.select()
|
|
67
|
+
.from(posChecklistItems)
|
|
68
|
+
.where(eq(posChecklistItems.checklistId, checklistId))
|
|
69
|
+
.orderBy(posChecklistItems.sortOrder);
|
|
70
|
+
return Ok({
|
|
71
|
+
id: checklists[0].id,
|
|
72
|
+
name: checklists[0].name,
|
|
73
|
+
type: checklists[0].type,
|
|
74
|
+
items: items.map((i) => ({ id: i.id, label: i.label, isRequired: i.isRequired })),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async completeChecklist(input) {
|
|
78
|
+
// Validate all required items are checked
|
|
79
|
+
const items = await this.db
|
|
80
|
+
.select()
|
|
81
|
+
.from(posChecklistItems)
|
|
82
|
+
.where(eq(posChecklistItems.checklistId, input.checklistId));
|
|
83
|
+
const requiredItems = items.filter((i) => i.isRequired);
|
|
84
|
+
for (const required of requiredItems) {
|
|
85
|
+
const completion = input.completedItems.find((c) => c.itemId === required.id);
|
|
86
|
+
if (!completion || !completion.checked) {
|
|
87
|
+
return Err(`Required checklist item '${required.label}' is not checked`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await this.db.insert(posChecklistCompletions).values({
|
|
91
|
+
checklistId: input.checklistId,
|
|
92
|
+
referenceType: input.referenceType,
|
|
93
|
+
referenceId: input.referenceId,
|
|
94
|
+
operatorId: input.operatorId,
|
|
95
|
+
completedItems: input.completedItems,
|
|
96
|
+
});
|
|
97
|
+
return Ok({ completed: true });
|
|
98
|
+
}
|
|
99
|
+
async isChecklistCompleted(checklistId, referenceType, referenceId) {
|
|
100
|
+
const rows = await this.db
|
|
101
|
+
.select()
|
|
102
|
+
.from(posChecklistCompletions)
|
|
103
|
+
.where(and(eq(posChecklistCompletions.checklistId, checklistId), eq(posChecklistCompletions.referenceType, referenceType), eq(posChecklistCompletions.referenceId, referenceId)));
|
|
104
|
+
return rows.length > 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDSService — Kitchen Display System station management, ticket generation,
|
|
3
|
+
* and status tracking.
|
|
4
|
+
*
|
|
5
|
+
* Informed by URY's KOT system:
|
|
6
|
+
* - URY Production Unit: item-group-based routing, per-station printers
|
|
7
|
+
* - URY KOT: submittable ticket per production unit, type enum, order_status,
|
|
8
|
+
* production_time tracking, Socket.IO broadcasting
|
|
9
|
+
* - URY KOT Items: course (Menu Course link), serve_priority, indicate_course,
|
|
10
|
+
* cancelled_qty
|
|
11
|
+
* - kot_execute(): compares previous vs current items, generates New Order,
|
|
12
|
+
* Order Modified, Partially cancelled KOTs per production unit
|
|
13
|
+
* - multi_print_kot(): production printer > room printer > POS printer cascade
|
|
14
|
+
*
|
|
15
|
+
* Improvements over URY:
|
|
16
|
+
* - Item-level status persisted to DB (not browser localStorage)
|
|
17
|
+
* - 4-state ticket status (pending -> preparing -> ready -> served)
|
|
18
|
+
* instead of URY's 2-state (Ready For Prepare -> Served)
|
|
19
|
+
* - prep_duration_seconds calculated and stored for analytics
|
|
20
|
+
* - Structured course priority on ticket items (not just display ordering)
|
|
21
|
+
*/
|
|
22
|
+
import type { Db, KDSStation, KDSStationItemGroup, KDSTicket, KDSTicketItem } from "../types.js";
|
|
23
|
+
import type { PluginResult } from "@porulle/core";
|
|
24
|
+
export declare class KDSService {
|
|
25
|
+
private db;
|
|
26
|
+
constructor(db: Db);
|
|
27
|
+
createStation(orgId: string, input: {
|
|
28
|
+
name: string;
|
|
29
|
+
alertThresholdMinutes?: number;
|
|
30
|
+
metadata?: Record<string, unknown>;
|
|
31
|
+
}): Promise<PluginResult<KDSStation>>;
|
|
32
|
+
listStations(orgId: string): Promise<PluginResult<KDSStation[]>>;
|
|
33
|
+
updateStation(orgId: string, id: string, input: {
|
|
34
|
+
name?: string;
|
|
35
|
+
isActive?: boolean;
|
|
36
|
+
alertThresholdMinutes?: number;
|
|
37
|
+
}): Promise<PluginResult<KDSStation>>;
|
|
38
|
+
addItemGroup(stationId: string, itemGroup: string): Promise<PluginResult<KDSStationItemGroup>>;
|
|
39
|
+
removeItemGroup(stationId: string, itemGroup: string): Promise<PluginResult<{
|
|
40
|
+
removed: boolean;
|
|
41
|
+
}>>;
|
|
42
|
+
getStationItemGroups(stationId: string): Promise<string[]>;
|
|
43
|
+
generateTickets(orgId: string, input: {
|
|
44
|
+
transactionId: string;
|
|
45
|
+
items: Array<{
|
|
46
|
+
entityId: string;
|
|
47
|
+
variantId?: string;
|
|
48
|
+
itemName: string;
|
|
49
|
+
quantity: number;
|
|
50
|
+
itemGroup: string;
|
|
51
|
+
courseName?: string;
|
|
52
|
+
coursePriority?: number;
|
|
53
|
+
showCourseLabel?: boolean;
|
|
54
|
+
modifiers?: Array<{
|
|
55
|
+
name: string;
|
|
56
|
+
priceAdjustment: number;
|
|
57
|
+
}>;
|
|
58
|
+
notes?: string;
|
|
59
|
+
}>;
|
|
60
|
+
tableNumber?: string;
|
|
61
|
+
orderType?: "dine_in" | "takeaway" | "delivery";
|
|
62
|
+
operatorName?: string;
|
|
63
|
+
comments?: string;
|
|
64
|
+
}): Promise<PluginResult<KDSTicket[]>>;
|
|
65
|
+
startTicket(id: string): Promise<PluginResult<KDSTicket>>;
|
|
66
|
+
readyTicket(id: string): Promise<PluginResult<KDSTicket>>;
|
|
67
|
+
serveTicket(id: string): Promise<PluginResult<KDSTicket>>;
|
|
68
|
+
private static readonly VALID_TRANSITIONS;
|
|
69
|
+
private updateTicketStatus;
|
|
70
|
+
markItemDone(ticketId: string, itemId: string): Promise<PluginResult<KDSTicketItem>>;
|
|
71
|
+
getTicketById(id: string): Promise<PluginResult<KDSTicket>>;
|
|
72
|
+
listPendingTickets(orgId: string, stationId: string): Promise<PluginResult<Array<KDSTicket & {
|
|
73
|
+
items: KDSTicketItem[];
|
|
74
|
+
}>>>;
|
|
75
|
+
private generateTicketNumber;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=kds-service.d.ts.map
|