@fayz-ai/plugin-orders 0.11.3 → 0.12.1

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.
Files changed (37) hide show
  1. package/dist/OrdersPage-IKIIQALX.js +450 -0
  2. package/dist/OrdersPage-IKIIQALX.js.map +1 -0
  3. package/dist/chunk-TYS3LCKX.js +57 -0
  4. package/dist/chunk-TYS3LCKX.js.map +1 -0
  5. package/dist/context.d.ts +9 -0
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/data/core-supabase.d.ts +26 -0
  8. package/dist/data/core-supabase.d.ts.map +1 -0
  9. package/dist/data/mock.d.ts.map +1 -1
  10. package/dist/data/platform.d.ts +1 -1
  11. package/dist/index.d.ts +59 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +608 -38
  14. package/dist/index.js.map +1 -1
  15. package/dist/lib/floor-plan-id.d.ts +13 -0
  16. package/dist/lib/floor-plan-id.d.ts.map +1 -0
  17. package/dist/locales/en.d.ts.map +1 -1
  18. package/dist/locales/pt-BR.d.ts.map +1 -1
  19. package/dist/store.d.ts +5 -1
  20. package/dist/store.d.ts.map +1 -1
  21. package/dist/types.d.ts +20 -3
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/views/CommercialOrdersScreen.d.ts +3 -0
  24. package/dist/views/CommercialOrdersScreen.d.ts.map +1 -0
  25. package/dist/views/OrderReceivableActions.d.ts +12 -0
  26. package/dist/views/OrderReceivableActions.d.ts.map +1 -0
  27. package/dist/views/OrdersPage.d.ts +7 -1
  28. package/dist/views/OrdersPage.d.ts.map +1 -1
  29. package/dist/views/OrdersScreen.d.ts +3 -0
  30. package/dist/views/OrdersScreen.d.ts.map +1 -0
  31. package/package.json +6 -4
  32. package/dist/OrdersPage-CQ7O74ZW.js +0 -155
  33. package/dist/OrdersPage-CQ7O74ZW.js.map +0 -1
  34. package/dist/chunk-MTVIXIPL.js +0 -23
  35. package/dist/chunk-MTVIXIPL.js.map +0 -1
  36. package/dist/views/OrderKanbanView.d.ts +0 -3
  37. package/dist/views/OrderKanbanView.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,15 @@
1
- import { getNextStatus, STATUS_FLOW } from './chunk-MTVIXIPL.js';
2
- import React from 'react';
1
+ import { getNextStatus, STATUS_FLOW } from './chunk-TYS3LCKX.js';
2
+ export { ADVANCE_VERB, DEFAULT_STATUS_CONFIGS, KANBAN_COLUMNS, STATUS_FLOW, getNextStatus, getPreviousStatus, mapToKanbanColumn } from './chunk-TYS3LCKX.js';
3
+ import * as React2 from 'react';
4
+ import React2__default from 'react';
5
+ import { fayzConnectorOAuth, OrderDetailPage, orderIsEditable, PageActions, adminNavigateTo, PluginSettingsPanel, dedup, usePermissionOptional } from '@fayz-ai/admin';
6
+ import { Pencil } from 'lucide-react';
7
+ import { createConnectionStore, connectionRuns, connectionSettings, connectionStatus, settingsStatus, getSupabaseClientOptional, usePluginRuntimeOptional, emitDomainEvent, errorMessage } from '@fayz-ai/core';
3
8
  import { createStore } from 'zustand/vanilla';
4
- import { fayzConnectorOAuth, PluginSettingsPanel, dedup } from '@fayz-ai/admin';
5
9
  import { toast } from 'sonner';
6
- import { createConnectionStore, connectionRuns, connectionSettings, connectionStatus, settingsStatus } from '@fayz-ai/core';
10
+ import { ReceivePaymentModal } from '@fayz-ai/plugin-financial';
11
+ import { Button, toast as toast$1 } from '@fayz-ai/ui';
12
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
7
13
  import { createFayzClient } from '@fayz-ai/sdk';
8
14
 
9
15
  // src/data/mock.ts
