@alizadefaig/pos-server 1.2.31 → 1.2.32

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.
@@ -29,6 +29,7 @@ const CORE_MUTATIONS = new Set([
29
29
  'orders.addExtra',
30
30
  'orders.changeTable',
31
31
  'orders.changeWaiter',
32
+ 'orders.fixSlot',
32
33
  'orders.changeGuestCount',
33
34
  'orders.setComment',
34
35
  'orders.addCustomer',
@@ -4,6 +4,7 @@ const SnapshotRepository = require('./SnapshotRepository');
4
4
  const {toSql, nowSql} = require('../support/time');
5
5
  const {phpJsonEncode} = require('../support/str');
6
6
  const {castColumn} = require('../support/db');
7
+ const {toInt} = require('../support/num');
7
8
 
8
9
  // Колонки, которые ядро пишет само. Список явный: SELECT * читаем, а вот при записи
9
10
  // молчаливый «сохраним всё, что оказалось на объекте» уронил бы UPDATE на первом же
@@ -96,6 +97,17 @@ class OrderRepository {
96
97
  return Promise.all(rows.map((row) => this.hydrate(row)));
97
98
  }
98
99
 
100
+ /** Слоты открытых чеков официанта (режим слотов), кроме чека exceptId. */
101
+ async openSlotIndexes(companyId, servingId, exceptId) {
102
+ const rows = await this.db.all(
103
+ `SELECT slot_index FROM orders
104
+ WHERE company_id = ? AND serving_id = ? AND id <> ?
105
+ AND status NOT IN ('closing', 'closed') AND deleted_at IS NULL`,
106
+ [companyId, servingId, exceptId]
107
+ );
108
+ return rows.map((row) => toInt(row.slot_index, 1));
109
+ }
110
+
99
111
  async archivedOrders(companyId, fromSql, toSql_) {
100
112
  const rows = await this.db.all(
101
113
  `SELECT * FROM orders
@@ -610,12 +610,43 @@ class OrderService {
610
610
  }
611
611
 
612
612
  async changeWaiter({orderId, waiterId}) {
613
- this.assertMutable(await this.findOrFail(orderId), 'orders.changeWaiter');
614
- await this.orders.updateRow(orderId, {serving_id: waiterId});
613
+ const order = await this.findOrFail(orderId);
614
+ this.assertMutable(order, 'orders.changeWaiter');
615
+ await this.orders.updateRow(orderId, {serving_id: waiterId, slot_index: await this.freeSlotIndex(order, waiterId)});
615
616
  await this.saveEvents([{event: 'orders:change_waiter', time: nowSql(), order_id: orderId}]);
616
617
  return await this.patchOrResource(orderId);
617
618
  }
618
619
 
620
+ /** Режим слотов: чек из занятого слота — в свободный (OrderService::fixSlot в Laravel). */
621
+ async fixSlot({orderId}) {
622
+ const order = await this.findOrFail(orderId);
623
+ this.assertMutable(order, 'orders.fixSlot');
624
+ const slotIndex = await this.freeSlotIndex(order, order.serving_id);
625
+ if (slotIndex === toInt(order.slot_index, 1)) {
626
+ return await this.resource().order(order);
627
+ }
628
+ await this.orders.updateRow(orderId, {slot_index: slotIndex});
629
+ return await this.patchOrResource(orderId);
630
+ }
631
+
632
+ /**
633
+ * Режим слотов: переданный чек сохраняет слот, только если у нового официанта он свободен,
634
+ * иначе — первый свободный (два чека в одном слоте выглядели как пропажа одного).
635
+ * Как OrderService::freeSlotIndex в Laravel.
636
+ */
637
+ async freeSlotIndex(order, waiterId) {
638
+ const current = Math.max(1, toInt(order.slot_index, 1));
639
+ const taken = new Set(await this.orders.openSlotIndexes(order.company_id, waiterId, order.id));
640
+ if (!taken.has(current)) {
641
+ return current;
642
+ }
643
+ let index = 1;
644
+ while (taken.has(index)) {
645
+ index++;
646
+ }
647
+ return index;
648
+ }
649
+
619
650
  async changeGuestCount({orderId, guestCount = 1}) {
620
651
  this.assertMutable(await this.findOrFail(orderId), 'orders.changeGuestCount');
621
652
  await this.orders.updateRow(orderId, {guest_count: toInt(guestCount, 1)});
package/app/sync/index.js CHANGED
@@ -284,7 +284,18 @@ const snapshotDue = () => {
284
284
  return now - state.lastSnapshotOk >= config.intervals().snapshot;
285
285
  };
286
286
 
287
- const runSnapshot = async () => {
287
+ // Один снапшот за раз: цикл и внеочередной refresh (/local/refresh) ждут один и тот же.
288
+ let snapshotRun = null;
289
+ const runSnapshot = () => {
290
+ if (!snapshotRun) {
291
+ snapshotRun = pullAndApplySnapshot().finally(() => {
292
+ snapshotRun = null;
293
+ });
294
+ }
295
+ return snapshotRun;
296
+ };
297
+
298
+ const pullAndApplySnapshot = async () => {
288
299
  state.lastSnapshotTry = Date.now();
289
300
  const pullFn = isFollower() ? snapshot.pullInitOnly : snapshot.pullSnapshot;
290
301
  const result = await pullFn({store: state.store, cloud: state.cloud});
@@ -1413,6 +1424,31 @@ const logout = async ({token} = {}) => {
1413
1424
  return {success: true, handled: true, parked: true, pending: state.pending, master: isServingAsMaster()};
1414
1425
  };
1415
1426
 
1427
+ /**
1428
+ * POST /local/refresh — «Keşi təmizlə» на кассе: снапшот справочников и app.init прямо сейчас,
1429
+ * а не через плановые OFFLINE_SNAPSHOT_MS. Без этого в локальном режиме правка в бэк-офисе
1430
+ * (режим зала, товары, столы) доходила до кассы только через 10 минут. Облака нет — 503.
1431
+ */
1432
+ const refresh = async () => {
1433
+ if (!config.isSyncEnabled()) {
1434
+ return {success: false, enabled: false};
1435
+ }
1436
+ await start();
1437
+ if (!state.session || !state.cloud || !state.store) {
1438
+ throw new HttpError(409, {success: false, error_code: 'no_session', message: 'No terminal session'});
1439
+ }
1440
+ try {
1441
+ await runSnapshot();
1442
+ } catch (e) {
1443
+ if (e.name === 'CloudUnavailable') {
1444
+ throw new HttpError(503, {success: false, error_code: 'cloud_unavailable', message: e.message});
1445
+ }
1446
+ state.lastError = `snapshot: ${e.message}`;
1447
+ throw e;
1448
+ }
1449
+ return {success: true, snapshot_at: kv.get(state.store, 'snapshot_at')};
1450
+ };
1451
+
1416
1452
  /**
1417
1453
  * POST /local/resume — вход обратно на запаркованную сессию по PIN сотрудника. PIN проверяется,
1418
1454
  * как в офлайн-auth.unlock (тот же список users и тот же счётчик неудач). Отдаёт то, что фронт
@@ -1638,6 +1674,7 @@ module.exports = {
1638
1674
  clearSession,
1639
1675
  logout,
1640
1676
  resume,
1677
+ refresh,
1641
1678
  wake,
1642
1679
  relayBroadcast,
1643
1680
  terminalRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizadefaig/pos-server",
3
- "version": "1.2.31",
3
+ "version": "1.2.32",
4
4
  "description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
5
5
  "homepage": "https://gitlab.com/faigalizade/pos-server#readme",
6
6
  "bugs": {
@@ -66,6 +66,10 @@ registerOfflineRoutes(router, '/api', {
66
66
  waiterId: get(p, 'waiter_id'),
67
67
  })],
68
68
 
69
+ 'orders.fixSlot': [MUTATION, ({orders}, p) => orders.fixSlot({
70
+ orderId: get(p, 'id'),
71
+ })],
72
+
69
73
  'orders.changeGuestCount': [MUTATION, ({orders}, p) => orders.changeGuestCount({
70
74
  orderId: get(p, 'id'),
71
75
  guestCount: get(p, 'guest_count', 1),
@@ -18,6 +18,7 @@ const {normalizeApiUrl} = require('../../app/sync/cloud');
18
18
  * POST /local/logout — выход терминала: сессия паркуется, токен отзывается, когда
19
19
  * очередь дослана и облако на связи;
20
20
  * POST /local/resume — вход обратно на запаркованную сессию по PIN сотрудника (без облака);
21
+ * POST /local/refresh — внеочередной снапшот («Keşi təmizlə»): свежие справочники и init;
21
22
  * GET /local/whoami — мастер подписанно отвечает ведомому, который его ищет;
22
23
  * GET /local/lan — телефону на LAN-странице мастера: пора ли назад в облако.
23
24
  *
@@ -127,4 +128,7 @@ router.post('/local/logout', loopbackOnly, strictOriginGuard,
127
128
  router.post('/local/resume', loopbackOnly, strictOriginGuard,
128
129
  respond('resume', (body) => sync.resume({code: body.code})));
129
130
 
131
+ router.post('/local/refresh', loopbackOnly, strictOriginGuard,
132
+ respond('refresh', () => sync.refresh()));
133
+
130
134
  module.exports = router;