@@ -278,7 +284,7 @@ function createMockOrdersProvider() {
278
284
  const flow = STATUS_FLOW[order.kind];
279
285
  const currentIdx = flow.indexOf(order.status);
280
286
  const targetIdx = flow.indexOf(input.status);
281
- if (targetIdx < 0 || targetIdx !== currentIdx + 1) {
287
+ if (targetIdx < 0 || targetIdx !== currentIdx && targetIdx !== currentIdx + 1) {
282
288
  const next = getNextStatus(order.kind, order.status);
283
289
  throw new Error(
284
290
  `Invalid status transition: ${order.status} \u2192 ${input.status}. ` + (next ? `Expected next status: ${next}` : "Order is already in a terminal state.")
@@ -411,7 +417,349 @@ Cancelled: ${reason}` : `Cancelled: ${reason}`;
411
417
  };
412
418
  return provider;
413
419
  }
414
- function createOrdersStore(provider) {
420
+ var READ_ONLY = "This provider reads the core order archetype; pass `writes` to let the app that owns the flow create and edit them.";
421
+ var DEFAULT_CHANNELS = {
422
+ dine_in: "dine_in",
423
+ delivery: "delivery",
424
+ takeout: "balcao"
425
+ };
426
+ function createCoreOrdersProvider(options) {
427
+ const db = () => {
428
+ const client = getSupabaseClientOptional();
429
+ if (!client) throw new Error("Supabase client is not configured");
430
+ return client;
431
+ };
432
+ const writes = options?.writes;
433
+ function requireWrites() {
434
+ if (!writes) throw new Error(READ_ONLY);
435
+ return writes;
436
+ }
437
+ async function load(query, only) {
438
+ const client = db();
439
+ let q = client.from("v_orders").select("id, tenant_id, unit_id, kind, channel, status, reference_number, party_id, assignee_id, subtotal, discount, tax, total, currency, notes, created_at, updated_at, completed_at, fulfillment_status, metadata", { count: "exact" }).order("created_at", { ascending: false });
440
+ if (only?.id) q = q.eq("id", only.id);
441
+ if (options?.kinds?.length) q = q.in("kind", options.kinds);
442
+ if (options?.channel) q = q.eq("channel", options.channel);
443
+ if (query?.kind) {
444
+ q = Array.isArray(query.kind) ? q.in("kind", query.kind) : q.eq("kind", query.kind);
445
+ }
446
+ if (query?.status) {
447
+ q = Array.isArray(query.status) ? q.in("status", query.status) : q.eq("status", query.status);
448
+ }
449
+ if (query?.tableId) q = q.eq("metadata->>tableId", query.tableId);
450
+ if (query?.assigneeId) q = q.eq("assignee_id", query.assigneeId);
451
+ if (query?.dateRange) {
452
+ q = q.gte("created_at", query.dateRange.from).lte("created_at", query.dateRange.to);
453
+ }
454
+ const pageSize = query?.pageSize ?? 100;
455
+ const from = ((query?.page ?? 1) - 1) * pageSize;
456
+ q = q.range(from, from + pageSize - 1);
457
+ const { data, error, count } = await q;
458
+ if (error) throw new Error(error.message);
459
+ const rows = data ?? [];
460
+ const ids = rows.map((r) => r.id);
461
+ const [{ data: people }, { data: payments }, { data: items }] = await Promise.all([
462
+ ids.length ? client.from("people").select("id, name").in("id", [...new Set(rows.flatMap((r) => [r.party_id, r.assignee_id]).filter(Boolean))]) : Promise.resolve({ data: [] }),
463
+ ids.length ? client.from("v_order_payment_status").select("order_id, status, total_amount, paid_amount").in("order_id", ids) : Promise.resolve({ data: [] }),
464
+ ids.length ? client.from("items").select("id, order_id, product_id, name, description, quantity, unit_price, discount, total, sort_order, metadata, created_at").in("order_id", ids).order("sort_order") : Promise.resolve({ data: [] })
465
+ ]);
466
+ const nameById = new Map((people ?? []).map((p) => [p.id, p.name]));
467
+ const payById = new Map((payments ?? []).map((p) => [p.order_id, p]));
468
+ const itemsByOrder = /* @__PURE__ */ new Map();
469
+ for (const item of items ?? []) {
470
+ const list = itemsByOrder.get(item.order_id) ?? [];
471
+ list.push(toItem(item));
472
+ itemsByOrder.set(item.order_id, list);
473
+ }
474
+ return {
475
+ data: rows.map((r) => {
476
+ const pay = payById.get(r.id);
477
+ const meta = r.metadata ?? {};
478
+ return {
479
+ id: r.id,
480
+ referenceNumber: r.reference_number ?? String(r.id).slice(0, 8),
481
+ kind: r.kind,
482
+ status: r.status,
483
+ partyId: r.party_id ?? void 0,
484
+ partyName: r.party_id ? nameById.get(r.party_id) : void 0,
485
+ assigneeId: r.assignee_id ?? void 0,
486
+ assigneeName: r.assignee_id ? nameById.get(r.assignee_id) : void 0,
487
+ subtotal: Number(r.subtotal) || 0,
488
+ discount: Number(r.discount) || 0,
489
+ tax: Number(r.tax) || 0,
490
+ total: Number(r.total) || 0,
491
+ currency: r.currency ?? "BRL",
492
+ notes: r.notes ?? void 0,
493
+ tags: [],
494
+ // `source` is the archetype's channel where it has one; the salon's
495
+ // counter sale has none, and 'balcao' is what that means.
496
+ source: r.channel ?? "balcao",
497
+ // NOT location_id: that column is a FK to `app.units`, i.e. the
498
+ // BRANCH this order belongs to. A mesa has no table of its own yet,
499
+ // so its id rides in metadata until the floor plan gets one.
500
+ tableId: typeof meta.tableId === "string" ? meta.tableId : void 0,
501
+ estimatedMinutes: typeof meta.estimatedMinutes === "number" ? meta.estimatedMinutes : void 0,
502
+ items: itemsByOrder.get(r.id) ?? [],
503
+ metadata: {
504
+ ...meta,
505
+ channel: r.channel ?? null,
506
+ fulfillmentStatus: r.fulfillment_status ?? null,
507
+ paymentStatus: pay?.status ?? "none",
508
+ invoicedAmount: Number(pay?.total_amount) || 0,
509
+ paidAmount: Number(pay?.paid_amount) || 0
510
+ },
511
+ tenantId: r.tenant_id,
512
+ unitId: r.unit_id ?? void 0,
513
+ createdAt: r.created_at,
514
+ updatedAt: r.updated_at,
515
+ completedAt: r.completed_at ?? void 0
516
+ };
517
+ }),
518
+ total: count ?? rows.length
519
+ };
520
+ }
521
+ function toItem(row) {
522
+ const meta = row.metadata ?? {};
523
+ return {
524
+ id: row.id,
525
+ orderId: row.order_id,
526
+ productId: row.product_id ?? void 0,
527
+ name: row.name ?? "",
528
+ description: row.description ?? void 0,
529
+ quantity: Number(row.quantity) || 0,
530
+ unitPrice: Number(row.unit_price) || 0,
531
+ discount: Number(row.discount) || 0,
532
+ total: Number(row.total) || 0,
533
+ sortOrder: row.sort_order ?? 0,
534
+ modifiers: typeof meta.modifiers === "string" ? meta.modifiers : void 0,
535
+ kitchenNotes: typeof meta.kitchenNotes === "string" ? meta.kitchenNotes : void 0,
536
+ metadata: meta,
537
+ createdAt: row.created_at
538
+ };
539
+ }
540
+ function lineTotal(item) {
541
+ return item.quantity * item.unitPrice - (item.discount ?? 0);
542
+ }
543
+ async function loadOne(id) {
544
+ const page = await load(void 0, { id });
545
+ return page.data[0] ?? null;
546
+ }
547
+ async function recalc(orderId) {
548
+ const client = db();
549
+ const { data, error } = await client.from("items").select("total").eq("order_id", orderId);
550
+ if (error) throw new Error(error.message);
551
+ const subtotal = (data ?? []).reduce((sum, r) => sum + (Number(r.total) || 0), 0);
552
+ const { error: upErr } = await client.from("orders").update({ subtotal, total: subtotal }).eq("id", orderId);
553
+ if (upErr) throw new Error(upErr.message);
554
+ }
555
+ async function orderIdOfItem(itemId) {
556
+ const client = db();
557
+ const { data, error } = await client.from("items").select("order_id").eq("id", itemId).single();
558
+ if (error) throw new Error(error.message);
559
+ return data.order_id;
560
+ }
561
+ return {
562
+ getOrders: (query) => load(query),
563
+ getOrderById: loadOne,
564
+ async createOrder(input) {
565
+ const w = requireWrites();
566
+ const client = db();
567
+ const tenantId = w.tenantId();
568
+ if (!tenantId) throw new Error("No active organization: an order cannot be opened without one");
569
+ const channel = (w.channelFor ?? ((k) => DEFAULT_CHANNELS[k]))(input.kind);
570
+ const subtotal = input.items.reduce(
571
+ (sum, i) => sum + lineTotal({ quantity: i.quantity, unitPrice: i.unitPrice, discount: i.discount }),
572
+ 0
573
+ );
574
+ const { data: order, error } = await client.from("orders").insert({
575
+ tenant_id: tenantId,
576
+ unit_id: w.unitId?.() ?? null,
577
+ kind: input.kind,
578
+ status: "new",
579
+ ...channel ? { channel } : {},
580
+ // party_id is left out on purpose: `trg_orders_require_party` fills it
581
+ // with `public.walkin_party(tenant_id)`, which is exactly what a table
582
+ // with no named customer is.
583
+ ...input.partyId ? { party_id: input.partyId } : {},
584
+ ...input.assigneeId ? { assignee_id: input.assigneeId } : {},
585
+ ...input.notes ? { notes: input.notes } : {},
586
+ subtotal,
587
+ total: subtotal,
588
+ currency: "BRL",
589
+ metadata: {
590
+ ...input.tableId ? { tableId: input.tableId } : {},
591
+ ...input.estimatedMinutes ? { estimatedMinutes: input.estimatedMinutes } : {}
592
+ }
593
+ }).select("id").single();
594
+ if (error) throw new Error(error.message);
595
+ if (input.items.length) {
596
+ const { error: itemsErr } = await client.from("items").insert(
597
+ input.items.map((item, index) => ({
598
+ order_id: order.id,
599
+ // tenant_id and parent_kind are inherited from the parent by
600
+ // `trg_items_inherit`; setting them here would only duplicate it.
601
+ product_id: item.productId ?? null,
602
+ name: item.name,
603
+ quantity: item.quantity,
604
+ unit_price: item.unitPrice,
605
+ discount: item.discount ?? 0,
606
+ total: lineTotal(item),
607
+ sort_order: index,
608
+ metadata: {
609
+ ...item.modifiers ? { modifiers: item.modifiers } : {},
610
+ ...item.kitchenNotes ? { kitchenNotes: item.kitchenNotes } : {}
611
+ }
612
+ }))
613
+ );
614
+ if (itemsErr) throw new Error(itemsErr.message);
615
+ }
616
+ const created = await loadOne(order.id);
617
+ if (!created) throw new Error("The order was created but is not readable \u2014 check the orders.read permission");
618
+ return created;
619
+ },
620
+ async updateOrderStatus(input) {
621
+ requireWrites();
622
+ const client = db();
623
+ const { error } = await client.from("orders").update({ status: input.status }).eq("id", input.orderId);
624
+ if (error) throw new Error(error.message);
625
+ const order = await loadOne(input.orderId);
626
+ if (!order) throw new Error("Order not found after the status change");
627
+ return order;
628
+ },
629
+ async updateOrder(id, data) {
630
+ requireWrites();
631
+ const client = db();
632
+ const patch = {};
633
+ if (data.status !== void 0) patch.status = data.status;
634
+ if (data.notes !== void 0) patch.notes = data.notes;
635
+ if (data.partyId !== void 0) patch.party_id = data.partyId;
636
+ if (data.assigneeId !== void 0) patch.assignee_id = data.assigneeId;
637
+ if (data.discount !== void 0) patch.discount = data.discount;
638
+ if (data.tax !== void 0) patch.tax = data.tax;
639
+ if (Object.keys(patch).length) {
640
+ const { error } = await client.from("orders").update(patch).eq("id", id);
641
+ if (error) throw new Error(error.message);
642
+ }
643
+ const order = await loadOne(id);
644
+ if (!order) throw new Error("Order not found after the update");
645
+ return order;
646
+ },
647
+ async cancelOrder(id, reason) {
648
+ requireWrites();
649
+ const client = db();
650
+ const { error } = await client.from("orders").update({ status: "cancelled", ...reason ? { notes: reason } : {} }).eq("id", id);
651
+ if (error) throw new Error(error.message);
652
+ },
653
+ async addOrderItem(orderId, item) {
654
+ requireWrites();
655
+ const client = db();
656
+ const { count } = await client.from("items").select("id", { count: "exact", head: true }).eq("order_id", orderId);
657
+ const { data, error } = await client.from("items").insert({
658
+ order_id: orderId,
659
+ product_id: item.productId ?? null,
660
+ name: item.name,
661
+ quantity: item.quantity,
662
+ unit_price: item.unitPrice,
663
+ discount: item.discount ?? 0,
664
+ total: lineTotal(item),
665
+ sort_order: count ?? 0,
666
+ metadata: {
667
+ ...item.modifiers ? { modifiers: item.modifiers } : {},
668
+ ...item.kitchenNotes ? { kitchenNotes: item.kitchenNotes } : {}
669
+ }
670
+ }).select("id, order_id, product_id, name, description, quantity, unit_price, discount, total, sort_order, metadata, created_at").single();
671
+ if (error) throw new Error(error.message);
672
+ await recalc(orderId);
673
+ return toItem(data);
674
+ },
675
+ async removeOrderItem(itemId) {
676
+ requireWrites();
677
+ const client = db();
678
+ const orderId = await orderIdOfItem(itemId);
679
+ const { error } = await client.from("items").delete().eq("id", itemId);
680
+ if (error) throw new Error(error.message);
681
+ await recalc(orderId);
682
+ },
683
+ async updateOrderItem(itemId, data) {
684
+ requireWrites();
685
+ const client = db();
686
+ const orderId = await orderIdOfItem(itemId);
687
+ const patch = {};
688
+ if (data.name !== void 0) patch.name = data.name;
689
+ if (data.quantity !== void 0) patch.quantity = data.quantity;
690
+ if (data.unitPrice !== void 0) patch.unit_price = data.unitPrice;
691
+ if (data.discount !== void 0) patch.discount = data.discount;
692
+ if (data.quantity !== void 0 || data.unitPrice !== void 0 || data.discount !== void 0) {
693
+ const { data: current, error: readErr } = await client.from("items").select("quantity, unit_price, discount").eq("id", itemId).single();
694
+ if (readErr) throw new Error(readErr.message);
695
+ patch.total = lineTotal({
696
+ quantity: Number(data.quantity ?? current.quantity) || 0,
697
+ unitPrice: Number(data.unitPrice ?? current.unit_price) || 0,
698
+ discount: Number(data.discount ?? current.discount) || 0
699
+ });
700
+ }
701
+ const { data: row, error } = await client.from("items").update(patch).eq("id", itemId).select("id, order_id, product_id, name, description, quantity, unit_price, discount, total, sort_order, metadata, created_at").single();
702
+ if (error) throw new Error(error.message);
703
+ await recalc(orderId);
704
+ return toItem(row);
705
+ },
706
+ async getSummary(dateRange) {
707
+ const page = await load({ pageSize: 500, ...dateRange ? { dateRange } : {} });
708
+ const live = page.data.filter((o) => o.status !== "cancelled");
709
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
710
+ const todays = live.filter((o) => o.createdAt.slice(0, 10) === today);
711
+ const revenue = todays.reduce((sum, o) => sum + o.total, 0);
712
+ const byKind = {};
713
+ const byStatus = {};
714
+ for (const o of live) {
715
+ byKind[o.kind] = (byKind[o.kind] ?? 0) + 1;
716
+ byStatus[o.status] = (byStatus[o.status] ?? 0) + 1;
717
+ }
718
+ return {
719
+ todayOrderCount: todays.length,
720
+ todayRevenue: revenue,
721
+ averageTicket: todays.length ? revenue / todays.length : 0,
722
+ activeOrderCount: live.filter((o) => o.status !== "completed").length,
723
+ ordersByKind: byKind,
724
+ ordersByStatus: byStatus,
725
+ topItems: []
726
+ };
727
+ }
728
+ };
729
+ }
730
+ var ACTIVE_STATUSES = [
731
+ "new",
732
+ "confirmed",
733
+ "preparing",
734
+ "ready",
735
+ // The `Saiu` column: one word per kind for the same moment.
736
+ "served",
737
+ "picked_up",
738
+ "dispatched",
739
+ "delivered"
740
+ ];
741
+ function emitOrderLifecycleEvent(key, order) {
742
+ emitDomainEvent(key, {
743
+ order_id: order.id,
744
+ reference_number: order.referenceNumber,
745
+ total: order.total,
746
+ item_count: order.items.length,
747
+ source: order.source
748
+ }, {
749
+ tenantId: order.tenantId,
750
+ subject: { type: "order", id: order.id }
751
+ });
752
+ }
753
+ async function runLifecycleHook(label, order, hook) {
754
+ if (!hook) return;
755
+ try {
756
+ await hook(order);
757
+ } catch (error) {
758
+ const message = errorMessage(error);
759
+ throw new Error(`Order was ${label}, but downstream synchronization is pending: ${message}`);
760
+ }
761
+ }
762
+ function createOrdersStore(provider, hooks = {}) {
415
763
  return createStore((set, get) => ({
416
764
  orders: [],
417
765
  ordersTotal: 0,
@@ -432,9 +780,7 @@ function createOrdersStore(provider) {
432
780
  async fetchActiveOrders() {
433
781
  return dedup("orders:active", async () => {
434
782
  set({ activeOrdersLoading: true });
435
- const result = await provider.getOrders({
436
- status: ["new", "confirmed", "preparing", "ready"]
437
- });
783
+ const result = await provider.getOrders({ status: [...ACTIVE_STATUSES] });
438
784
  set({ activeOrders: result.data, activeOrdersLoading: false });
439
785
  });
440
786
  },
@@ -457,10 +803,10 @@ function createOrdersStore(provider) {
457
803
  provider.getSummary()
458
804
  ]);
459
805
  set({ orders: result.data, ordersTotal: result.total, summary });
460
- toast.success("Order created");
806
+ toast.success("Pedido criado");
461
807
  return order;
462
808
  } catch (err) {
463
- toast.error("Failed to create order", { description: err?.message });
809
+ toast.error("N\xE3o foi poss\xEDvel criar o pedido", { description: err?.message });
464
810
  throw err;
465
811
  }
466
812
  },
@@ -471,44 +817,58 @@ function createOrdersStore(provider) {
471
817
  if (!order) throw new Error("Order not found");
472
818
  const nextStatus = getNextStatus(order.kind, order.status);
473
819
  if (!nextStatus) throw new Error("No next status available");
474
- await provider.updateOrderStatus({ orderId, status: nextStatus });
475
- const activeResult = await provider.getOrders({
476
- status: ["new", "confirmed", "preparing", "ready"]
477
- });
820
+ const updated = await provider.updateOrderStatus({ orderId, status: nextStatus });
821
+ if (updated.status === "completed") {
822
+ emitOrderLifecycleEvent("orders.order_completed", updated);
823
+ await runLifecycleHook("completed", updated, hooks.onOrderCompleted);
824
+ }
825
+ const activeResult = await provider.getOrders({ status: [...ACTIVE_STATUSES] });
478
826
  set({ activeOrders: activeResult.data });
479
- toast.success(`Order advanced to ${nextStatus.replace("_", " ")}`);
827
+ toast.success("Pedido avan\xE7ado");
480
828
  } catch (err) {
481
- toast.error("Failed to advance order", { description: err?.message });
829
+ toast.error("N\xE3o foi poss\xEDvel avan\xE7ar o pedido", { description: err?.message });
482
830
  throw err;
483
831
  }
484
832
  },
485
833
  async updateOrderStatus(input) {
486
834
  try {
487
- await provider.updateOrderStatus(input);
835
+ const before = await provider.getOrderById(input.orderId);
836
+ const updated = await provider.updateOrderStatus(input);
837
+ if (updated.status === "completed") {
838
+ if (before?.status !== "completed") emitOrderLifecycleEvent("orders.order_completed", updated);
839
+ await runLifecycleHook("completed", updated, hooks.onOrderCompleted);
840
+ }
488
841
  const query = get().orderQuery;
489
842
  const [result, summary] = await Promise.all([
490
843
  provider.getOrders(query),
491
844
  provider.getSummary()
492
845
  ]);
493
846
  set({ orders: result.data, ordersTotal: result.total, summary });
494
- toast.success("Order status updated");
847
+ toast.success("Status atualizado");
495
848
  } catch (err) {
496
- toast.error("Failed to update status", { description: err?.message });
849
+ toast.error("N\xE3o foi poss\xEDvel atualizar o status", { description: err?.message });
497
850
  throw err;
498
851
  }
499
852
  },
500
853
  async cancelOrder(id, reason) {
501
854
  try {
855
+ const before = await provider.getOrderById(id);
502
856
  await provider.cancelOrder(id, reason);
857
+ const cancelled = await provider.getOrderById(id);
858
+ const order = cancelled ?? (before ? { ...before, status: "cancelled" } : null);
859
+ if (order) {
860
+ if (before?.status !== "cancelled") emitOrderLifecycleEvent("orders.order_cancelled", order);
861
+ await runLifecycleHook("cancelled", order, hooks.onOrderCancelled);
862
+ }
503
863
  const query = get().orderQuery;
504
864
  const [result, summary] = await Promise.all([
505
865
  provider.getOrders(query),
506
866
  provider.getSummary()
507
867
  ]);
508
868
  set({ orders: result.data, ordersTotal: result.total, summary });
509
- toast.success("Order cancelled");
869
+ toast.success("Pedido cancelado");
510
870
  } catch (err) {
511
- toast.error("Failed to cancel order", { description: err?.message });
871
+ toast.error("N\xE3o foi poss\xEDvel cancelar o pedido", { description: err?.message });
512
872
  throw err;
513
873
  }
514
874
  },
@@ -518,9 +878,9 @@ function createOrdersStore(provider) {
518
878
  const query = get().orderQuery;
519
879
  const result = await provider.getOrders(query);
520
880
  set({ orders: result.data, ordersTotal: result.total });
521
- toast.success("Item added");
881
+ toast.success("Item adicionado");
522
882
  } catch (err) {
523
- toast.error("Failed to add item", { description: err?.message });
883
+ toast.error("N\xE3o foi poss\xEDvel adicionar o item", { description: err?.message });
524
884
  throw err;
525
885
  }
526
886
  },
@@ -530,9 +890,9 @@ function createOrdersStore(provider) {
530
890
  const query = get().orderQuery;
531
891
  const result = await provider.getOrders(query);
532
892
  set({ orders: result.data, ordersTotal: result.total });
533
- toast.success("Item removed");
893
+ toast.success("Item removido");
534
894
  } catch (err) {
535
- toast.error("Failed to remove item", { description: err?.message });
895
+ toast.error("N\xE3o foi poss\xEDvel remover o item", { description: err?.message });
536
896
  throw err;
537
897
  }
538
898
  }
@@ -572,22 +932,47 @@ var ordersRegistries = [
572
932
  // src/locales/en.ts
573
933
  var en = {
574
934
  "orders.title": "Orders",
935
+ "orders.detail.addItem": "Add item",
936
+ "orders.detail.searchMenu": "Search the menu\u2026",
937
+ "orders.detail.cancel": "Cancel order",
938
+ "orders.detail.cancelConfirm": "Cancel {reference}? This cannot be undone.",
575
939
  "orders.nav.kanban": "Orders",
576
940
  "orders.nav.list": "History",
577
941
  "orders.nav.new": "New Order",
578
942
  "orders.nav.dashboard": "Dashboard",
943
+ "orders.list.column.order": "Order",
944
+ "orders.list.column.customer": "Customer",
945
+ "orders.list.column.total": "Total",
946
+ "orders.list.column.status": "Status",
947
+ "orders.list.searchPlaceholder": "Search by number, customer or table\u2026",
579
948
  "orders.kanban.title": "Orders",
580
949
  "orders.kanban.subtitle": "Active orders",
581
950
  "orders.kanban.noOrders": "No active orders",
582
951
  "orders.kanban.noOrdersDesc": "Orders will appear here as they come in",
583
952
  "orders.kanban.newOrder": "New Order",
584
953
  "orders.kanban.advance": "Advance",
954
+ // O verbo de cada passo, chaveado pelo estado ALVO. E o que o botao do
955
+ // cartao diz: quem esta no salao nao "avanca" um pedido, ele confirma,
956
+ // prepara, serve ou despacha.
957
+ "orders.advance.draft": "Save draft",
958
+ "orders.advance.new": "Send",
959
+ "orders.advance.confirmed": "Confirm",
960
+ "orders.advance.preparing": "Start prep",
961
+ "orders.advance.ready": "Mark ready",
962
+ "orders.advance.served": "Serve",
963
+ "orders.advance.picked_up": "Hand over",
964
+ "orders.advance.dispatched": "Dispatch",
965
+ "orders.advance.delivered": "Confirm delivery",
966
+ "orders.advance.completed": "Complete",
967
+ "orders.advance.cancelled": "Cancel",
585
968
  "orders.kanban.cancel": "Cancel Order",
586
969
  "orders.kanban.column.new": "New",
587
970
  "orders.kanban.column.confirmed": "Confirmed",
971
+ "orders.kanban.column.open": "Open",
588
972
  "orders.kanban.column.preparing": "Preparing",
589
973
  "orders.kanban.column.ready": "Ready",
590
974
  "orders.kanban.column.out": "Out",
975
+ "orders.kanban.column.served": "Served",
591
976
  "orders.kanban.column.completed": "Completed",
592
977
  "orders.kanban.column.cancelled": "Cancelled",
593
978
  "orders.detail.title": "Order Details",
@@ -627,22 +1012,47 @@ var en = {
627
1012
  // src/locales/pt-BR.ts
628
1013
  var ptBR = {
629
1014
  "orders.title": "Pedidos",
1015
+ "orders.detail.addItem": "Lan\xE7ar item",
1016
+ "orders.detail.searchMenu": "Buscar no card\xE1pio\u2026",
1017
+ "orders.detail.cancel": "Cancelar pedido",
1018
+ "orders.detail.cancelConfirm": "Cancelar {reference}? N\xE3o d\xE1 para desfazer.",
630
1019
  "orders.nav.kanban": "Pedidos",
631
1020
  "orders.nav.list": "Hist\xF3rico",
632
1021
  "orders.nav.new": "Novo Pedido",
633
1022
  "orders.nav.dashboard": "Painel",
1023
+ "orders.list.column.order": "Pedido",
1024
+ "orders.list.column.customer": "Cliente",
1025
+ "orders.list.column.total": "Total",
1026
+ "orders.list.column.status": "Estado",
1027
+ "orders.list.searchPlaceholder": "Buscar por n\xFAmero, cliente ou mesa\u2026",
634
1028
  "orders.kanban.title": "Pedidos",
635
1029
  "orders.kanban.subtitle": "Pedidos ativos",
636
1030
  "orders.kanban.noOrders": "Nenhum pedido ativo",
637
1031
  "orders.kanban.noOrdersDesc": "Os pedidos aparecer\xE3o aqui conforme chegarem",
638
1032
  "orders.kanban.newOrder": "Novo Pedido",
639
1033
  "orders.kanban.advance": "Avan\xE7ar",
1034
+ // O verbo de cada passo, chaveado pelo estado ALVO. E o que o botao do
1035
+ // cartao diz: quem esta no salao nao "avanca" um pedido, ele confirma,
1036
+ // prepara, serve ou despacha.
1037
+ "orders.advance.draft": "Salvar rascunho",
1038
+ "orders.advance.new": "Enviar",
1039
+ "orders.advance.confirmed": "Confirmar",
1040
+ "orders.advance.preparing": "Preparar",
1041
+ "orders.advance.ready": "Marcar pronto",
1042
+ "orders.advance.served": "Servir",
1043
+ "orders.advance.picked_up": "Entregar",
1044
+ "orders.advance.dispatched": "Despachar",
1045
+ "orders.advance.delivered": "Confirmar entrega",
1046
+ "orders.advance.completed": "Concluir",
1047
+ "orders.advance.cancelled": "Cancelar",
640
1048
  "orders.kanban.cancel": "Cancelar Pedido",
641
1049
  "orders.kanban.column.new": "Novo",
642
1050
  "orders.kanban.column.confirmed": "Confirmado",
1051
+ "orders.kanban.column.open": "Aberta",
643
1052
  "orders.kanban.column.preparing": "Preparando",
644
1053
  "orders.kanban.column.ready": "Pronto",
645
1054
  "orders.kanban.column.out": "Saiu",
1055
+ "orders.kanban.column.served": "Servido",
646
1056
  "orders.kanban.column.completed": "Conclu\xEDdo",
647
1057
  "orders.kanban.column.cancelled": "Cancelado",
648
1058
  "orders.detail.title": "Detalhes do Pedido",
@@ -1128,10 +1538,88 @@ var aiqfomeConnectorDef = {
1128
1538
  disconnect: () => store6.disconnect(),
1129
1539
  disconnectNote: "Desconectar para de trazer pedidos novos. Os pedidos que j\xE1 entraram continuam aqui."
1130
1540
  };
1541
+ function toModalInstallment(bill) {
1542
+ return {
1543
+ id: bill.id,
1544
+ amount: Number(bill.amount ?? 0),
1545
+ paidAmount: Number(bill.paid_amount ?? 0),
1546
+ installmentNumber: bill.installment_number ?? 1,
1547
+ direction: bill.direction ?? "credit",
1548
+ dueDate: bill.due_date,
1549
+ invoiceId: bill.financial_invoice_id ?? bill.invoice_id
1550
+ };
1551
+ }
1552
+ function firstOpenBill(receivable) {
1553
+ return receivable?.bills.find(
1554
+ (b) => Number(b.paid_amount ?? 0) < Number(b.amount)
1555
+ );
1556
+ }
1557
+ function OrderReceivableActions({ orderId, orderTotal, receivable, reload }) {
1558
+ const [busy, setBusy] = React2.useState(false);
1559
+ const [paying, setPaying] = React2.useState(null);
1560
+ const can = usePermissionOptional();
1561
+ const runtime = usePluginRuntimeOptional();
1562
+ const hasFinancialRoute = (runtime?.activePlugins ?? []).some(
1563
+ (p) => p.routes?.some((r) => r.path === "/financial")
1564
+ );
1565
+ const canInvoice = can("financial", "create");
1566
+ const balance = receivable ? Number(receivable.balance.balance ?? 0) : 0;
1567
+ const cancelled = receivable?.balance.status === "cancelled";
1568
+ const openBill = firstOpenBill(receivable);
1569
+ const canReceive = receivable ? balance > 0 && !cancelled && Boolean(openBill) : canInvoice && Number(orderTotal) > 0;
1570
+ async function registrarRecebimento() {
1571
+ if (receivable) {
1572
+ const bill = firstOpenBill(receivable);
1573
+ if (bill) setPaying(toModalInstallment(bill));
1574
+ return;
1575
+ }
1576
+ const db = getSupabaseClientOptional();
1577
+ if (!db) return;
1578
+ setBusy(true);
1579
+ try {
1580
+ const { error } = await db.rpc("fn_invoice_from_order", { p_order_id: orderId });
1581
+ if (error) throw new Error(error.message ?? "Falha ao faturar o pedido");
1582
+ const { data } = await db.from("v_financial_bills").select("*").eq("invoice_id", orderId).order("installment_number", { ascending: true });
1583
+ const bill = (data ?? []).find(
1584
+ (b) => Number(b.paid_amount ?? 0) < Number(b.amount ?? 0)
1585
+ );
1586
+ await reload(true);
1587
+ if (bill) setPaying(toModalInstallment(bill));
1588
+ else toast$1.error("Pedido faturado, mas nenhuma parcela em aberto foi encontrada");
1589
+ } catch (err) {
1590
+ toast$1.error(err instanceof Error ? err.message : "Falha ao faturar o pedido");
1591
+ } finally {
1592
+ setBusy(false);
1593
+ }
1594
+ }
1595
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1596
+ canReceive && /* @__PURE__ */ jsx(Button, { onClick: () => void registrarRecebimento(), disabled: busy, "data-testid": "order-register-receipt", children: busy ? "Abrindo\u2026" : "Registrar recebimento" }),
1597
+ hasFinancialRoute && /* @__PURE__ */ jsx(
1598
+ "button",
1599
+ {
1600
+ type: "button",
1601
+ onClick: () => adminNavigateTo("/financial"),
1602
+ className: "rounded-md border px-3 py-1.5 text-sm font-medium transition hover:bg-muted",
1603
+ children: "Abrir no Financeiro"
1604
+ }
1605
+ ),
1606
+ paying != null && /* @__PURE__ */ jsx(
1607
+ ReceivePaymentModal,
1608
+ {
1609
+ installment: paying,
1610
+ onClose: () => setPaying(null),
1611
+ onPaid: () => {
1612
+ setPaying(null);
1613
+ void reload(true);
1614
+ }
1615
+ }
1616
+ )
1617
+ ] });
1618
+ }
1131
1619
  var DEFAULT_ORDERS_TABLE = "orders";
1132
- var DEFAULT_ORDER_ITEMS_TABLE = "order_items";
1620
+ var DEFAULT_ORDER_ITEMS_TABLE = "items";
1133
1621
  var ORDERS_METADATA_KEY = "fayzOrders";
1134
- var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["new", "confirmed", "preparing", "ready", "served", "dispatched"]);
1622
+ var ACTIVE_STATUSES2 = /* @__PURE__ */ new Set(["new", "confirmed", "preparing", "ready", "served", "dispatched"]);
1135
1623
  var RESTAURANT_STATUSES = /* @__PURE__ */ new Set([
1136
1624
  "new",
1137
1625
  "confirmed",
@@ -1373,7 +1861,7 @@ function summarize(orders, dateRange) {
1373
1861
  todayOrderCount: inRange.length,
1374
1862
  todayRevenue: revenue,
1375
1863
  averageTicket: billable.length > 0 ? revenue / billable.length : 0,
1376
- activeOrderCount: orders.filter((order) => ACTIVE_STATUSES.has(order.status)).length,
1864
+ activeOrderCount: orders.filter((order) => ACTIVE_STATUSES2.has(order.status)).length,
1377
1865
  ordersByKind,
1378
1866
  ordersByStatus,
1379
1867
  topItems: [...top.values()].sort((a, b) => b.quantity - a.quantity).slice(0, 5)
@@ -1557,7 +2045,7 @@ function createFayzOrdersProvider(options = {}) {
1557
2045
  }
1558
2046
 
1559
2047
  // src/index.ts
1560
- var OrdersPage = React.lazy(() => import('./OrdersPage-CQ7O74ZW.js').then((m) => ({ default: m.OrdersPage })));
2048
+ var OrdersPage = React2__default.lazy(() => import('./OrdersPage-IKIIQALX.js').then((m) => ({ default: m.OrdersPage })));
1561
2049
  var DEFAULT_LABELS = {
1562
2050
  pageTitle: "Orders",
1563
2051
  kanban: "Orders",
@@ -1580,18 +2068,58 @@ function resolveConfig(options) {
1580
2068
  orderSources: options?.orderSources ?? DEFAULT_ORDER_SOURCES,
1581
2069
  menuItemLookup: options?.menuItemLookup,
1582
2070
  staffLookup: options?.staffLookup,
1583
- onOrderCompleted: options?.onOrderCompleted
2071
+ onOrderCompleted: options?.onOrderCompleted,
2072
+ onOrderCancelled: options?.onOrderCancelled,
2073
+ createRoute: options?.createRoute,
2074
+ editRoute: options?.editRoute,
2075
+ canEditOrder: options?.canEditOrder,
2076
+ orderRoute: options?.orderRoute
1584
2077
  };
1585
2078
  }
1586
2079
  function createOrdersPlugin(options) {
1587
2080
  const config = resolveConfig(options);
1588
- const provider = options?.dataProvider ?? createMockOrdersProvider();
1589
- const store7 = createOrdersStore(provider);
1590
- const PageComponent = () => React.createElement(
1591
- React.Suspense,
2081
+ const provider = options?.dataProvider ?? (options?.variant === "commercial" ? createCoreOrdersProvider() : createMockOrdersProvider());
2082
+ const store7 = createOrdersStore(provider, {
2083
+ onOrderCompleted: config.onOrderCompleted,
2084
+ onOrderCancelled: config.onOrderCancelled
2085
+ });
2086
+ const PageComponent = () => React2__default.createElement(
2087
+ React2__default.Suspense,
1592
2088
  { fallback: null },
1593
- React.createElement(OrdersPage, { config, provider, store: store7, registries: ordersRegistries })
2089
+ React2__default.createElement(OrdersPage, { config, provider, store: store7, registries: ordersRegistries, variant: options?.variant ?? "restaurant" })
1594
2090
  );
2091
+ const OrderDetailRoute = ({ id }) => id ? React2__default.createElement(OrderDetailPage, {
2092
+ orderId: id,
2093
+ backTo: "/orders",
2094
+ // The header had no verbs at all — not just no "Editar". The page
2095
+ // always had the slot; nobody was filling it.
2096
+ actions: (order) => {
2097
+ const edit = options?.editRoute;
2098
+ if (!edit) return null;
2099
+ const allowed = options?.canEditOrder ?? orderIsEditable;
2100
+ if (!allowed(order)) return null;
2101
+ return React2__default.createElement(PageActions, {
2102
+ actions: [{
2103
+ id: "edit",
2104
+ testId: "order-edit",
2105
+ label: "Editar pedido",
2106
+ icon: Pencil,
2107
+ intent: "primary",
2108
+ onSelect: () => adminNavigateTo(edit(order.id))
2109
+ }]
2110
+ });
2111
+ },
2112
+ // Os verbos do dinheiro. O seam existia desde a primeira passada do
2113
+ // archetype e nunca foi preenchido fora da loja — entao a tela sabia
2114
+ // dizer "sem conta a receber" e nao oferecia como criar uma. Ver
2115
+ // ./views/OrderReceivableActions.
2116
+ receivableActions: (order, ctx) => React2__default.createElement(OrderReceivableActions, {
2117
+ orderId: order.id,
2118
+ orderTotal: order.total ?? 0,
2119
+ receivable: ctx.receivable,
2120
+ reload: ctx.reload
2121
+ })
2122
+ }) : null;
1595
2123
  return {
1596
2124
  id: "orders",
1597
2125
  defaultAgentRole: "operations",
@@ -1602,6 +2130,40 @@ function createOrdersPlugin(options) {
1602
2130
  verticalId: options?.verticalId,
1603
2131
  defaultEnabled: true,
1604
2132
  dependencies: ["menu"],
2133
+ events: [
2134
+ {
2135
+ name: "orders.order_completed",
2136
+ title: "Order completed",
2137
+ trigger: "When an order reaches the completed status.",
2138
+ payloadSchema: {
2139
+ type: "object",
2140
+ properties: {
2141
+ order_id: { type: "string" },
2142
+ reference_number: { type: "string" },
2143
+ total: { type: "number" },
2144
+ item_count: { type: "number" },
2145
+ source: { type: "string" }
2146
+ },
2147
+ required: ["order_id", "reference_number", "total", "item_count", "source"]
2148
+ }
2149
+ },
2150
+ {
2151
+ name: "orders.order_cancelled",
2152
+ title: "Order cancelled",
2153
+ trigger: "When an order reaches the cancelled status.",
2154
+ payloadSchema: {
2155
+ type: "object",
2156
+ properties: {
2157
+ order_id: { type: "string" },
2158
+ reference_number: { type: "string" },
2159
+ total: { type: "number" },
2160
+ item_count: { type: "number" },
2161
+ source: { type: "string" }
2162
+ },
2163
+ required: ["order_id", "reference_number", "total", "item_count", "source"]
2164
+ }
2165
+ }
2166
+ ],
1605
2167
  // Orders/month quota. NOTE: order creation currently flows only through the
1606
2168
  // store/AI-tool (createOrder) with no dedicated UI create handler, so no
1607
2169
  // client-side guard is wired yet — the declaration keeps the cap countable.
@@ -1623,6 +2185,14 @@ function createOrdersPlugin(options) {
1623
2185
  path: "/orders",
1624
2186
  component: PageComponent,
1625
2187
  permission: { feature: "orders", action: "read" }
2188
+ },
2189
+ {
2190
+ // The page the rail's "Ver pedido completo" points at. It used to exist
2191
+ // only in plugin-shop, so the restaurant's link opened a blank screen —
2192
+ // the route was never mounted, not the data missing.
2193
+ path: "/orders/:id",
2194
+ component: OrderDetailRoute,
2195
+ permission: { feature: "orders", action: "read" }
1626
2196
  }
1627
2197
  ],
1628
2198
  widgets: [],
@@ -1692,7 +2262,7 @@ function createOrdersPlugin(options) {
1692
2262
  label: config.labels.pageTitle,
1693
2263
  icon: "ClipboardList",
1694
2264
  component: (() => {
1695
- const OrdersSettingsTab = () => React.createElement(PluginSettingsPanel, {
2265
+ const OrdersSettingsTab = () => React2__default.createElement(PluginSettingsPanel, {
1696
2266
  title: "Orders Settings",
1697
2267
  registries: ordersRegistries,
1698
2268
  routeBase: "/settings/orders"
@@ -1718,6 +2288,6 @@ function createOrdersPlugin(options) {
1718
2288
  };
1719
2289
  }
1720
2290
 
1721
- export { AIQFOME_CONNECTOR_ID, AIQFOME_SETTINGS, AUTO_ACCEPT_SETTING, IFOOD_CONNECTOR_ID, IFOOD_SETTINGS, KEETA_CONNECTOR_ID, KEETA_SETTINGS, NINETY_NINE_FOOD_CONNECTOR_ID, NINETY_NINE_FOOD_SETTINGS, PREP_TIME_SETTING, RAPPI_CONNECTOR_ID, RAPPI_SETTINGS, UBER_EATS_CONNECTOR_ID, UBER_EATS_SETTINGS, aiqfomeConnectorDef, createFayzOrdersProvider, createOrdersPlugin, ifoodConnectorDef, keetaConnectorDef, marketplacePermissions, nineNineFoodConnectorDef, rappiConnectorDef, uberEatsConnectorDef };
2291
+ export { AIQFOME_CONNECTOR_ID, AIQFOME_SETTINGS, AUTO_ACCEPT_SETTING, IFOOD_CONNECTOR_ID, IFOOD_SETTINGS, KEETA_CONNECTOR_ID, KEETA_SETTINGS, NINETY_NINE_FOOD_CONNECTOR_ID, NINETY_NINE_FOOD_SETTINGS, PREP_TIME_SETTING, RAPPI_CONNECTOR_ID, RAPPI_SETTINGS, UBER_EATS_CONNECTOR_ID, UBER_EATS_SETTINGS, aiqfomeConnectorDef, createCoreOrdersProvider, createFayzOrdersProvider, createOrdersPlugin, ifoodConnectorDef, keetaConnectorDef, marketplacePermissions, nineNineFoodConnectorDef, rappiConnectorDef, uberEatsConnectorDef };
1722
2292
  //# sourceMappingURL=index.js.map
1723
2293
  //# sourceMappingURL=index.js.map