@fayz-ai/plugin-getting-started 0.0.0-stage → 0.2.0-canary.20261027153628
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/LICENSE +21 -0
- package/dist/chunk-PP3YVZGM.js +2960 -0
- package/dist/chunk-PP3YVZGM.js.map +1 -0
- package/dist/components/ProgressRing.d.ts +16 -0
- package/dist/components/ProgressRing.d.ts.map +1 -0
- package/dist/components/StartTrialAction.d.ts +70 -0
- package/dist/components/StartTrialAction.d.ts.map +1 -0
- package/dist/components/StartTrialCta.d.ts +12 -0
- package/dist/components/StartTrialCta.d.ts.map +1 -0
- package/dist/components/StoreLiveCard.d.ts +21 -0
- package/dist/components/StoreLiveCard.d.ts.map +1 -0
- package/dist/components/TrialEndBanner.d.ts +12 -0
- package/dist/components/TrialEndBanner.d.ts.map +1 -0
- package/dist/config.d.ts +82 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +27 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +10 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/tables.d.ts +15 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/types.d.ts +24 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/host.d.ts +93 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +78 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agent-tools.d.ts +143 -0
- package/dist/lib/agent-tools.d.ts.map +1 -0
- package/dist/lib/journey-steps.d.ts +152 -0
- package/dist/lib/journey-steps.d.ts.map +1 -0
- package/dist/lib/program.d.ts +37 -0
- package/dist/lib/program.d.ts.map +1 -0
- package/dist/lib/store-profile.d.ts +75 -0
- package/dist/lib/store-profile.d.ts.map +1 -0
- package/dist/lib/store-settings.d.ts +70 -0
- package/dist/lib/store-settings.d.ts.map +1 -0
- package/dist/lib/tour.d.ts +86 -0
- package/dist/lib/tour.d.ts.map +1 -0
- package/dist/lib/trial-clock.d.ts +14 -0
- package/dist/lib/trial-clock.d.ts.map +1 -0
- package/dist/lib/trial-source-bridge.d.ts +17 -0
- package/dist/lib/trial-source-bridge.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +10 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/main.browser.js +11 -0
- package/dist/migrations/main.browser.js.map +1 -0
- package/dist/migrations/main.js +992 -0
- package/dist/migrations/main.js.map +1 -0
- package/dist/settings/TrialSettingsTab.d.ts +23 -0
- package/dist/settings/TrialSettingsTab.d.ts.map +1 -0
- package/dist/store.d.ts +23 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +74 -3
- package/README.md +0 -3
|
@@ -0,0 +1,2960 @@
|
|
|
1
|
+
import { HOURS_FORMAT_PT_BR, PAYMENT_METHOD_LABELS_PT_BR, DAY_KEYS, PAYMENT_METHOD_IDS, useTourStore, cn, toast, Button, BottomSheet, Sheet, SheetContent, SheetHeader, SheetTitle, ConfirmDialog, PhoneField, useOnboardingProgress, Card, CardHeader, CardContent, HoursField, Checkbox, Input, normalizePaymentMethods, fireConfetti, registerTourAction, normalizeHours, isHoursValue, formatHours, openDays, formatShifts } from '@fayz-ai/ui';
|
|
2
|
+
export { PAYMENT_METHOD_IDS } from '@fayz-ai/ui';
|
|
3
|
+
import * as React5 from 'react';
|
|
4
|
+
import { emitDomainEvent, registerTenantScopedStore, journeySteps, tl, onDomainEvent, getActiveTenantId, usePluginRuntimeOptional, getSupabaseClientOptional, getCurrentLocale, useTranslation } from '@fayz-ai/core';
|
|
5
|
+
import { create } from 'zustand';
|
|
6
|
+
import { createPluginContext, useActivationStore, useOrganizationStore, useBusinessProfileStore, unregisterAIToolHandler, registerAIToolHandler, useSdkPermission, SDK_PERMISSIONS, adminNavigateTo, useClaimOnboardingTasks, closeRightRailOnPhone, OnboardingStepRow, startGuidedRun, SettingsGroup, hasAIToolHandler, setTrialSource, useRightRailStore } from '@fayz-ai/admin/host';
|
|
7
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
import { Check, ChevronRight, ChevronDown, Rocket, Clock, X, Lock, AlertTriangle, Compass, WandSparkles, Loader2, Crown, Circle } from 'lucide-react';
|
|
9
|
+
import { createPortal } from 'react-dom';
|
|
10
|
+
|
|
11
|
+
// src/config.ts
|
|
12
|
+
var DEFAULT_TRIAL_DAYS = 14;
|
|
13
|
+
var TRIAL_ROUTE = "/settings/trial";
|
|
14
|
+
var SUBSCRIPTION_ROUTE = "/settings/subscription";
|
|
15
|
+
var HOME_ROUTE = "/";
|
|
16
|
+
var DEFAULT_ROUTES = Object.freeze({
|
|
17
|
+
home: HOME_ROUTE,
|
|
18
|
+
trial: TRIAL_ROUTE,
|
|
19
|
+
subscription: SUBSCRIPTION_ROUTE
|
|
20
|
+
});
|
|
21
|
+
var route = (value, fallback) => typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
22
|
+
function resolveRoutes(routes) {
|
|
23
|
+
return {
|
|
24
|
+
home: route(routes?.home, DEFAULT_ROUTES.home),
|
|
25
|
+
trial: route(routes?.trial, DEFAULT_ROUTES.trial),
|
|
26
|
+
subscription: route(routes?.subscription, DEFAULT_ROUTES.subscription)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
var keys = (list) => [...new Set((list ?? []).filter((k) => typeof k === "string" && k.trim().length > 0))];
|
|
30
|
+
function resolveJourneyOverride(journey) {
|
|
31
|
+
return {
|
|
32
|
+
launch: keys(journey?.launch),
|
|
33
|
+
later: keys(journey?.later),
|
|
34
|
+
extra: keys(journey?.extra),
|
|
35
|
+
minimums: keys(journey?.minimums)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function resolveConfig(options) {
|
|
39
|
+
const days = Number(options?.trialDays);
|
|
40
|
+
const routes = resolveRoutes(options?.routes);
|
|
41
|
+
return {
|
|
42
|
+
trialDays: Number.isFinite(days) && days > 0 ? Math.floor(days) : DEFAULT_TRIAL_DAYS,
|
|
43
|
+
verticals: options?.verticals?.length ? [...options.verticals] : ["food"],
|
|
44
|
+
journey: resolveJourneyOverride(options?.journey),
|
|
45
|
+
routes,
|
|
46
|
+
trialRoute: routes.trial,
|
|
47
|
+
subscriptionRoute: routes.subscription
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
var activeConfig = null;
|
|
51
|
+
function setActiveConfig(config) {
|
|
52
|
+
activeConfig = config;
|
|
53
|
+
}
|
|
54
|
+
function getActiveConfig() {
|
|
55
|
+
return activeConfig ?? resolveConfig();
|
|
56
|
+
}
|
|
57
|
+
var divergenceNoted = null;
|
|
58
|
+
function noteTrialDaysDivergence(config, poolTrialDays) {
|
|
59
|
+
if (!poolTrialDays || poolTrialDays === config.trialDays) return;
|
|
60
|
+
const signature = `${config.trialDays}:${poolTrialDays}`;
|
|
61
|
+
if (divergenceNoted === signature) return;
|
|
62
|
+
divergenceNoted = signature;
|
|
63
|
+
if (typeof console !== "undefined") {
|
|
64
|
+
console.warn(
|
|
65
|
+
`[getting-started] trialDays: the app says ${config.trialDays}, plg_getting_started_config.trial_days says ${poolTrialDays}. The pool wins; align billing.trialDays.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
|
|
70
|
+
var MINIMUM_IDS = ["menu", "hours", "testOrder"];
|
|
71
|
+
var SLOT_BY_SUFFIX = { hours: "hours", menu: "menu", "test-order": "testOrder" };
|
|
72
|
+
function minimumSlotOf(key2) {
|
|
73
|
+
const suffix = key2.slice(key2.lastIndexOf(".") + 1);
|
|
74
|
+
return SLOT_BY_SUFFIX[suffix];
|
|
75
|
+
}
|
|
76
|
+
var unique = (list) => [...new Set(list)];
|
|
77
|
+
function declareJourney(programs, override) {
|
|
78
|
+
const tagged = journeySteps(programs);
|
|
79
|
+
const extra = resolveJourneyOverride(override);
|
|
80
|
+
const launch = unique([...tagged.filter((s) => s.group === "launch").map((s) => s.key), ...extra.launch]);
|
|
81
|
+
const later = unique([...tagged.filter((s) => s.group === "later").map((s) => s.key), ...extra.later]);
|
|
82
|
+
const extraKeys = unique([...tagged.filter((s) => s.group === "extra").map((s) => s.key), ...extra.extra]);
|
|
83
|
+
const minimums = unique([...tagged.filter((s) => s.minimum).map((s) => s.key), ...extra.minimums]);
|
|
84
|
+
const slots = {};
|
|
85
|
+
for (const key2 of minimums) {
|
|
86
|
+
const slot = minimumSlotOf(key2);
|
|
87
|
+
if (slot && !slots[slot]) slots[slot] = key2;
|
|
88
|
+
}
|
|
89
|
+
return { launch, later, extra: extraKeys, minimums, slots, declared: tagged.length > 0 };
|
|
90
|
+
}
|
|
91
|
+
var EMPTY_PROGRAMS = [];
|
|
92
|
+
var activeOverride;
|
|
93
|
+
var activePrograms = EMPTY_PROGRAMS;
|
|
94
|
+
var active = declareJourney(EMPTY_PROGRAMS);
|
|
95
|
+
function setJourneyOverride(override) {
|
|
96
|
+
activeOverride = override;
|
|
97
|
+
active = declareJourney(activePrograms, activeOverride);
|
|
98
|
+
return active;
|
|
99
|
+
}
|
|
100
|
+
function syncJourneyPrograms(programs) {
|
|
101
|
+
const next = programs ?? EMPTY_PROGRAMS;
|
|
102
|
+
if (next !== activePrograms) {
|
|
103
|
+
activePrograms = next;
|
|
104
|
+
active = declareJourney(activePrograms, activeOverride);
|
|
105
|
+
}
|
|
106
|
+
return active;
|
|
107
|
+
}
|
|
108
|
+
function currentJourney() {
|
|
109
|
+
return active;
|
|
110
|
+
}
|
|
111
|
+
var hostPrograms;
|
|
112
|
+
function setHostJourneyPrograms(programs) {
|
|
113
|
+
hostPrograms = programs;
|
|
114
|
+
return syncJourneyPrograms(programs);
|
|
115
|
+
}
|
|
116
|
+
function useJourneyDeclaration() {
|
|
117
|
+
const runtime = usePluginRuntimeOptional();
|
|
118
|
+
const programs = runtime?.onboardingPrograms ?? hostPrograms;
|
|
119
|
+
return React5.useMemo(() => syncJourneyPrograms(programs), [programs]);
|
|
120
|
+
}
|
|
121
|
+
var FALLBACK_SIZE = 9;
|
|
122
|
+
var FALLBACK_LAUNCH = 6;
|
|
123
|
+
function selectJourneySteps(catalog, declaration = currentJourney()) {
|
|
124
|
+
const byKey = new Map(catalog.map((s) => [s.key, s]));
|
|
125
|
+
const pick = (keys2) => keys2.map((k) => byKey.get(k)).filter((s) => !!s);
|
|
126
|
+
const launch = pick(declaration.launch);
|
|
127
|
+
const later = pick(declaration.later);
|
|
128
|
+
const extra = pick(declaration.extra).slice(0, 1);
|
|
129
|
+
if (launch.length > 0) return { launch, later, extra, fallback: false };
|
|
130
|
+
const head = catalog.slice(0, FALLBACK_SIZE);
|
|
131
|
+
return { launch: head.slice(0, FALLBACK_LAUNCH), later: head.slice(FALLBACK_LAUNCH), extra, fallback: true };
|
|
132
|
+
}
|
|
133
|
+
function rowDone(rows, key2) {
|
|
134
|
+
return !!rows?.some((r) => r.sourceKey === key2 && (r.status === "done" || r.status === "cancelled"));
|
|
135
|
+
}
|
|
136
|
+
var SENSOR_RULED_KEYS = ["getting-started.hours", "getting-started.payment-methods"];
|
|
137
|
+
function toStepInputs(steps, rows, onSensorDone, onSensorOpen) {
|
|
138
|
+
return steps.map((step) => {
|
|
139
|
+
const closed = rowDone(rows, step.key);
|
|
140
|
+
const ruled = !!step.check && SENSOR_RULED_KEYS.includes(step.key);
|
|
141
|
+
const trustRow = closed && !ruled;
|
|
142
|
+
return {
|
|
143
|
+
id: step.key,
|
|
144
|
+
title: step.title,
|
|
145
|
+
description: step.description,
|
|
146
|
+
icon: step.icon,
|
|
147
|
+
action: step.route,
|
|
148
|
+
done: trustRow ? true : void 0,
|
|
149
|
+
check: trustRow || !step.check ? void 0 : async () => {
|
|
150
|
+
const ok = await step.check();
|
|
151
|
+
if (ok) onSensorDone?.(step.key);
|
|
152
|
+
else if (closed) onSensorOpen?.(step.key);
|
|
153
|
+
return ok;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function countJourney(catalog, rows, declaration = currentJourney()) {
|
|
159
|
+
const { launch, later } = selectJourneySteps(catalog, declaration);
|
|
160
|
+
const counted = [...launch, ...later];
|
|
161
|
+
return { done: counted.filter((s) => rowDone(rows, s.key)).length, total: counted.length };
|
|
162
|
+
}
|
|
163
|
+
function countMinimums(journey, rows, declaration = currentJourney()) {
|
|
164
|
+
const done = MINIMUM_IDS.filter((id) => {
|
|
165
|
+
if (journey?.minimums[id]) return true;
|
|
166
|
+
const key2 = declaration.slots[id];
|
|
167
|
+
return !!key2 && rowDone(rows, key2);
|
|
168
|
+
});
|
|
169
|
+
const missing = MINIMUM_IDS.filter((id) => !done.includes(id));
|
|
170
|
+
const slotted = new Set(Object.values(declaration.slots));
|
|
171
|
+
const missingKeys = declaration.minimums.filter((key2) => {
|
|
172
|
+
const slot = minimumSlotOf(key2);
|
|
173
|
+
if (slot && slotted.has(key2)) return missing.includes(slot);
|
|
174
|
+
return !rowDone(rows, key2);
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
count: done.length,
|
|
178
|
+
total: MINIMUM_IDS.length,
|
|
179
|
+
done,
|
|
180
|
+
missing,
|
|
181
|
+
missingKeys,
|
|
182
|
+
ready: missing.length === 0 && missingKeys.length === 0
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function minimumsFromRows(rows, declaration = currentJourney()) {
|
|
186
|
+
const { done, ready } = countMinimums(null, rows, declaration);
|
|
187
|
+
return {
|
|
188
|
+
menu: done.includes("menu"),
|
|
189
|
+
hours: done.includes("hours"),
|
|
190
|
+
testOrder: done.includes("testOrder"),
|
|
191
|
+
ready
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function clientJourney(input) {
|
|
195
|
+
return {
|
|
196
|
+
tenantId: input.tenantId,
|
|
197
|
+
phase: "setup",
|
|
198
|
+
trialDays: input.trialDays,
|
|
199
|
+
readyAt: null,
|
|
200
|
+
trialStartedAt: null,
|
|
201
|
+
trialEndsAt: null,
|
|
202
|
+
setupDeadlineAt: null,
|
|
203
|
+
convertedAt: null,
|
|
204
|
+
wantsContractAt: null,
|
|
205
|
+
source: "client",
|
|
206
|
+
utm: {},
|
|
207
|
+
minimums: minimumsFromRows(input.rows, input.declaration ?? currentJourney())
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function nowStepKey(selection, completed) {
|
|
211
|
+
return [...selection.launch, ...selection.later].find((s) => !completed[s.key])?.key;
|
|
212
|
+
}
|
|
213
|
+
function minimumLines(catalog, rows, journey, declaration = currentJourney()) {
|
|
214
|
+
const byKey = new Map(catalog.map((s) => [s.key, s]));
|
|
215
|
+
const counted = countMinimums(journey, rows, declaration);
|
|
216
|
+
const lines = declaration.minimums.map((key2) => {
|
|
217
|
+
const inferred = minimumSlotOf(key2);
|
|
218
|
+
const slot = inferred && declaration.slots[inferred] === key2 ? inferred : void 0;
|
|
219
|
+
const done = slot ? counted.done.includes(slot) : !counted.missingKeys.includes(key2);
|
|
220
|
+
return { key: key2, slot, step: byKey.get(key2), done };
|
|
221
|
+
});
|
|
222
|
+
for (const slot of MINIMUM_IDS) {
|
|
223
|
+
if (declaration.slots[slot]) continue;
|
|
224
|
+
lines.push({ key: `minimum:${slot}`, slot, done: counted.done.includes(slot) });
|
|
225
|
+
}
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
var JOURNEY_TOUR_ID = "getting-started.journey";
|
|
229
|
+
var CARD_ANCHOR = "getting-started.card";
|
|
230
|
+
var TRIAL_TAB_ANCHOR = "getting-started.trial-tab";
|
|
231
|
+
var DESCRIBE_CARD_ACTION = "getting-started.describe-card";
|
|
232
|
+
var DESCRIBE_TRIAL_TAB_ACTION = "getting-started.describe-trial-tab";
|
|
233
|
+
var TRIAL_STARTED_EVENT = "getting_started.trial_started";
|
|
234
|
+
var MINIMUMS_READY_EVENT = "getting_started.minimums_ready";
|
|
235
|
+
var TOUR_DONE_EVENT = "getting_started.tour_done";
|
|
236
|
+
var TRIAL_START_SETTLE_MS = 600;
|
|
237
|
+
var OWN_PLUGIN_ID = "getting-started";
|
|
238
|
+
function buildOwnJourneyTour(routes) {
|
|
239
|
+
return {
|
|
240
|
+
id: OWN_PLUGIN_ID,
|
|
241
|
+
order: 0,
|
|
242
|
+
when: "trial_started",
|
|
243
|
+
stops: [
|
|
244
|
+
{
|
|
245
|
+
anchor: CARD_ANCHOR,
|
|
246
|
+
route: routes?.home ?? DEFAULT_ROUTES.home,
|
|
247
|
+
action: DESCRIBE_CARD_ACTION,
|
|
248
|
+
title: tl("Your store, live", "Sua loja no ar"),
|
|
249
|
+
body: tl(
|
|
250
|
+
"What is ready and what is still open live here. Each line opens its screen.",
|
|
251
|
+
"O que j\xE1 est\xE1 pronto e o que falta fica aqui. Cada linha abre a tela dela."
|
|
252
|
+
),
|
|
253
|
+
skipIfMissing: true
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
var journeyTour = buildOwnJourneyTour();
|
|
259
|
+
function buildJourneyClosingStop(routes) {
|
|
260
|
+
return {
|
|
261
|
+
anchor: TRIAL_TAB_ANCHOR,
|
|
262
|
+
route: routes?.trial ?? DEFAULT_ROUTES.trial,
|
|
263
|
+
action: DESCRIBE_TRIAL_TAB_ACTION,
|
|
264
|
+
title: tl("Free trial", "Teste gr\xE1tis"),
|
|
265
|
+
body: tl(
|
|
266
|
+
"Your hours, payment methods and the trial clock live in this tab.",
|
|
267
|
+
"Seus hor\xE1rios, formas de pagamento e o rel\xF3gio do teste moram nesta aba."
|
|
268
|
+
),
|
|
269
|
+
skipIfMissing: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
buildJourneyClosingStop();
|
|
273
|
+
function buildJourneyTour(runtime, routes = getActiveConfig().routes) {
|
|
274
|
+
const tours = (runtime?.onboardingTours ?? []).filter((t) => t.when !== "manual");
|
|
275
|
+
const own = tours.find((t) => t.pluginId === OWN_PLUGIN_ID);
|
|
276
|
+
const opening = own ? own.stops : buildOwnJourneyTour(routes).stops;
|
|
277
|
+
const others = tours.filter((t) => t !== own).sort((a, b) => a.order - b.order);
|
|
278
|
+
const seen = /* @__PURE__ */ new Set();
|
|
279
|
+
const stops = [];
|
|
280
|
+
for (const stop of [...opening, ...others.flatMap((t) => t.stops), buildJourneyClosingStop(routes)]) {
|
|
281
|
+
const key2 = `${stop.anchor}|${stop.action ?? ""}`;
|
|
282
|
+
if (seen.has(key2)) continue;
|
|
283
|
+
seen.add(key2);
|
|
284
|
+
stops.push({ ...stop, skipIfMissing: stop.skipIfMissing ?? true });
|
|
285
|
+
}
|
|
286
|
+
return { id: JOURNEY_TOUR_ID, title: tl("A tour of your store", "Um tour pela sua loja"), stops, onEnd: "none" };
|
|
287
|
+
}
|
|
288
|
+
var memory = /* @__PURE__ */ new Map();
|
|
289
|
+
var key = (what, tenantId) => `fayz:getting-started:${what}:${tenantId}`;
|
|
290
|
+
function flagged(what, tenantId) {
|
|
291
|
+
try {
|
|
292
|
+
if (typeof localStorage !== "undefined") return localStorage.getItem(key(what, tenantId)) !== null;
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
return memory.has(key(what, tenantId));
|
|
296
|
+
}
|
|
297
|
+
function flag(what, tenantId) {
|
|
298
|
+
memory.set(key(what, tenantId), "1");
|
|
299
|
+
try {
|
|
300
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(key(what, tenantId), (/* @__PURE__ */ new Date()).toISOString());
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function journeyTourDone(tenantId) {
|
|
305
|
+
return !!tenantId && flagged("journey-tour", tenantId);
|
|
306
|
+
}
|
|
307
|
+
function markJourneyTourDone(tenantId) {
|
|
308
|
+
flag("journey-tour", tenantId);
|
|
309
|
+
}
|
|
310
|
+
var useJourneyTourStore = create((set) => ({
|
|
311
|
+
pending: null,
|
|
312
|
+
runtime: null,
|
|
313
|
+
rememberRuntime: (runtime) => set((s) => s.runtime === runtime ? s : { runtime })
|
|
314
|
+
}));
|
|
315
|
+
var boundStore = null;
|
|
316
|
+
function currentTenant() {
|
|
317
|
+
return boundStore?.getState().tenantId ?? getActiveTenantId() ?? null;
|
|
318
|
+
}
|
|
319
|
+
function startJourneyTour(opts) {
|
|
320
|
+
const moment = opts?.moment ?? "manual";
|
|
321
|
+
const tenantId = currentTenant();
|
|
322
|
+
if (!opts?.force) {
|
|
323
|
+
if (!tenantId || journeyTourDone(tenantId)) return false;
|
|
324
|
+
if (useTourStore.getState().tour) return false;
|
|
325
|
+
}
|
|
326
|
+
const tour = buildJourneyTour(opts?.runtime ?? useJourneyTourStore.getState().runtime);
|
|
327
|
+
if (tenantId) markJourneyTourDone(tenantId);
|
|
328
|
+
useJourneyTourStore.setState({ pending: null });
|
|
329
|
+
useTourStore.getState().start(tour);
|
|
330
|
+
reportEnd(tenantId, moment);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
function reportEnd(tenantId, moment) {
|
|
334
|
+
const off = useTourStore.subscribe((state) => {
|
|
335
|
+
if (state.tour || state.lastOutcome?.tourId !== JOURNEY_TOUR_ID) return;
|
|
336
|
+
off();
|
|
337
|
+
emitDomainEvent(TOUR_DONE_EVENT, { completed: state.lastOutcome.completed, moment }, { tenantId });
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function startJourneyTourSoon(moment, delayMs = TRIAL_START_SETTLE_MS) {
|
|
341
|
+
setTimeout(() => {
|
|
342
|
+
startJourneyTour({ moment });
|
|
343
|
+
}, delayMs);
|
|
344
|
+
}
|
|
345
|
+
function minimumsCheck() {
|
|
346
|
+
if (!boundStore) return;
|
|
347
|
+
const { journey, error } = boundStore.getState();
|
|
348
|
+
const tenantId = currentTenant();
|
|
349
|
+
const rows = useActivationStore.getState().rows;
|
|
350
|
+
if (!tenantId || !rows || rows.length === 0) return;
|
|
351
|
+
const phase = journey?.phase ?? (error ? "setup" : null);
|
|
352
|
+
if (phase !== "setup") return;
|
|
353
|
+
if (!countMinimums(journey, rows).ready) return;
|
|
354
|
+
if (flagged("minimums-ready", tenantId)) return;
|
|
355
|
+
flag("minimums-ready", tenantId);
|
|
356
|
+
emitDomainEvent(MINIMUMS_READY_EVENT, {}, { tenantId });
|
|
357
|
+
if (!journeyTourDone(tenantId)) useJourneyTourStore.setState({ pending: "minimums_ready" });
|
|
358
|
+
}
|
|
359
|
+
var uninstall = null;
|
|
360
|
+
function installJourneyTourTriggers(store) {
|
|
361
|
+
uninstall?.();
|
|
362
|
+
boundStore = store;
|
|
363
|
+
const offs = [
|
|
364
|
+
onDomainEvent(TRIAL_STARTED_EVENT, () => {
|
|
365
|
+
useJourneyTourStore.setState({ pending: null });
|
|
366
|
+
startJourneyTourSoon("trial_started");
|
|
367
|
+
}),
|
|
368
|
+
store.subscribe(minimumsCheck),
|
|
369
|
+
useActivationStore.subscribe(minimumsCheck)
|
|
370
|
+
];
|
|
371
|
+
minimumsCheck();
|
|
372
|
+
uninstall = () => {
|
|
373
|
+
for (const off of offs) off();
|
|
374
|
+
if (boundStore === store) boundStore = null;
|
|
375
|
+
uninstall = null;
|
|
376
|
+
};
|
|
377
|
+
return uninstall;
|
|
378
|
+
}
|
|
379
|
+
function useRememberJourneyRuntime() {
|
|
380
|
+
const runtime = usePluginRuntimeOptional();
|
|
381
|
+
const remember = useJourneyTourStore((s) => s.rememberRuntime);
|
|
382
|
+
React5.useEffect(() => {
|
|
383
|
+
if (runtime) remember(runtime);
|
|
384
|
+
}, [runtime, remember]);
|
|
385
|
+
return runtime;
|
|
386
|
+
}
|
|
387
|
+
function useTourDescription(action, describe) {
|
|
388
|
+
const latest = React5.useRef(describe);
|
|
389
|
+
latest.current = describe;
|
|
390
|
+
React5.useEffect(() => registerTourAction(action, () => latest.current()), [action]);
|
|
391
|
+
}
|
|
392
|
+
function useJourneyTourAutostart(ref, enabled, runtime) {
|
|
393
|
+
const pending = useJourneyTourStore((s) => s.pending);
|
|
394
|
+
React5.useEffect(() => {
|
|
395
|
+
if (!pending || !enabled) return;
|
|
396
|
+
const attempt = () => {
|
|
397
|
+
const el = ref.current;
|
|
398
|
+
if (!el || typeof document === "undefined" || !document.elementFromPoint) return;
|
|
399
|
+
const r = el.getBoundingClientRect();
|
|
400
|
+
if (r.width === 0 || r.height === 0) return;
|
|
401
|
+
const x = Math.min(Math.max(r.left + r.width / 2, 0), window.innerWidth - 1);
|
|
402
|
+
const y = Math.min(Math.max(r.top + 24, 0), window.innerHeight - 1);
|
|
403
|
+
const hit = document.elementFromPoint(x, y);
|
|
404
|
+
if (!hit || !el.contains(hit)) return;
|
|
405
|
+
startJourneyTour({ moment: pending, runtime });
|
|
406
|
+
};
|
|
407
|
+
attempt();
|
|
408
|
+
const id = window.setInterval(attempt, 800);
|
|
409
|
+
return () => window.clearInterval(id);
|
|
410
|
+
}, [pending, enabled, runtime, ref]);
|
|
411
|
+
}
|
|
412
|
+
var message = (e) => e instanceof Error ? e.message : String(e);
|
|
413
|
+
var useJourneyStore = create((set, get) => ({
|
|
414
|
+
provider: null,
|
|
415
|
+
tenantId: void 0,
|
|
416
|
+
journey: null,
|
|
417
|
+
loading: false,
|
|
418
|
+
starting: false,
|
|
419
|
+
error: null,
|
|
420
|
+
bind: (provider) => {
|
|
421
|
+
set({ provider });
|
|
422
|
+
installJourneyTourTriggers(useJourneyStore);
|
|
423
|
+
},
|
|
424
|
+
load: async (tenantId) => {
|
|
425
|
+
const { provider } = get();
|
|
426
|
+
if (!provider) return null;
|
|
427
|
+
if (tenantId !== void 0 && tenantId !== get().tenantId) set({ tenantId, journey: null, error: null });
|
|
428
|
+
set({ loading: true });
|
|
429
|
+
try {
|
|
430
|
+
const journey = await provider.getJourney();
|
|
431
|
+
if (tenantId !== void 0 && get().tenantId !== tenantId) return null;
|
|
432
|
+
set({ journey, loading: false, error: null });
|
|
433
|
+
return journey;
|
|
434
|
+
} catch (e) {
|
|
435
|
+
set({ loading: false, error: message(e) });
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
startTrial: async () => {
|
|
440
|
+
const { provider } = get();
|
|
441
|
+
if (!provider) throw new Error("getting-started: no data provider bound");
|
|
442
|
+
set({ starting: true });
|
|
443
|
+
try {
|
|
444
|
+
const result = await provider.startTrial();
|
|
445
|
+
await get().load();
|
|
446
|
+
if (result.ok && !result.alreadyStarted) {
|
|
447
|
+
emitDomainEvent(
|
|
448
|
+
"getting_started.trial_started",
|
|
449
|
+
{ trialEndsAt: result.trialEndsAt, trialDays: result.trialDays ?? null },
|
|
450
|
+
{ tenantId: get().tenantId ?? null }
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
} finally {
|
|
455
|
+
set({ starting: false });
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
requestContract: async (note) => {
|
|
459
|
+
const { provider } = get();
|
|
460
|
+
if (!provider) throw new Error("getting-started: no data provider bound");
|
|
461
|
+
const result = await provider.requestContract(note);
|
|
462
|
+
await get().load();
|
|
463
|
+
if (!result.alreadyRequestedToday) {
|
|
464
|
+
emitDomainEvent("getting_started.wants_contract", { note: note ?? null }, { tenantId: get().tenantId ?? null });
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
},
|
|
468
|
+
reset: () => set({ tenantId: void 0, journey: null, loading: false, starting: false, error: null })
|
|
469
|
+
}));
|
|
470
|
+
registerTenantScopedStore("getting-started.journey", () => useJourneyStore.getState().reset());
|
|
471
|
+
var ctx = createPluginContext("GettingStarted");
|
|
472
|
+
var GettingStartedContextProvider = ctx.ContextProvider;
|
|
473
|
+
var useGettingStartedConfig = ctx.useConfig;
|
|
474
|
+
ctx.useProvider;
|
|
475
|
+
var useGettingStartedStore = ctx.useStore;
|
|
476
|
+
function withGettingStartedContext(Component, scope) {
|
|
477
|
+
const Wrapped = (props) => /* @__PURE__ */ jsx(GettingStartedContextProvider, { config: scope.config, provider: scope.provider, store: useJourneyStore, children: /* @__PURE__ */ jsx(Component, { ...props }) });
|
|
478
|
+
Wrapped.displayName = `GettingStarted(${Component.displayName ?? Component.name ?? "Component"})`;
|
|
479
|
+
return Wrapped;
|
|
480
|
+
}
|
|
481
|
+
function useEnsureJourney() {
|
|
482
|
+
const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
483
|
+
const load = useGettingStartedStore((s) => s.load);
|
|
484
|
+
const known = useGettingStartedStore((s) => s.tenantId);
|
|
485
|
+
const journey = useGettingStartedStore((s) => s.journey);
|
|
486
|
+
const loading = useGettingStartedStore((s) => s.loading);
|
|
487
|
+
const error = useGettingStartedStore((s) => s.error);
|
|
488
|
+
React5.useEffect(() => {
|
|
489
|
+
if (!tenantId) return;
|
|
490
|
+
if (tenantId === known && (journey || loading || error)) return;
|
|
491
|
+
void load(tenantId);
|
|
492
|
+
}, [tenantId, known, journey, loading, error, load]);
|
|
493
|
+
}
|
|
494
|
+
function withJourneyDeclaration(Component) {
|
|
495
|
+
const Synced = (props) => {
|
|
496
|
+
useJourneyDeclaration();
|
|
497
|
+
return /* @__PURE__ */ jsx(Component, { ...props });
|
|
498
|
+
};
|
|
499
|
+
Synced.displayName = `JourneyDeclared(${Component.displayName ?? Component.name ?? "Component"})`;
|
|
500
|
+
return Synced;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/data/types.ts
|
|
504
|
+
var GettingStartedError = class extends Error {
|
|
505
|
+
constructor(message2, code2) {
|
|
506
|
+
super(message2);
|
|
507
|
+
this.name = "GettingStartedError";
|
|
508
|
+
this.code = code2;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
function isForbidden(error) {
|
|
512
|
+
return error instanceof GettingStartedError && error.code === "42501";
|
|
513
|
+
}
|
|
514
|
+
function isMissingClock(error) {
|
|
515
|
+
if (error instanceof GettingStartedError && (error.code === "PGRST202" || error.code === "42883")) return true;
|
|
516
|
+
const message2 = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
517
|
+
return /could not find the function|function .* does not exist|schema cache/i.test(message2);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/data/tables.ts
|
|
521
|
+
var T = {
|
|
522
|
+
config: "plg_getting_started_config",
|
|
523
|
+
journeys: "plg_getting_started_journeys",
|
|
524
|
+
events: "plg_getting_started_events",
|
|
525
|
+
journeyView: "v_getting_started_journey"
|
|
526
|
+
};
|
|
527
|
+
var RPC = {
|
|
528
|
+
journey: "getting_started_journey",
|
|
529
|
+
startTrial: "getting_started_start_trial",
|
|
530
|
+
requestContract: "getting_started_request_contract",
|
|
531
|
+
// 002_platform_operator.sql — the platform reads every tenant's journey and acts on one trial.
|
|
532
|
+
isPlatformOperator: "getting_started_is_platform_operator",
|
|
533
|
+
platformJourneys: "getting_started_platform_journeys",
|
|
534
|
+
trialSet: "getting_started_trial_set"
|
|
535
|
+
};
|
|
536
|
+
function client() {
|
|
537
|
+
const c = getSupabaseClientOptional();
|
|
538
|
+
if (!c) throw new GettingStartedError("getting-started: no Supabase client registered");
|
|
539
|
+
return c;
|
|
540
|
+
}
|
|
541
|
+
function raise(error) {
|
|
542
|
+
const e = error ?? {};
|
|
543
|
+
throw new GettingStartedError(
|
|
544
|
+
typeof e.message === "string" ? e.message : "getting-started: request failed",
|
|
545
|
+
typeof e.code === "string" ? e.code : void 0
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
var str = (v) => typeof v === "string" ? v : null;
|
|
549
|
+
function mapMinimums(raw) {
|
|
550
|
+
const m = raw ?? {};
|
|
551
|
+
const menu = m.menu === true;
|
|
552
|
+
const hours = m.hours === true;
|
|
553
|
+
const testOrder = m.test_order === true;
|
|
554
|
+
return { menu, hours, testOrder, ready: m.ready === true || menu && hours && testOrder };
|
|
555
|
+
}
|
|
556
|
+
function mapJourney(raw) {
|
|
557
|
+
const phase = str(raw.phase);
|
|
558
|
+
return {
|
|
559
|
+
tenantId: String(raw.tenant_id ?? ""),
|
|
560
|
+
phase: phase === "trial" || phase === "expired" || phase === "converted" ? phase : "setup",
|
|
561
|
+
trialDays: Number(raw.trial_days ?? 0) || 0,
|
|
562
|
+
readyAt: str(raw.ready_at),
|
|
563
|
+
trialStartedAt: str(raw.trial_started_at),
|
|
564
|
+
trialEndsAt: str(raw.trial_ends_at),
|
|
565
|
+
setupDeadlineAt: str(raw.setup_deadline_at),
|
|
566
|
+
convertedAt: str(raw.converted_at),
|
|
567
|
+
wantsContractAt: str(raw.wants_contract_at),
|
|
568
|
+
source: str(raw.source),
|
|
569
|
+
utm: raw.utm && typeof raw.utm === "object" ? raw.utm : {},
|
|
570
|
+
minimums: mapMinimums(raw.minimums)
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function mapStartTrial(raw) {
|
|
574
|
+
if (raw.ok === true) {
|
|
575
|
+
return {
|
|
576
|
+
ok: true,
|
|
577
|
+
alreadyStarted: raw.already_started === true,
|
|
578
|
+
trialStartedAt: String(raw.trial_started_at ?? ""),
|
|
579
|
+
trialEndsAt: String(raw.trial_ends_at ?? ""),
|
|
580
|
+
trialDays: raw.trial_days == null ? void 0 : Number(raw.trial_days)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return { ok: false, reason: refusal(raw.reason), minimums: mapMinimums(raw.minimums) };
|
|
584
|
+
}
|
|
585
|
+
var REFUSALS = ["minimums_missing", "no_trial_window", "already_converted", "already_paid"];
|
|
586
|
+
function refusal(raw) {
|
|
587
|
+
return REFUSALS.find((r) => r === raw) ?? "minimums_missing";
|
|
588
|
+
}
|
|
589
|
+
function mapEvent(r) {
|
|
590
|
+
return {
|
|
591
|
+
id: String(r.id),
|
|
592
|
+
tenantId: String(r.tenant_id),
|
|
593
|
+
kind: String(r.kind),
|
|
594
|
+
day: String(r.day),
|
|
595
|
+
channel: str(r.channel),
|
|
596
|
+
payload: r.payload && typeof r.payload === "object" ? r.payload : {},
|
|
597
|
+
actorId: str(r.actor_id),
|
|
598
|
+
createdAt: String(r.created_at)
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function createSupabaseGettingStartedProvider() {
|
|
602
|
+
return {
|
|
603
|
+
async getJourney() {
|
|
604
|
+
const { data, error } = await client().rpc(RPC.journey);
|
|
605
|
+
if (error) raise(error);
|
|
606
|
+
return data && typeof data === "object" ? mapJourney(data) : null;
|
|
607
|
+
},
|
|
608
|
+
async startTrial() {
|
|
609
|
+
const { data, error } = await client().rpc(RPC.startTrial);
|
|
610
|
+
if (error) raise(error);
|
|
611
|
+
return mapStartTrial(data ?? {});
|
|
612
|
+
},
|
|
613
|
+
async requestContract(note) {
|
|
614
|
+
const { data, error } = await client().rpc(RPC.requestContract, { p_note: note ?? null });
|
|
615
|
+
if (error) raise(error);
|
|
616
|
+
const r = data ?? {};
|
|
617
|
+
return { ok: true, alreadyRequestedToday: r.already_requested_today === true };
|
|
618
|
+
},
|
|
619
|
+
async listEvents(limit = 20) {
|
|
620
|
+
let query = client().from(T.events).select("*").order("created_at", { ascending: false }).limit(limit);
|
|
621
|
+
const tenantId = getActiveTenantId();
|
|
622
|
+
if (tenantId) query = query.eq("tenant_id", tenantId);
|
|
623
|
+
const { data, error } = await query;
|
|
624
|
+
if (error) raise(error);
|
|
625
|
+
return (data ?? []).map(mapEvent);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/data/mock.ts
|
|
631
|
+
var DAY_MS = 864e5;
|
|
632
|
+
var isoDay = (d) => d.toISOString().slice(0, 10);
|
|
633
|
+
function derivePhase(input) {
|
|
634
|
+
if (input.convertedAt) return "converted";
|
|
635
|
+
if (input.trialEndsAt && new Date(input.trialEndsAt).getTime() <= input.now.getTime()) return "expired";
|
|
636
|
+
if (input.trialStartedAt) return "trial";
|
|
637
|
+
return "setup";
|
|
638
|
+
}
|
|
639
|
+
function createMockGettingStartedProvider(options = {}) {
|
|
640
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
641
|
+
const tenantId = options.tenantId ?? "mock-tenant";
|
|
642
|
+
const trialDays = options.trialDays ?? 14;
|
|
643
|
+
const createdAt = now();
|
|
644
|
+
const state = {
|
|
645
|
+
trialEndsAt: new Date(createdAt.getTime() + (options.setupWindowDays ?? 30) * DAY_MS).toISOString(),
|
|
646
|
+
readyAt: null,
|
|
647
|
+
trialStartedAt: null,
|
|
648
|
+
trialDays: null,
|
|
649
|
+
convertedAt: null,
|
|
650
|
+
wantsContractAt: null,
|
|
651
|
+
minimums: { menu: false, hours: false, testOrder: false, ...options.minimums },
|
|
652
|
+
events: []
|
|
653
|
+
};
|
|
654
|
+
const minimums = () => ({
|
|
655
|
+
...state.minimums,
|
|
656
|
+
ready: state.minimums.menu && state.minimums.hours && state.minimums.testOrder
|
|
657
|
+
});
|
|
658
|
+
const recordEvent = (kind, payload) => {
|
|
659
|
+
const day = isoDay(now());
|
|
660
|
+
if (state.events.some((e) => e.kind === kind && e.day === day)) return false;
|
|
661
|
+
state.events.unshift({
|
|
662
|
+
id: `evt-${state.events.length + 1}`,
|
|
663
|
+
tenantId,
|
|
664
|
+
kind,
|
|
665
|
+
day,
|
|
666
|
+
channel: null,
|
|
667
|
+
payload,
|
|
668
|
+
actorId: null,
|
|
669
|
+
createdAt: now().toISOString()
|
|
670
|
+
});
|
|
671
|
+
return true;
|
|
672
|
+
};
|
|
673
|
+
return {
|
|
674
|
+
async getJourney() {
|
|
675
|
+
const current = now();
|
|
676
|
+
return {
|
|
677
|
+
tenantId,
|
|
678
|
+
phase: derivePhase({ now: current, ...state }),
|
|
679
|
+
trialDays: state.trialDays ?? trialDays,
|
|
680
|
+
readyAt: state.readyAt,
|
|
681
|
+
trialStartedAt: state.trialStartedAt,
|
|
682
|
+
trialEndsAt: state.trialEndsAt,
|
|
683
|
+
setupDeadlineAt: state.trialStartedAt ? null : state.trialEndsAt,
|
|
684
|
+
convertedAt: state.convertedAt,
|
|
685
|
+
wantsContractAt: state.wantsContractAt,
|
|
686
|
+
source: null,
|
|
687
|
+
utm: {},
|
|
688
|
+
minimums: minimums()
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
async startTrial() {
|
|
692
|
+
if (state.trialStartedAt) {
|
|
693
|
+
return { ok: true, alreadyStarted: true, trialStartedAt: state.trialStartedAt, trialEndsAt: state.trialEndsAt ?? "" };
|
|
694
|
+
}
|
|
695
|
+
const m = minimums();
|
|
696
|
+
if (!m.ready) return { ok: false, reason: "minimums_missing", minimums: m };
|
|
697
|
+
const started = now();
|
|
698
|
+
const ends = new Date(started.getTime() + trialDays * DAY_MS);
|
|
699
|
+
state.readyAt = state.readyAt ?? started.toISOString();
|
|
700
|
+
state.trialStartedAt = started.toISOString();
|
|
701
|
+
state.trialDays = trialDays;
|
|
702
|
+
state.trialEndsAt = ends.toISOString();
|
|
703
|
+
recordEvent("trial_started", { trial_days: trialDays, ends_at: state.trialEndsAt });
|
|
704
|
+
return { ok: true, alreadyStarted: false, trialStartedAt: state.trialStartedAt, trialEndsAt: state.trialEndsAt, trialDays };
|
|
705
|
+
},
|
|
706
|
+
async requestContract(note) {
|
|
707
|
+
const inserted = recordEvent("wants_contract", { note: (note ?? "").slice(0, 500) });
|
|
708
|
+
if (!inserted) return { ok: true, alreadyRequestedToday: true };
|
|
709
|
+
state.wantsContractAt = state.wantsContractAt ?? now().toISOString();
|
|
710
|
+
return { ok: true, alreadyRequestedToday: false };
|
|
711
|
+
},
|
|
712
|
+
async listEvents(limit = 20) {
|
|
713
|
+
return state.events.slice(0, limit);
|
|
714
|
+
},
|
|
715
|
+
setMinimums(patch) {
|
|
716
|
+
state.minimums = { ...state.minimums, ...patch };
|
|
717
|
+
},
|
|
718
|
+
markConverted() {
|
|
719
|
+
state.convertedAt = now().toISOString();
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
var STORE_SETTINGS_SAVED_EVENT = "getting_started.store_settings_saved";
|
|
724
|
+
function asHours(value) {
|
|
725
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
726
|
+
const out = {};
|
|
727
|
+
for (const day of WEEKDAYS) {
|
|
728
|
+
const ranges = value[day];
|
|
729
|
+
if (Array.isArray(ranges)) {
|
|
730
|
+
out[day] = ranges.filter((r) => !!r && typeof r === "object" && typeof r.open === "string" && typeof r.close === "string").map((r) => ({ open: r.open, close: r.close }));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
function asMethods(value) {
|
|
736
|
+
return normalizePaymentMethods(value);
|
|
737
|
+
}
|
|
738
|
+
function storeOf(settings) {
|
|
739
|
+
const store = settings?.store;
|
|
740
|
+
return store && typeof store === "object" && !Array.isArray(store) ? store : {};
|
|
741
|
+
}
|
|
742
|
+
function readStoreSettings(settings = useOrganizationStore.getState().currentOrg?.settings) {
|
|
743
|
+
const store = storeOf(settings);
|
|
744
|
+
return { hours: asHours(store.hours), methods: asMethods(store.payment_methods) };
|
|
745
|
+
}
|
|
746
|
+
function reflectInWorkspace(key2, value) {
|
|
747
|
+
const org = useOrganizationStore.getState().currentOrg;
|
|
748
|
+
if (!org) return;
|
|
749
|
+
const store = { ...storeOf(org.settings), [key2]: value };
|
|
750
|
+
useOrganizationStore.getState().paintCurrentOrg({ ...org, settings: { ...org.settings ?? {}, store } });
|
|
751
|
+
emitDomainEvent(STORE_SETTINGS_SAVED_EVENT, { key: `store.${key2}` }, { tenantId: org.id });
|
|
752
|
+
}
|
|
753
|
+
var text = (e) => e instanceof Error ? e.message : typeof e?.message === "string" ? String(e.message) : String(e);
|
|
754
|
+
var code = (e) => typeof e?.code === "string" ? String(e.code) : void 0;
|
|
755
|
+
function isUnknownKey(error) {
|
|
756
|
+
return code(error) === "22023" || /unknown key/i.test(text(error));
|
|
757
|
+
}
|
|
758
|
+
var supabaseStoreSettingsProvider = {
|
|
759
|
+
async read(tenantId) {
|
|
760
|
+
const supabase = getSupabaseClientOptional();
|
|
761
|
+
if (!supabase) return void 0;
|
|
762
|
+
const { data, error } = await supabase.from("tenants").select("settings").eq("id", tenantId).maybeSingle();
|
|
763
|
+
if (error) throw error;
|
|
764
|
+
return storeBranch(data?.settings ?? void 0);
|
|
765
|
+
},
|
|
766
|
+
async save(tenantId, key2, value) {
|
|
767
|
+
const supabase = getSupabaseClientOptional();
|
|
768
|
+
if (!supabase) return { ok: false, reason: "offline" };
|
|
769
|
+
const { error } = await supabase.rpc("tenant_setting_set", { p_key: `store.${key2}`, p_value: value ?? null });
|
|
770
|
+
if (!error) return { ok: true, via: "rpc" };
|
|
771
|
+
if (!isUnknownKey(error)) return { ok: false, reason: "error", message: text(error) };
|
|
772
|
+
const fresh = useOrganizationStore.getState().currentOrg;
|
|
773
|
+
const base = fresh?.id === tenantId ? fresh.settings ?? {} : {};
|
|
774
|
+
const settings = { ...base, store: { ...storeOf(base), [key2]: value } };
|
|
775
|
+
const { error: rowError } = await supabase.from("tenants").update({ settings }).eq("id", tenantId);
|
|
776
|
+
if (rowError) return { ok: false, reason: "error", message: text(rowError) };
|
|
777
|
+
return { ok: true, via: "row" };
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var activeProvider = supabaseStoreSettingsProvider;
|
|
781
|
+
function configureStoreSettings(provider) {
|
|
782
|
+
activeProvider = provider ?? supabaseStoreSettingsProvider;
|
|
783
|
+
branchCache = null;
|
|
784
|
+
}
|
|
785
|
+
function getStoreSettingsProvider() {
|
|
786
|
+
return activeProvider;
|
|
787
|
+
}
|
|
788
|
+
function storeBranch(settings) {
|
|
789
|
+
return settings && "store" in settings ? storeOf(settings) : void 0;
|
|
790
|
+
}
|
|
791
|
+
async function saveStoreSetting(key2, value) {
|
|
792
|
+
const org = useOrganizationStore.getState().currentOrg;
|
|
793
|
+
if (!org) return { ok: false, reason: "no-tenant" };
|
|
794
|
+
reflectInWorkspace(key2, value);
|
|
795
|
+
branchCache = null;
|
|
796
|
+
let result;
|
|
797
|
+
try {
|
|
798
|
+
result = await activeProvider.save(org.id, key2, value);
|
|
799
|
+
} catch (e) {
|
|
800
|
+
result = { ok: false, reason: "error", message: text(e) };
|
|
801
|
+
}
|
|
802
|
+
if (result.ok) landed(org.id, key2);
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
function landed(tenantId, key2) {
|
|
806
|
+
emitDomainEvent(STORE_SETTINGS_SAVED_EVENT, { key: `store.${key2}`, persisted: true }, { tenantId });
|
|
807
|
+
}
|
|
808
|
+
var READ_CACHE_MS = 1e4;
|
|
809
|
+
var branchCache = null;
|
|
810
|
+
async function loadStoreSettings() {
|
|
811
|
+
const org = useOrganizationStore.getState().currentOrg;
|
|
812
|
+
const inMemory = org?.settings;
|
|
813
|
+
if (inMemory && "store" in inMemory) return inMemory;
|
|
814
|
+
const tenantId = org?.id ?? getActiveTenantId();
|
|
815
|
+
if (!tenantId) return inMemory;
|
|
816
|
+
const now = Date.now();
|
|
817
|
+
let store;
|
|
818
|
+
if (branchCache && branchCache.tenantId === tenantId && now - branchCache.at < READ_CACHE_MS) {
|
|
819
|
+
store = branchCache.store;
|
|
820
|
+
} else {
|
|
821
|
+
try {
|
|
822
|
+
store = await activeProvider.read(tenantId);
|
|
823
|
+
} catch {
|
|
824
|
+
store = void 0;
|
|
825
|
+
}
|
|
826
|
+
branchCache = { tenantId, at: now, store };
|
|
827
|
+
}
|
|
828
|
+
if (!store) return inMemory;
|
|
829
|
+
const current = useOrganizationStore.getState().currentOrg;
|
|
830
|
+
if (current && current.id === tenantId) {
|
|
831
|
+
useOrganizationStore.getState().paintCurrentOrg({ ...current, settings: { ...current.settings ?? {}, store } });
|
|
832
|
+
}
|
|
833
|
+
return { ...inMemory ?? {}, store };
|
|
834
|
+
}
|
|
835
|
+
var PROFILE_KEYS = {
|
|
836
|
+
operation: "food.operation",
|
|
837
|
+
serviceModes: "food.serviceModes",
|
|
838
|
+
deliveryMethod: "food.deliveryMethod",
|
|
839
|
+
tables: "food.tables",
|
|
840
|
+
marketplaceApps: "food.marketplaceApps",
|
|
841
|
+
welcomeArticle: "workspace.welcomeArticle"
|
|
842
|
+
};
|
|
843
|
+
var OPERATIONS = ["restaurant", "snack-bar", "pizzeria", "cafe", "bar", "dark-kitchen", "food-truck", "bakery", "other"];
|
|
844
|
+
var SERVICE_MODES = ["counter", "own-delivery", "dine-in", "aggregator"];
|
|
845
|
+
var DELIVERY_METHODS = ["radius", "neighborhood"];
|
|
846
|
+
var TABLE_BANDS = ["lt10", "10-25", "25-50", "gt50"];
|
|
847
|
+
var WELCOME_ARTICLES = ["a", "o"];
|
|
848
|
+
var STORE_PROFILE_STEP_KEY = "profile";
|
|
849
|
+
function oneOf(value, allowed) {
|
|
850
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
851
|
+
}
|
|
852
|
+
function someOf(value, allowed) {
|
|
853
|
+
const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
854
|
+
return allowed.filter((id) => list.includes(id));
|
|
855
|
+
}
|
|
856
|
+
function storeProfileFrom(source) {
|
|
857
|
+
const out = {};
|
|
858
|
+
if (!source) return out;
|
|
859
|
+
const operation = oneOf(source.operation, OPERATIONS);
|
|
860
|
+
if (operation) out.operation = operation;
|
|
861
|
+
const modes = someOf(source.serviceModes, SERVICE_MODES);
|
|
862
|
+
if (modes.length > 0) out.serviceModes = modes;
|
|
863
|
+
const delivery = oneOf(source.deliveryMethod, DELIVERY_METHODS);
|
|
864
|
+
if (delivery) out.deliveryMethod = delivery;
|
|
865
|
+
const tables = oneOf(source.tables, TABLE_BANDS);
|
|
866
|
+
if (tables) out.tables = tables;
|
|
867
|
+
if (Array.isArray(source.marketplaceApps)) {
|
|
868
|
+
const apps = source.marketplaceApps.filter((a) => typeof a === "string" && a.trim() !== "").map((a) => a.trim().toLowerCase());
|
|
869
|
+
if (apps.length > 0) out.marketplaceApps = [...new Set(apps)];
|
|
870
|
+
}
|
|
871
|
+
const article = oneOf(source.welcomeArticle, WELCOME_ARTICLES);
|
|
872
|
+
if (article) out.welcomeArticle = article;
|
|
873
|
+
return out;
|
|
874
|
+
}
|
|
875
|
+
function storeProfileToAnswers(profile) {
|
|
876
|
+
const answers = {};
|
|
877
|
+
for (const [field, key2] of Object.entries(PROFILE_KEYS)) {
|
|
878
|
+
if (profile[field] !== void 0) answers[key2] = profile[field];
|
|
879
|
+
}
|
|
880
|
+
return answers;
|
|
881
|
+
}
|
|
882
|
+
function storeProfileFromAnswers(answers) {
|
|
883
|
+
if (!answers) return {};
|
|
884
|
+
const source = {};
|
|
885
|
+
for (const [field, key2] of Object.entries(PROFILE_KEYS)) source[field] = answers[key2];
|
|
886
|
+
return storeProfileFrom(source);
|
|
887
|
+
}
|
|
888
|
+
function storeProfileAnswered(profile) {
|
|
889
|
+
return (profile?.serviceModes?.length ?? 0) > 0;
|
|
890
|
+
}
|
|
891
|
+
var businessProfileStoreProfileProvider = {
|
|
892
|
+
read: async () => {
|
|
893
|
+
const answers = useBusinessProfileStore.getState().profile?.answers;
|
|
894
|
+
return answers ? storeProfileFromAnswers(answers) : null;
|
|
895
|
+
},
|
|
896
|
+
save: (tenantId, patch, context) => useBusinessProfileStore.getState().record({ tenantId, answers: storeProfileToAnswers(patch), businessName: context.businessName })
|
|
897
|
+
};
|
|
898
|
+
var activeProfileProvider = businessProfileStoreProfileProvider;
|
|
899
|
+
function configureStoreProfile(provider) {
|
|
900
|
+
activeProfileProvider = provider ?? businessProfileStoreProfileProvider;
|
|
901
|
+
}
|
|
902
|
+
function getStoreProfileProvider() {
|
|
903
|
+
return activeProfileProvider;
|
|
904
|
+
}
|
|
905
|
+
var MINIMUM_WORDS = {
|
|
906
|
+
menu: "card\xE1pio com ao menos um produto",
|
|
907
|
+
hours: "hor\xE1rio de funcionamento",
|
|
908
|
+
testOrder: "um pedido de teste"
|
|
909
|
+
};
|
|
910
|
+
function missingMinimums(minimums) {
|
|
911
|
+
return MINIMUM_IDS.filter((id) => !minimums[id]);
|
|
912
|
+
}
|
|
913
|
+
function describeJourney(journey) {
|
|
914
|
+
const missing = missingMinimums(journey.minimums);
|
|
915
|
+
return {
|
|
916
|
+
phase: journey.phase,
|
|
917
|
+
trialDays: journey.trialDays,
|
|
918
|
+
trialStartedAt: journey.trialStartedAt,
|
|
919
|
+
trialEndsAt: journey.trialEndsAt,
|
|
920
|
+
setupDeadlineAt: journey.setupDeadlineAt,
|
|
921
|
+
wantsContractAt: journey.wantsContractAt,
|
|
922
|
+
minimums: journey.minimums,
|
|
923
|
+
missing: missing.map((id) => MINIMUM_WORDS[id]),
|
|
924
|
+
ready: journey.minimums.ready
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
var HOURS_TEMPLATE_IDS = ["lunch", "dinner", "lunch-dinner", "weekend"];
|
|
928
|
+
var EVERY_DAY = (shifts) => ({
|
|
929
|
+
mon: shifts,
|
|
930
|
+
tue: shifts,
|
|
931
|
+
wed: shifts,
|
|
932
|
+
thu: shifts,
|
|
933
|
+
fri: shifts,
|
|
934
|
+
sat: shifts,
|
|
935
|
+
sun: shifts
|
|
936
|
+
});
|
|
937
|
+
var HOURS_TEMPLATES = [
|
|
938
|
+
{ id: "lunch", label: "Almo\xE7o", hint: "todo dia \xB7 11:00\u201315:00", hours: EVERY_DAY([{ open: "11:00", close: "15:00" }]) },
|
|
939
|
+
{ id: "dinner", label: "Jantar", hint: "todo dia \xB7 18:00\u201323:00", hours: EVERY_DAY([{ open: "18:00", close: "23:00" }]) },
|
|
940
|
+
{
|
|
941
|
+
id: "lunch-dinner",
|
|
942
|
+
label: "Almo\xE7o e jantar",
|
|
943
|
+
hint: "todo dia \xB7 11\u201315 e 18\u201323",
|
|
944
|
+
hours: EVERY_DAY([{ open: "11:00", close: "15:00" }, { open: "18:00", close: "23:00" }])
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
id: "weekend",
|
|
948
|
+
label: "Fim de semana",
|
|
949
|
+
hint: "sex a dom \xB7 18:00\u201300:00",
|
|
950
|
+
hours: {
|
|
951
|
+
mon: [],
|
|
952
|
+
tue: [],
|
|
953
|
+
wed: [],
|
|
954
|
+
thu: [],
|
|
955
|
+
fri: [{ open: "18:00", close: "00:00" }],
|
|
956
|
+
sat: [{ open: "18:00", close: "00:00" }],
|
|
957
|
+
sun: [{ open: "18:00", close: "00:00" }]
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
];
|
|
961
|
+
var HOURS_TEMPLATE_CHOICE = "store-hours-template";
|
|
962
|
+
var HOURS_CUSTOM_OPTION = "custom";
|
|
963
|
+
function stripAccents(text2) {
|
|
964
|
+
return text2.normalize("NFD").replace(/[̀-ͯ]/g, "");
|
|
965
|
+
}
|
|
966
|
+
var DAY_WORDS = [
|
|
967
|
+
[/\bsegunda(?:-feira)?\b|\bseg\b/, "mon"],
|
|
968
|
+
[/\bterca(?:-feira)?\b|\bter\b/, "tue"],
|
|
969
|
+
[/\bquarta(?:-feira)?\b|\bqua\b/, "wed"],
|
|
970
|
+
[/\bquinta(?:-feira)?\b|\bqui\b/, "thu"],
|
|
971
|
+
[/\bsexta(?:-feira)?\b|\bsex\b/, "fri"],
|
|
972
|
+
[/\bsabado\b|\bsab\b/, "sat"],
|
|
973
|
+
[/\bdomingo\b|\bdom\b/, "sun"]
|
|
974
|
+
];
|
|
975
|
+
var ALL_DAYS = /\btodos? (?:os )?dias?\b|\bdiariamente\b|\ba semana (?:toda|inteira)\b|\btoda semana\b|\bsempre\b/;
|
|
976
|
+
var WEEKEND = /\bfi(?:m|nal) de semana\b|\bfds\b/;
|
|
977
|
+
var WEEKDAYS_ONLY = /\bdias? uteis\b|\bde segunda a sexta\b/;
|
|
978
|
+
var CLOSED = /\bfechad[oa]s?\b|\bnao abr(?:e|imos)\b|\bfecha(?:mos)?\b|\bfolga\b/;
|
|
979
|
+
function hhmm(hour, minute) {
|
|
980
|
+
const h = Number(hour);
|
|
981
|
+
const m = Number(minute ?? "0");
|
|
982
|
+
if (!Number.isInteger(h) || h < 0 || h > 24 || !Number.isInteger(m) || m < 0 || m > 59) return null;
|
|
983
|
+
return `${String(h === 24 ? 0 : h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
|
|
984
|
+
}
|
|
985
|
+
function dayRun(from, to) {
|
|
986
|
+
const a = DAY_KEYS.indexOf(from);
|
|
987
|
+
const b = DAY_KEYS.indexOf(to);
|
|
988
|
+
const out = [];
|
|
989
|
+
for (let i = a; ; i = (i + 1) % 7) {
|
|
990
|
+
out.push(DAY_KEYS[i]);
|
|
991
|
+
if (i === b) break;
|
|
992
|
+
if (out.length > 7) break;
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
function parseHoursText(raw) {
|
|
997
|
+
if (!raw || typeof raw !== "string") return null;
|
|
998
|
+
const text2 = stripAccents(raw.toLowerCase()).replace(/meia[- ]noite/g, "00:00").replace(/meio[- ]dia/g, "12:00");
|
|
999
|
+
const tokens = [];
|
|
1000
|
+
const SHIFT = /(\d{1,2})(?:[:h](\d{2}))?\s*(?:h|hs|horas)?\s*(?:as|a|ate|-|–|—|\/)\s*(?:as\s+)?(\d{1,2})(?:[:h](\d{2}))?\s*(?:h|hs|horas)?/g;
|
|
1001
|
+
for (const m of text2.matchAll(SHIFT)) {
|
|
1002
|
+
const open = hhmm(m[1], m[2]);
|
|
1003
|
+
const close = hhmm(m[3], m[4]);
|
|
1004
|
+
if (!open || !close || open === close) continue;
|
|
1005
|
+
tokens.push({ at: m.index ?? 0, kind: "shift", shift: { open, close } });
|
|
1006
|
+
}
|
|
1007
|
+
const covered = [];
|
|
1008
|
+
const inRange = (i) => covered.some(([a, b]) => i >= a && i < b);
|
|
1009
|
+
const dayAlt = DAY_WORDS.map(([re]) => re.source).join("|");
|
|
1010
|
+
const RANGE = new RegExp(`(?:de\\s+)?(${dayAlt})\\s*(?:a|as|ate|-|\u2013|\u2014)\\s*(${dayAlt})`, "g");
|
|
1011
|
+
for (const m of text2.matchAll(RANGE)) {
|
|
1012
|
+
const from = DAY_WORDS.find(([re]) => re.test(m[1]))?.[1];
|
|
1013
|
+
const to = DAY_WORDS.find(([re]) => re.test(m[2]))?.[1];
|
|
1014
|
+
if (!from || !to) continue;
|
|
1015
|
+
const at = m.index ?? 0;
|
|
1016
|
+
covered.push([at, at + m[0].length]);
|
|
1017
|
+
tokens.push({ at, kind: "days", days: dayRun(from, to), closed: false });
|
|
1018
|
+
}
|
|
1019
|
+
for (const [re, day] of DAY_WORDS) {
|
|
1020
|
+
for (const m of text2.matchAll(new RegExp(re.source, "g"))) {
|
|
1021
|
+
const at = m.index ?? 0;
|
|
1022
|
+
if (inRange(at)) continue;
|
|
1023
|
+
tokens.push({ at, kind: "days", days: [day], closed: false });
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
for (const m of text2.matchAll(new RegExp(ALL_DAYS.source, "g"))) tokens.push({ at: m.index ?? 0, kind: "days", days: [...DAY_KEYS], closed: false });
|
|
1027
|
+
for (const m of text2.matchAll(new RegExp(WEEKEND.source, "g"))) tokens.push({ at: m.index ?? 0, kind: "days", days: ["sat", "sun"], closed: false });
|
|
1028
|
+
for (const m of text2.matchAll(new RegExp(WEEKDAYS_ONLY.source, "g"))) {
|
|
1029
|
+
if (inRange(m.index ?? 0)) continue;
|
|
1030
|
+
tokens.push({ at: m.index ?? 0, kind: "days", days: ["mon", "tue", "wed", "thu", "fri"], closed: false });
|
|
1031
|
+
}
|
|
1032
|
+
for (const m of text2.matchAll(new RegExp(CLOSED.source, "g"))) {
|
|
1033
|
+
const at = m.index ?? 0;
|
|
1034
|
+
const groups = tokens.filter((t) => t.kind === "days");
|
|
1035
|
+
const nearest = groups.reduce((best, t) => {
|
|
1036
|
+
const d = Math.abs(t.at - at);
|
|
1037
|
+
return !best || d < Math.abs(best.at - at) ? t : best;
|
|
1038
|
+
}, null);
|
|
1039
|
+
if (nearest && Math.abs(nearest.at - at) <= 24) nearest.closed = true;
|
|
1040
|
+
}
|
|
1041
|
+
tokens.sort((a, b) => a.at - b.at);
|
|
1042
|
+
if (!tokens.some((t) => t.kind === "shift")) return null;
|
|
1043
|
+
const segments = [];
|
|
1044
|
+
for (const token of tokens) {
|
|
1045
|
+
const last = segments[segments.length - 1];
|
|
1046
|
+
if (token.kind === "days") {
|
|
1047
|
+
if (last && last.shifts.length === 0 && !last.closed && !token.closed) {
|
|
1048
|
+
last.days = [.../* @__PURE__ */ new Set([...last.days, ...token.days])];
|
|
1049
|
+
} else {
|
|
1050
|
+
segments.push({ days: token.days, shifts: [], closed: token.closed });
|
|
1051
|
+
}
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (!last) segments.push({ days: [...DAY_KEYS], shifts: [token.shift], closed: false });
|
|
1055
|
+
else last.shifts.push(token.shift);
|
|
1056
|
+
}
|
|
1057
|
+
const week = {};
|
|
1058
|
+
for (const day of DAY_KEYS) week[day] = [];
|
|
1059
|
+
for (const segment of segments) {
|
|
1060
|
+
if (!segment.closed && segment.shifts.length === 0) continue;
|
|
1061
|
+
for (const day of segment.days) week[day] = segment.closed ? [] : segment.shifts.map((s) => ({ ...s }));
|
|
1062
|
+
}
|
|
1063
|
+
if (!isHoursValue(week) || openDays(week).length === 0) return null;
|
|
1064
|
+
return week;
|
|
1065
|
+
}
|
|
1066
|
+
var PT_DAYS = HOURS_FORMAT_PT_BR.days;
|
|
1067
|
+
function labelWeek(hours) {
|
|
1068
|
+
const week = normalizeHours(hours);
|
|
1069
|
+
return openDays(week).map((day) => ({
|
|
1070
|
+
day,
|
|
1071
|
+
label: `${PT_DAYS[day]} ${formatShifts(week[day], HOURS_FORMAT_PT_BR)}`,
|
|
1072
|
+
shifts: (week[day] ?? []).map((s) => ({ open: s.open, close: s.close }))
|
|
1073
|
+
}));
|
|
1074
|
+
}
|
|
1075
|
+
function weekFromRows(rows) {
|
|
1076
|
+
if (!Array.isArray(rows)) return null;
|
|
1077
|
+
const week = {};
|
|
1078
|
+
for (const day of DAY_KEYS) week[day] = [];
|
|
1079
|
+
let any = false;
|
|
1080
|
+
for (const raw of rows) {
|
|
1081
|
+
const row = raw ?? {};
|
|
1082
|
+
if (typeof row.day !== "string" || !DAY_KEYS.includes(row.day) || !Array.isArray(row.shifts)) continue;
|
|
1083
|
+
const shifts = row.shifts.filter((s) => isHoursValue({ mon: [s] }));
|
|
1084
|
+
week[row.day] = shifts.map((s) => ({ open: s.open, close: s.close }));
|
|
1085
|
+
if (shifts.length > 0) any = true;
|
|
1086
|
+
}
|
|
1087
|
+
return any ? week : null;
|
|
1088
|
+
}
|
|
1089
|
+
function hoursFromArgs(args) {
|
|
1090
|
+
const templateId = typeof args.template === "string" ? args.template : void 0;
|
|
1091
|
+
const template = templateId ? HOURS_TEMPLATES.find((t) => t.id === templateId) : void 0;
|
|
1092
|
+
if (template) return { hours: normalizeHours(template.hours), template: template.id };
|
|
1093
|
+
const rows = weekFromRows(args.week);
|
|
1094
|
+
if (rows) return { hours: rows };
|
|
1095
|
+
if (isHoursValue(args.hours) && openDays(args.hours).length > 0) return { hours: normalizeHours(args.hours) };
|
|
1096
|
+
const parsed = typeof args.text === "string" ? parseHoursText(args.text) : null;
|
|
1097
|
+
return parsed ? { hours: parsed } : null;
|
|
1098
|
+
}
|
|
1099
|
+
function storeHoursOf(hours) {
|
|
1100
|
+
const week = normalizeHours(hours);
|
|
1101
|
+
const out = {};
|
|
1102
|
+
for (const day of DAY_KEYS) {
|
|
1103
|
+
const shifts = week[day] ?? [];
|
|
1104
|
+
if (shifts.length > 0) out[day] = shifts.map((s) => ({ open: s.open, close: s.close }));
|
|
1105
|
+
}
|
|
1106
|
+
return out;
|
|
1107
|
+
}
|
|
1108
|
+
var PROPOSE_HOURS_TOOL = "proposeStoreHours";
|
|
1109
|
+
var SAVE_HOURS_TOOL = "saveStoreHours";
|
|
1110
|
+
var HOURS_NOT_UNDERSTOOD = 'N\xE3o consegui montar a semana com isso. Pe\xE7a assim: "seg a sex 11h \xE0s 15h e 18h \xE0s 23h, s\xE1b 18h \xE0 meia-noite", ou ofere\xE7a os modelos (Almo\xE7o, Jantar, Almo\xE7o e jantar, Fim de semana).';
|
|
1111
|
+
function proposeStoreHours(args) {
|
|
1112
|
+
const found = hoursFromArgs(args);
|
|
1113
|
+
if (!found) return { ok: false, reason: "not_understood", hint: HOURS_NOT_UNDERSTOOD };
|
|
1114
|
+
const summary = formatHours(found.hours, HOURS_FORMAT_PT_BR);
|
|
1115
|
+
return {
|
|
1116
|
+
ok: true,
|
|
1117
|
+
saved: false,
|
|
1118
|
+
...found.template ? { template: found.template } : {},
|
|
1119
|
+
week: labelWeek(found.hours),
|
|
1120
|
+
summary,
|
|
1121
|
+
next: `Nada foi gravado. Comece o texto da resposta pelo summary ("${summary}") e, NESTA MESMA resposta, chame ${SAVE_HOURS_TOOL} com este week e este summary. O card nativo dela \xE9 onde a pessoa confirma ou ajusta \u2014 n\xE3o pergunte "posso gravar?" em texto. Se ela cancelar, pergunte o que mudar.`
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
async function saveStoreHours(args) {
|
|
1125
|
+
const found = hoursFromArgs(args);
|
|
1126
|
+
if (!found) return { ok: false, reason: "not_understood", message: "Nenhum dia aberto para gravar.", hint: HOURS_NOT_UNDERSTOOD };
|
|
1127
|
+
const result = await saveStoreSetting("hours", storeHoursOf(found.hours));
|
|
1128
|
+
if (!result.ok) return { ok: false, reason: result.reason, message: result.message };
|
|
1129
|
+
return { ok: true, saved: true, summary: formatHours(found.hours, HOURS_FORMAT_PT_BR), via: result.via };
|
|
1130
|
+
}
|
|
1131
|
+
var PAYMENT_METHOD_LABELS = PAYMENT_METHOD_LABELS_PT_BR.label;
|
|
1132
|
+
var PAYMENT_METHODS_CHOICE = "store-payment-methods";
|
|
1133
|
+
var SAVE_PAYMENT_METHODS_TOOL = "savePaymentMethods";
|
|
1134
|
+
var METHOD_WORDS = [
|
|
1135
|
+
[/\bpix\b/, "pix"],
|
|
1136
|
+
[/\bdinheiro\b|\bespecie\b|\bcash\b/, "cash"],
|
|
1137
|
+
[/\bcart(?:ao|oes)\b|\bmaquininha\b|\bcredito\b|\bdebito\b|\bcard\b/, "card"],
|
|
1138
|
+
[/\bvale\b|\bvr\b|\bva\b|\balelo\b|\bsodexo\b|\bticket\b|\brefeicao\b|\bmeal-voucher\b|\bvoucher\b/, "meal-voucher"],
|
|
1139
|
+
[/\bonline\b|\blink\b/, "online"]
|
|
1140
|
+
];
|
|
1141
|
+
function methodsFromArgs(args) {
|
|
1142
|
+
const raw = Array.isArray(args.methods) ? args.methods : typeof args.methods === "string" ? [args.methods] : [];
|
|
1143
|
+
const text2 = typeof args.text === "string" ? args.text : "";
|
|
1144
|
+
const picked = new Set(normalizePaymentMethods(raw));
|
|
1145
|
+
for (const item of raw) {
|
|
1146
|
+
if (typeof item !== "string" || picked.has(item)) continue;
|
|
1147
|
+
const hit = METHOD_WORDS.find(([re]) => re.test(stripAccents(item.toLowerCase())));
|
|
1148
|
+
if (hit) picked.add(hit[1]);
|
|
1149
|
+
}
|
|
1150
|
+
if (text2) {
|
|
1151
|
+
const plain = stripAccents(text2.toLowerCase());
|
|
1152
|
+
for (const [re, id] of METHOD_WORDS) if (re.test(plain)) picked.add(id);
|
|
1153
|
+
}
|
|
1154
|
+
return PAYMENT_METHOD_IDS.filter((id) => picked.has(id));
|
|
1155
|
+
}
|
|
1156
|
+
function describeMethods(ids) {
|
|
1157
|
+
const labels = ids.map((id) => PAYMENT_METHOD_LABELS[id]);
|
|
1158
|
+
if (labels.length <= 1) return labels[0] ?? "";
|
|
1159
|
+
return `${labels.slice(0, -1).join(", ")} e ${labels[labels.length - 1]}`;
|
|
1160
|
+
}
|
|
1161
|
+
async function savePaymentMethods(args) {
|
|
1162
|
+
const methods = methodsFromArgs(args);
|
|
1163
|
+
if (methods.length === 0) {
|
|
1164
|
+
return {
|
|
1165
|
+
ok: false,
|
|
1166
|
+
reason: "not_understood",
|
|
1167
|
+
message: "Nenhuma forma de pagamento reconhecida.",
|
|
1168
|
+
hint: `Pe\xE7a para marcar entre: ${Object.values(PAYMENT_METHOD_LABELS).join(", ")}.`
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const result = await saveStoreSetting("payment_methods", methods);
|
|
1172
|
+
if (!result.ok) return { ok: false, reason: result.reason, message: result.message };
|
|
1173
|
+
return { ok: true, saved: true, methods, summary: describeMethods(methods), via: result.via };
|
|
1174
|
+
}
|
|
1175
|
+
var SERVICE_MODE_LABELS = {
|
|
1176
|
+
counter: "Balc\xE3o e retirada",
|
|
1177
|
+
"own-delivery": "Entrega pr\xF3pria",
|
|
1178
|
+
"dine-in": "Sal\xE3o",
|
|
1179
|
+
aggregator: "Apps de entrega"
|
|
1180
|
+
};
|
|
1181
|
+
var SERVICE_MODES_CHOICE = "store-service-modes";
|
|
1182
|
+
var SAVE_PROFILE_TOOL = "saveStoreProfile";
|
|
1183
|
+
var STORE_PROFILE_SAVED_EVENT = "getting_started.store_profile_saved";
|
|
1184
|
+
async function saveStoreProfile(args) {
|
|
1185
|
+
const profile = storeProfileFrom(args);
|
|
1186
|
+
const patch = storeProfileToAnswers(profile);
|
|
1187
|
+
if (Object.keys(patch).length === 0) {
|
|
1188
|
+
return { ok: false, reason: "not_understood", message: "Nenhuma resposta reconhecida: use os ids do cat\xE1logo (serviceModes: counter, own-delivery, dine-in, aggregator; tables: lt10, 10-25, 25-50, gt50)." };
|
|
1189
|
+
}
|
|
1190
|
+
const org = useOrganizationStore.getState().currentOrg;
|
|
1191
|
+
if (!org) return { ok: false, reason: "no-tenant" };
|
|
1192
|
+
let result;
|
|
1193
|
+
try {
|
|
1194
|
+
result = await getStoreProfileProvider().save(org.id, profile, { businessName: org.name });
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
result = { ok: false, reason: "write-failed", message: e instanceof Error ? e.message : String(e) };
|
|
1197
|
+
}
|
|
1198
|
+
if (!result.ok) return { ok: false, reason: result.reason, message: result.message };
|
|
1199
|
+
emitDomainEvent(STORE_PROFILE_SAVED_EVENT, { keys: Object.keys(patch) }, { tenantId: org.id });
|
|
1200
|
+
const modes = profile.serviceModes ?? [];
|
|
1201
|
+
const summary = modes.length > 0 ? modes.map((m) => SERVICE_MODE_LABELS[m]).join(", ") : Object.keys(patch).join(", ");
|
|
1202
|
+
return { ok: true, saved: true, answers: patch, summary };
|
|
1203
|
+
}
|
|
1204
|
+
var gettingStartedAiTools = [
|
|
1205
|
+
{
|
|
1206
|
+
id: "getting-started.read-journey",
|
|
1207
|
+
name: "readTrialJourney",
|
|
1208
|
+
title: "Ver a configura\xE7\xE3o da loja",
|
|
1209
|
+
description: "Reads where the store is in its trial journey: the phase (setup, trial, expired, converted), the dates and which of the three minimums are green (menu with a product, opening hours, a completed test order). Read it before talking about the trial.",
|
|
1210
|
+
icon: "Rocket",
|
|
1211
|
+
mode: "read",
|
|
1212
|
+
category: "Getting started",
|
|
1213
|
+
execution: { plane: "client" },
|
|
1214
|
+
parameters: { type: "object", properties: {} },
|
|
1215
|
+
// The journey's openers (UX spec, moment 3). The plugin that leads the
|
|
1216
|
+
// journey composes the chips; each prompt is routed by the model to
|
|
1217
|
+
// whichever module owns the step, so no key of theirs is named here.
|
|
1218
|
+
suggestions: [
|
|
1219
|
+
{ label: "Configurar entrega", prompt: "Quero configurar a entrega da minha loja." },
|
|
1220
|
+
{ label: "Importar card\xE1pio", prompt: "Quero importar o meu card\xE1pio." },
|
|
1221
|
+
{ label: "Fazer pedido de teste", prompt: "Quero fazer um pedido de teste." },
|
|
1222
|
+
{ label: "Como est\xE1 a configura\xE7\xE3o da loja?" }
|
|
1223
|
+
]
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
id: "getting-started.propose-hours",
|
|
1227
|
+
name: PROPOSE_HOURS_TOOL,
|
|
1228
|
+
title: "Montar hor\xE1rio de funcionamento",
|
|
1229
|
+
description: 'HOR\xC1RIO DE FUNCIONAMENTO por conversa, fase 1: a escolha de um modelo (template: lunch, dinner, lunch-dinner, weekend) ou uma frase com dias e horas ("seg a sex 11h \xE0s 15h e 18h \xE0s 23h, s\xE1b at\xE9 meia-noite", "todo dia das 18 \xE0s 23", "fecha segunda") vira a semana. NUNCA grava e n\xE3o pergunta nada: devolve week (um item por dia aberto, com label) e summary. Passe text EXATAMENTE como veio. Em seguida chame saveStoreHours na mesma resposta.',
|
|
1230
|
+
icon: "Clock",
|
|
1231
|
+
mode: "read",
|
|
1232
|
+
category: "Getting started",
|
|
1233
|
+
execution: { plane: "client" },
|
|
1234
|
+
parameters: {
|
|
1235
|
+
type: "object",
|
|
1236
|
+
properties: {
|
|
1237
|
+
template: { type: "string", enum: [...HOURS_TEMPLATE_IDS], description: "Um dos quatro modelos, quando a pessoa escolheu um." },
|
|
1238
|
+
text: { type: "string", description: "A frase da pessoa, tal qual, quando ela disse a semana com palavras." },
|
|
1239
|
+
week: {
|
|
1240
|
+
type: "array",
|
|
1241
|
+
description: "Alternativa estruturada: um item por dia aberto.",
|
|
1242
|
+
items: {
|
|
1243
|
+
type: "object",
|
|
1244
|
+
properties: {
|
|
1245
|
+
day: { type: "string", enum: [...DAY_KEYS] },
|
|
1246
|
+
shifts: { type: "array", items: { type: "object", properties: { open: { type: "string" }, close: { type: "string" } }, required: ["open", "close"] } }
|
|
1247
|
+
},
|
|
1248
|
+
required: ["day", "shifts"]
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
suggestions: [{ label: "Revisar hor\xE1rio", prompt: "Quero ajustar o hor\xE1rio de funcionamento." }]
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
id: "getting-started.save-hours",
|
|
1257
|
+
name: SAVE_HOURS_TOOL,
|
|
1258
|
+
title: "Gravar hor\xE1rio de funcionamento",
|
|
1259
|
+
description: 'HOR\xC1RIO DE FUNCIONAMENTO, fase 2: grava a semana proposta em store.hours. Chame com o week e o summary que proposeStoreHours devolveu; o card nativo desta ferramenta \xE9 a \xDANICA confirma\xE7\xE3o \u2014 nunca pergunte "posso gravar?" em texto, nunca confirme duas vezes.',
|
|
1260
|
+
icon: "Clock",
|
|
1261
|
+
mode: "persist",
|
|
1262
|
+
category: "Getting started",
|
|
1263
|
+
requiresConfirmation: true,
|
|
1264
|
+
// Settled in the conversation already (a proposal accepted, a choice card
|
|
1265
|
+
// ticked) and undone by saying it again — a card would ask twice. Still a
|
|
1266
|
+
// write: the trace seals it and the owner can revoke it.
|
|
1267
|
+
autoApproveByDefault: true,
|
|
1268
|
+
// No `permission`: `settings` is no plugin's feature (see index.ts on the
|
|
1269
|
+
// trial tab); the server gates the write.
|
|
1270
|
+
execution: { plane: "client" },
|
|
1271
|
+
parameters: {
|
|
1272
|
+
type: "object",
|
|
1273
|
+
properties: {
|
|
1274
|
+
week: {
|
|
1275
|
+
type: "array",
|
|
1276
|
+
description: "Os dias abertos, como proposeStoreHours devolveu (day, label, shifts).",
|
|
1277
|
+
items: {
|
|
1278
|
+
type: "object",
|
|
1279
|
+
properties: {
|
|
1280
|
+
day: { type: "string", enum: [...DAY_KEYS] },
|
|
1281
|
+
label: { type: "string" },
|
|
1282
|
+
shifts: { type: "array", items: { type: "object", properties: { open: { type: "string" }, close: { type: "string" } }, required: ["open", "close"] } }
|
|
1283
|
+
},
|
|
1284
|
+
required: ["day", "shifts"]
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
summary: { type: "string", description: "A semana em uma linha, como proposeStoreHours devolveu." },
|
|
1288
|
+
template: { type: "string", enum: [...HOURS_TEMPLATE_IDS] }
|
|
1289
|
+
},
|
|
1290
|
+
required: ["week", "summary"]
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
id: "getting-started.save-payment-methods",
|
|
1295
|
+
name: SAVE_PAYMENT_METHODS_TOOL,
|
|
1296
|
+
title: "Gravar formas de pagamento",
|
|
1297
|
+
description: 'FORMAS DE PAGAMENTO por conversa: grava o que a casa aceita em store.payment_methods. A resposta do card "Como voc\xEA recebe?" ou qualquer frase com pix, dinheiro, cart\xE3o/maquininha, vale-refei\xE7\xE3o ou pagamento online \xE9 esta ferramenta (ids: pix, cash, card, meal-voucher, online). O card nativo \xE9 a \xFAnica confirma\xE7\xE3o.',
|
|
1298
|
+
icon: "Wallet",
|
|
1299
|
+
mode: "persist",
|
|
1300
|
+
category: "Getting started",
|
|
1301
|
+
requiresConfirmation: true,
|
|
1302
|
+
// Settled in the conversation already (a proposal accepted, a choice card
|
|
1303
|
+
// ticked) and undone by saying it again — a card would ask twice. Still a
|
|
1304
|
+
// write: the trace seals it and the owner can revoke it.
|
|
1305
|
+
autoApproveByDefault: true,
|
|
1306
|
+
execution: { plane: "client" },
|
|
1307
|
+
parameters: {
|
|
1308
|
+
type: "object",
|
|
1309
|
+
properties: {
|
|
1310
|
+
methods: { type: "array", items: { type: "string", enum: [...PAYMENT_METHOD_IDS] }, description: "As formas aceitas." },
|
|
1311
|
+
summary: { type: "string", description: 'As formas em uma linha, como a pessoa l\xEA: "Pix, Dinheiro e Cart\xE3o na maquininha".' }
|
|
1312
|
+
},
|
|
1313
|
+
required: ["methods", "summary"]
|
|
1314
|
+
},
|
|
1315
|
+
suggestions: [{ label: "Revisar pagamentos", prompt: "Quero revisar as formas de pagamento que aceito." }]
|
|
1316
|
+
},
|
|
1317
|
+
{
|
|
1318
|
+
id: "getting-started.save-profile",
|
|
1319
|
+
name: SAVE_PROFILE_TOOL,
|
|
1320
|
+
title: "Gravar perfil da casa",
|
|
1321
|
+
description: "PERFIL DA CASA por conversa: como as pessoas pedem (serviceModes: counter, own-delivery, dine-in, aggregator), quantas mesas (tables: lt10, 10-25, 25-50, gt50), quais apps de entrega (marketplaceApps), como cobra a entrega (deliveryMethod: radius, neighborhood), o tipo de casa (operation) e o artigo da sauda\xE7\xE3o (welcomeArticle: a, o). Grava uma nova resposta do question\xE1rio e religa o checklist (a entrega aparece quando a casa entrega). O card nativo \xE9 a \xFAnica confirma\xE7\xE3o.",
|
|
1322
|
+
icon: "Store",
|
|
1323
|
+
mode: "persist",
|
|
1324
|
+
category: "Getting started",
|
|
1325
|
+
requiresConfirmation: true,
|
|
1326
|
+
// Settled in the conversation already (a proposal accepted, a choice card
|
|
1327
|
+
// ticked) and undone by saying it again — a card would ask twice. Still a
|
|
1328
|
+
// write: the trace seals it and the owner can revoke it.
|
|
1329
|
+
autoApproveByDefault: true,
|
|
1330
|
+
execution: { plane: "client" },
|
|
1331
|
+
parameters: {
|
|
1332
|
+
type: "object",
|
|
1333
|
+
properties: {
|
|
1334
|
+
serviceModes: { type: "array", items: { type: "string", enum: [...SERVICE_MODES] } },
|
|
1335
|
+
tables: { type: "string", enum: [...TABLE_BANDS] },
|
|
1336
|
+
marketplaceApps: { type: "array", items: { type: "string" }, description: "ifood, rappi, aiqfome, uber-eats, 99food, keeta\u2026" },
|
|
1337
|
+
deliveryMethod: { type: "string", enum: [...DELIVERY_METHODS] },
|
|
1338
|
+
operation: { type: "string", enum: [...OPERATIONS] },
|
|
1339
|
+
welcomeArticle: { type: "string", enum: ["a", "o"] },
|
|
1340
|
+
summary: { type: "string", description: 'O perfil em uma linha, como a pessoa l\xEA: "Balc\xE3o, entrega pr\xF3pria e sal\xE3o com at\xE9 10 mesas".' }
|
|
1341
|
+
},
|
|
1342
|
+
required: ["summary"]
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
id: "getting-started.start-trial",
|
|
1347
|
+
name: "startTrial",
|
|
1348
|
+
title: "Iniciar o teste gr\xE1tis",
|
|
1349
|
+
description: "Starts the free trial. Only works when the three minimums are ready; otherwise it answers which ones are missing. The trial days start counting now and cannot be paused \u2014 confirm with the owner first.",
|
|
1350
|
+
icon: "Play",
|
|
1351
|
+
mode: "persist",
|
|
1352
|
+
category: "Getting started",
|
|
1353
|
+
requiresConfirmation: true,
|
|
1354
|
+
permission: { feature: "settings", action: "manage" },
|
|
1355
|
+
execution: { plane: "server", kind: "rpc", rpc: "agent_getting_started_start_trial" },
|
|
1356
|
+
parameters: { type: "object", properties: {} },
|
|
1357
|
+
suggestions: [{ label: "Iniciar teste" }]
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: "getting-started.request-contract",
|
|
1361
|
+
name: "requestContract",
|
|
1362
|
+
title: "Pedir para contratar",
|
|
1363
|
+
description: "Tells Faya the store wants to contract a plan; someone calls back within one business day. Accepted once a day per store.",
|
|
1364
|
+
icon: "PhoneCall",
|
|
1365
|
+
mode: "persist",
|
|
1366
|
+
category: "Getting started",
|
|
1367
|
+
requiresConfirmation: true,
|
|
1368
|
+
permission: { feature: "settings", action: "manage" },
|
|
1369
|
+
execution: { plane: "client" },
|
|
1370
|
+
parameters: {
|
|
1371
|
+
type: "object",
|
|
1372
|
+
properties: {
|
|
1373
|
+
note: { type: "string", description: "Anything the owner wants the sales team to know (optional, 500 chars)." }
|
|
1374
|
+
}
|
|
1375
|
+
},
|
|
1376
|
+
suggestions: [{ label: "Quero contratar" }]
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
id: "getting-started.seed-demo",
|
|
1380
|
+
name: "exploreWithDemoData",
|
|
1381
|
+
title: "Criar card\xE1pio de exemplo",
|
|
1382
|
+
description: 'Fills the menu with sample items marked as demo (removable in one click) so the store can see the product working before typing its own menu. Demo items never count as "menu ready".',
|
|
1383
|
+
icon: "Sparkles",
|
|
1384
|
+
mode: "persist",
|
|
1385
|
+
category: "Getting started",
|
|
1386
|
+
requiresConfirmation: true,
|
|
1387
|
+
execution: { plane: "client" },
|
|
1388
|
+
parameters: { type: "object", properties: {} },
|
|
1389
|
+
suggestions: [{ label: "Ver com dados de exemplo" }]
|
|
1390
|
+
}
|
|
1391
|
+
];
|
|
1392
|
+
var DEMO_TOOL = "seedDemoCatalog";
|
|
1393
|
+
function failure(e) {
|
|
1394
|
+
return {
|
|
1395
|
+
ok: false,
|
|
1396
|
+
reason: isForbidden(e) ? "forbidden" : "error",
|
|
1397
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function gettingStartedAiToolsFor(opts) {
|
|
1401
|
+
return opts?.profile === false ? gettingStartedAiTools.filter((t) => t.name !== SAVE_PROFILE_TOOL) : gettingStartedAiTools;
|
|
1402
|
+
}
|
|
1403
|
+
function registerGettingStartedToolHandlers(store, opts) {
|
|
1404
|
+
const handlers = {
|
|
1405
|
+
readTrialJourney: async () => {
|
|
1406
|
+
const journey = await store.getState().load() ?? store.getState().journey;
|
|
1407
|
+
return journey ? { ok: true, ...describeJourney(journey) } : { ok: false, reason: "no_journey" };
|
|
1408
|
+
},
|
|
1409
|
+
[PROPOSE_HOURS_TOOL]: (args) => proposeStoreHours(args),
|
|
1410
|
+
[SAVE_HOURS_TOOL]: (args) => saveStoreHours(args),
|
|
1411
|
+
[SAVE_PAYMENT_METHODS_TOOL]: (args) => savePaymentMethods(args),
|
|
1412
|
+
[SAVE_PROFILE_TOOL]: (args) => saveStoreProfile(args),
|
|
1413
|
+
startTrial: async () => {
|
|
1414
|
+
try {
|
|
1415
|
+
const result = await store.getState().startTrial();
|
|
1416
|
+
if (!result.ok) return { ...result, missing: missingMinimums(result.minimums).map((id) => MINIMUM_WORDS[id]) };
|
|
1417
|
+
return result;
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
return failure(e);
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
requestContract: async (args) => {
|
|
1423
|
+
const note = typeof args.note === "string" ? args.note.slice(0, 500) : void 0;
|
|
1424
|
+
try {
|
|
1425
|
+
return await store.getState().requestContract(note);
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
return failure(e);
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
// The catalogue belongs to plugin-menu (tech design §8); this tool only points at it.
|
|
1431
|
+
exploreWithDemoData: async () => {
|
|
1432
|
+
if (!hasAIToolHandler(DEMO_TOOL)) return { ok: false, reason: "menu_plugin_missing" };
|
|
1433
|
+
return { ok: true, next: DEMO_TOOL, hint: `Call ${DEMO_TOOL} to create the sample menu; call clearDemoCatalog to remove it.` };
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
if (opts?.profile === false) {
|
|
1437
|
+
delete handlers[SAVE_PROFILE_TOOL];
|
|
1438
|
+
unregisterAIToolHandler(SAVE_PROFILE_TOOL);
|
|
1439
|
+
}
|
|
1440
|
+
for (const [name, handler] of Object.entries(handlers)) registerAIToolHandler(name, handler);
|
|
1441
|
+
}
|
|
1442
|
+
var PROGRAM_ID = "getting-started";
|
|
1443
|
+
var PROGRAM_ORDER = 10;
|
|
1444
|
+
function readStoreSetting(settings, path) {
|
|
1445
|
+
let node = settings;
|
|
1446
|
+
for (const key2 of path) node = node?.[key2];
|
|
1447
|
+
return node;
|
|
1448
|
+
}
|
|
1449
|
+
function hasOpeningHours(settings) {
|
|
1450
|
+
const hours = readStoreSetting(settings, ["store", "hours"]);
|
|
1451
|
+
if (!hours || typeof hours !== "object" || Array.isArray(hours)) return false;
|
|
1452
|
+
return Object.values(hours).some(
|
|
1453
|
+
(day) => Array.isArray(day) ? day.length > 0 : !!day && typeof day === "object"
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
function hasPaymentMethods(settings) {
|
|
1457
|
+
const methods = readStoreSetting(settings, ["store", "payment_methods"]);
|
|
1458
|
+
return Array.isArray(methods) && methods.length > 0;
|
|
1459
|
+
}
|
|
1460
|
+
var currentSettings = loadStoreSettings;
|
|
1461
|
+
function chooseMany(move) {
|
|
1462
|
+
return { ...move, multi: true };
|
|
1463
|
+
}
|
|
1464
|
+
function storeSetupRouting(opts) {
|
|
1465
|
+
const profile = opts?.profile !== false;
|
|
1466
|
+
return [
|
|
1467
|
+
profile ? `ROTEAMENTO \u2014 hor\xE1rio, pagamentos e perfil se resolvem AQUI, por conversa, nunca mandando a pessoa para Configura\xE7\xF5es (a aba Teste gr\xE1tis \xE9 para revisar depois):` : `ROTEAMENTO \u2014 hor\xE1rio e pagamentos se resolvem AQUI, por conversa, nunca mandando a pessoa para Configura\xE7\xF5es (a aba Teste gr\xE1tis \xE9 para revisar depois):`,
|
|
1468
|
+
profile && `- Perfil da casa: a resposta do card "Como as pessoas pedem?" ou qualquer frase sobre balc\xE3o/retirada, entrega pr\xF3pria, sal\xE3o/mesas ou apps de entrega (iFood, Rappi, aiqfome\u2026) \xE9 ${SAVE_PROFILE_TOOL} (serviceModes com os ids counter, own-delivery, dine-in, aggregator). Se marcou sal\xE3o e n\xE3o disse quantas mesas, pergunte antes (tables: lt10, 10-25, 25-50, gt50); se marcou apps, pergunte quais (marketplaceApps). O card nativo \xE9 a \xFAnica confirma\xE7\xE3o. Depois que gravar, a entrega entra no checklist quando a casa entrega.`,
|
|
1469
|
+
`- Hor\xE1rio: a escolha de um modelo (Almo\xE7o, Jantar, Almo\xE7o e jantar, Fim de semana) ou qualquer frase com dias e horas ("seg a sex 11h \xE0s 15h e 18h \xE0s 23h", "todo dia das 18 \xE0s 23", "fecha segunda") \xE9 ${PROPOSE_HOURS_TOOL} \u2014 passe template ou text EXATAMENTE como veio. Ela n\xE3o grava: devolve week e summary. Comece a resposta pelo summary e, NA MESMA resposta, chame ${SAVE_HOURS_TOOL} com week e summary; o card nativo dela \xE9 a \xFAnica confirma\xE7\xE3o. "Eu digo" significa pedir a frase. Se a pessoa cancelar o card, pergunte o que mudar.`,
|
|
1470
|
+
`- Pagamentos: a resposta do card "Como voc\xEA recebe?" ou qualquer frase com pix, dinheiro, cart\xE3o/maquininha, vale-refei\xE7\xE3o ou pagamento online \xE9 ${SAVE_PAYMENT_METHODS_TOOL} (methods com os ids pix, cash, card, meal-voucher, online; summary em palavras).`,
|
|
1471
|
+
`- Nunca pergunte em texto "posso gravar?": a pergunta \xE9 o card.`
|
|
1472
|
+
].filter((line) => typeof line === "string").join("\n");
|
|
1473
|
+
}
|
|
1474
|
+
async function profileStepDone() {
|
|
1475
|
+
const tenantId = useOrganizationStore.getState().currentOrg?.id ?? getActiveTenantId();
|
|
1476
|
+
if (!tenantId) return false;
|
|
1477
|
+
try {
|
|
1478
|
+
return storeProfileAnswered(await getStoreProfileProvider().read(tenantId));
|
|
1479
|
+
} catch {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
function buildStoreLiveProgram(input = {}) {
|
|
1484
|
+
const { trialDays, verticals, trialRoute } = input.config ?? getActiveConfig();
|
|
1485
|
+
const profile = input.profile !== false;
|
|
1486
|
+
const program = {
|
|
1487
|
+
id: PROGRAM_ID,
|
|
1488
|
+
title: "Sua loja no ar",
|
|
1489
|
+
// Before the workspace base (50) — programs.ts orders by this.
|
|
1490
|
+
order: PROGRAM_ORDER,
|
|
1491
|
+
role: "setup",
|
|
1492
|
+
verticals,
|
|
1493
|
+
instructions: `Voc\xEA conduz a casa at\xE9 o primeiro pedido. Os m\xEDnimos para iniciar o teste s\xE3o: card\xE1pio com ao menos um produto, hor\xE1rio de funcionamento e um pedido de teste. Banco, WhatsApp e impressora N\xC3O travam nada \u2014 ofere\xE7a depois. Quando os tr\xEAs m\xEDnimos estiverem verdes, ofere\xE7a "Iniciar teste" (ferramenta startTrial) e explique que os ${trialDays} dias come\xE7am s\xF3 agora.
|
|
1494
|
+
${storeSetupRouting({ profile })}`,
|
|
1495
|
+
steps: [
|
|
1496
|
+
{
|
|
1497
|
+
key: STORE_PROFILE_STEP_KEY,
|
|
1498
|
+
title: "Como a casa atende",
|
|
1499
|
+
description: "Balc\xE3o, entrega pr\xF3pria, sal\xE3o, apps \u2014 \xE9 o que liga cada parte do sistema.",
|
|
1500
|
+
icon: "Store",
|
|
1501
|
+
priority: "high",
|
|
1502
|
+
journey: { group: "launch" },
|
|
1503
|
+
howTo: "Conte para a assistente como as pessoas pedem (balc\xE3o e retirada, entrega pr\xF3pria, sal\xE3o, apps de entrega), quantas mesas tem e por quais apps o pedido chega. Ela grava e liga s\xF3 o que a casa usa.",
|
|
1504
|
+
agentHint: `Pergunte como a casa atende e grave com ${SAVE_PROFILE_TOOL}; pergunte mesas e apps s\xF3 quando a resposta pedir. Nunca pergunte o que o cadastro j\xE1 respondeu.`,
|
|
1505
|
+
celebrate: "Perfil da casa salvo.",
|
|
1506
|
+
script: [
|
|
1507
|
+
{ kind: "say", text: "Primeiro, me conta como a casa atende \u2014 \xE9 isso que liga cada parte do sistema." },
|
|
1508
|
+
chooseMany({
|
|
1509
|
+
kind: "choose",
|
|
1510
|
+
id: SERVICE_MODES_CHOICE,
|
|
1511
|
+
question: "Como as pessoas pedem?",
|
|
1512
|
+
options: [
|
|
1513
|
+
{ id: "counter", label: "Balc\xE3o e retirada" },
|
|
1514
|
+
{ id: "own-delivery", label: "Entrega pr\xF3pria", meta: "Voc\xEA mesmo entrega" },
|
|
1515
|
+
{ id: "dine-in", label: "Sal\xE3o", meta: "Mesas e comandas" },
|
|
1516
|
+
{ id: "aggregator", label: "Apps de entrega", meta: "O pedido chega por eles" }
|
|
1517
|
+
]
|
|
1518
|
+
}),
|
|
1519
|
+
{ kind: "await" }
|
|
1520
|
+
],
|
|
1521
|
+
// The wizard used to answer this; now the conversation does, and the
|
|
1522
|
+
// profile store carries both — so an older tenant is born closed.
|
|
1523
|
+
check: profileStepDone
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
key: "hours",
|
|
1527
|
+
title: "Hor\xE1rio de funcionamento",
|
|
1528
|
+
description: "Quando a casa abre e fecha \u2014 o card\xE1pio digital mostra aberto/fechado por ele.",
|
|
1529
|
+
icon: "Clock",
|
|
1530
|
+
route: trialRoute,
|
|
1531
|
+
priority: "high",
|
|
1532
|
+
journey: { group: "launch", minimum: true },
|
|
1533
|
+
howTo: 'Diga \xE0 assistente ("seg a sex 11h \xE0s 15h e 18h \xE0s 23h") ou escolha um modelo no card e confirme. Para revisar depois: Configura\xE7\xF5es \u203A Teste gr\xE1tis \u203A Hor\xE1rios.',
|
|
1534
|
+
agentHint: `Ofere\xE7a os quatro modelos ou a frase; ${PROPOSE_HOURS_TOOL} monta a semana e ${SAVE_HOURS_TOOL} grava atr\xE1s do card nativo.`,
|
|
1535
|
+
celebrate: "Hor\xE1rio salvo \u23F0",
|
|
1536
|
+
script: [
|
|
1537
|
+
{ kind: "say", text: "Seus hor\xE1rios: que tal um modelo?" },
|
|
1538
|
+
{
|
|
1539
|
+
kind: "choose",
|
|
1540
|
+
id: HOURS_TEMPLATE_CHOICE,
|
|
1541
|
+
question: "Quando a casa abre?",
|
|
1542
|
+
options: [
|
|
1543
|
+
...HOURS_TEMPLATES.map((t) => ({ id: t.id, label: t.label, meta: t.hint })),
|
|
1544
|
+
{ id: HOURS_CUSTOM_OPTION, label: "Eu digo", meta: 'Escreva do seu jeito: "seg a sex 11h \xE0s 15h\u2026"' }
|
|
1545
|
+
]
|
|
1546
|
+
},
|
|
1547
|
+
{ kind: "await", text: "Pode mandar do seu jeito \u2014 eu monto a semana e voc\xEA confirma." }
|
|
1548
|
+
],
|
|
1549
|
+
// Born closed when the wizard answered: the sensor runs on the first mount (decision 4).
|
|
1550
|
+
check: async () => hasOpeningHours(await currentSettings())
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
key: "payment-methods",
|
|
1554
|
+
title: "Formas de pagamento",
|
|
1555
|
+
description: "Dinheiro, Pix, cart\xE3o \u2014 o que a casa aceita no balc\xE3o e na entrega.",
|
|
1556
|
+
icon: "Wallet",
|
|
1557
|
+
route: trialRoute,
|
|
1558
|
+
priority: "high",
|
|
1559
|
+
journey: { group: "launch" },
|
|
1560
|
+
howTo: "Marque no card o que a casa aceita e confirme; a maquininha e o Pix integrados ficam em Conectores, quando quiser. Para revisar depois: Configura\xE7\xF5es \u203A Teste gr\xE1tis \u203A Pagamentos.",
|
|
1561
|
+
agentHint: `A resposta do card ou a frase vira ${SAVE_PAYMENT_METHODS_TOOL}; o card nativo \xE9 a \xFAnica confirma\xE7\xE3o.`,
|
|
1562
|
+
celebrate: "Pagamentos definidos \u{1F4B3}",
|
|
1563
|
+
script: [
|
|
1564
|
+
chooseMany({
|
|
1565
|
+
kind: "choose",
|
|
1566
|
+
id: PAYMENT_METHODS_CHOICE,
|
|
1567
|
+
question: "Como voc\xEA recebe?",
|
|
1568
|
+
options: PAYMENT_METHOD_IDS.map((id) => ({ id, label: PAYMENT_METHOD_LABELS[id] }))
|
|
1569
|
+
}),
|
|
1570
|
+
{ kind: "await", text: "Marque o que voc\xEA aceita. D\xE1 para mudar depois." }
|
|
1571
|
+
],
|
|
1572
|
+
check: async () => hasPaymentMethods(await currentSettings())
|
|
1573
|
+
}
|
|
1574
|
+
]
|
|
1575
|
+
};
|
|
1576
|
+
return profile ? program : withoutStoreProfileStep(program);
|
|
1577
|
+
}
|
|
1578
|
+
function withoutStoreProfileStep(program) {
|
|
1579
|
+
if (program.id !== PROGRAM_ID) return program;
|
|
1580
|
+
const steps = program.steps.filter((s) => s.key !== STORE_PROFILE_STEP_KEY);
|
|
1581
|
+
if (steps.length === program.steps.length) return program;
|
|
1582
|
+
const instructions = program.instructions?.replace(storeSetupRouting(), storeSetupRouting({ profile: false }));
|
|
1583
|
+
return { ...program, steps, instructions };
|
|
1584
|
+
}
|
|
1585
|
+
function toTrialSource(input) {
|
|
1586
|
+
const { journey } = input;
|
|
1587
|
+
if (!journey) return null;
|
|
1588
|
+
return {
|
|
1589
|
+
phase: journey.phase,
|
|
1590
|
+
startedAt: journey.trialStartedAt ?? void 0,
|
|
1591
|
+
// The end as the server last told THIS plugin — the date the card and the
|
|
1592
|
+
// tab print. The shell's clock counts from it, not from its login-time copy.
|
|
1593
|
+
endsAt: journey.trialEndsAt ?? void 0,
|
|
1594
|
+
totalDays: journey.trialDays > 0 ? journey.trialDays : void 0,
|
|
1595
|
+
setupDeadlineAt: journey.setupDeadlineAt ?? void 0,
|
|
1596
|
+
readyCount: input.readyCount,
|
|
1597
|
+
readyTotal: input.readyTotal,
|
|
1598
|
+
onStart: input.onStart,
|
|
1599
|
+
onRequestContract: input.onRequestContract
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function publishTrialSource(input) {
|
|
1603
|
+
setTrialSource(toTrialSource(input));
|
|
1604
|
+
}
|
|
1605
|
+
function clearTrialSource() {
|
|
1606
|
+
setTrialSource(null);
|
|
1607
|
+
}
|
|
1608
|
+
function formatTrialDate(iso, locale = getCurrentLocale()) {
|
|
1609
|
+
if (!iso) return "";
|
|
1610
|
+
const date = new Date(iso);
|
|
1611
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
1612
|
+
try {
|
|
1613
|
+
return new Intl.DateTimeFormat(locale, { day: "numeric", month: "long" }).format(date);
|
|
1614
|
+
} catch {
|
|
1615
|
+
return date.toLocaleDateString();
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function ProgressRing({ value, max, size = 28, label, complete, onPrimary, className }) {
|
|
1619
|
+
const stroke = 3;
|
|
1620
|
+
const r = (size - stroke) / 2;
|
|
1621
|
+
const circumference = 2 * Math.PI * r;
|
|
1622
|
+
const pct = max > 0 ? Math.min(1, value / max) : 0;
|
|
1623
|
+
const textSize = size >= 40 ? "text-[11px]" : "text-[9px]";
|
|
1624
|
+
return /* @__PURE__ */ jsxs(
|
|
1625
|
+
"span",
|
|
1626
|
+
{
|
|
1627
|
+
role: "progressbar",
|
|
1628
|
+
"aria-valuemin": 0,
|
|
1629
|
+
"aria-valuemax": max,
|
|
1630
|
+
"aria-valuenow": value,
|
|
1631
|
+
"aria-label": label,
|
|
1632
|
+
className: cn("relative inline-flex shrink-0 items-center justify-center", className),
|
|
1633
|
+
style: { width: size, height: size },
|
|
1634
|
+
children: [
|
|
1635
|
+
/* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", className: "-rotate-90", children: [
|
|
1636
|
+
/* @__PURE__ */ jsx(
|
|
1637
|
+
"circle",
|
|
1638
|
+
{
|
|
1639
|
+
cx: size / 2,
|
|
1640
|
+
cy: size / 2,
|
|
1641
|
+
r,
|
|
1642
|
+
fill: "none",
|
|
1643
|
+
stroke: "currentColor",
|
|
1644
|
+
strokeWidth: stroke,
|
|
1645
|
+
className: onPrimary ? "text-primary-foreground/30" : "text-border"
|
|
1646
|
+
}
|
|
1647
|
+
),
|
|
1648
|
+
/* @__PURE__ */ jsx(
|
|
1649
|
+
"circle",
|
|
1650
|
+
{
|
|
1651
|
+
cx: size / 2,
|
|
1652
|
+
cy: size / 2,
|
|
1653
|
+
r,
|
|
1654
|
+
fill: "none",
|
|
1655
|
+
stroke: "currentColor",
|
|
1656
|
+
strokeWidth: stroke,
|
|
1657
|
+
strokeLinecap: "round",
|
|
1658
|
+
strokeDasharray: circumference,
|
|
1659
|
+
strokeDashoffset: circumference * (1 - pct),
|
|
1660
|
+
className: cn(
|
|
1661
|
+
"transition-[stroke-dashoffset] duration-300 ease-out motion-reduce:transition-none",
|
|
1662
|
+
onPrimary ? "text-primary-foreground" : complete ? "text-success" : "text-primary"
|
|
1663
|
+
)
|
|
1664
|
+
}
|
|
1665
|
+
)
|
|
1666
|
+
] }),
|
|
1667
|
+
/* @__PURE__ */ jsx("span", { className: cn("absolute font-mono font-medium leading-none tabular-nums", textSize, complete && !onPrimary && "text-success"), children: complete ? /* @__PURE__ */ jsx(Check, { className: size >= 40 ? "h-4 w-4" : "h-3 w-3", "aria-hidden": "true" }) : `${value}/${max}` })
|
|
1668
|
+
]
|
|
1669
|
+
}
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
var DAY_MS2 = 864e5;
|
|
1673
|
+
function useEnsureJourneyStandalone() {
|
|
1674
|
+
const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
1675
|
+
const load = useJourneyStore((s) => s.load);
|
|
1676
|
+
const known = useJourneyStore((s) => s.tenantId);
|
|
1677
|
+
const journey = useJourneyStore((s) => s.journey);
|
|
1678
|
+
const loading = useJourneyStore((s) => s.loading);
|
|
1679
|
+
const error = useJourneyStore((s) => s.error);
|
|
1680
|
+
React5.useEffect(() => {
|
|
1681
|
+
if (!tenantId) return;
|
|
1682
|
+
if (tenantId === known && (journey || loading || error)) return;
|
|
1683
|
+
void load(tenantId);
|
|
1684
|
+
}, [tenantId, known, journey, loading, error, load]);
|
|
1685
|
+
}
|
|
1686
|
+
function useStartTrial() {
|
|
1687
|
+
useEnsureJourneyStandalone();
|
|
1688
|
+
const t = useTranslation();
|
|
1689
|
+
const config = getActiveConfig();
|
|
1690
|
+
const declaration = useJourneyDeclaration();
|
|
1691
|
+
const loaded = useJourneyStore((s) => s.journey);
|
|
1692
|
+
const journeyError = useJourneyStore((s) => s.error);
|
|
1693
|
+
const starting = useJourneyStore((s) => s.starting);
|
|
1694
|
+
const startTrial = useJourneyStore((s) => s.startTrial);
|
|
1695
|
+
const load = useJourneyStore((s) => s.load);
|
|
1696
|
+
const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
1697
|
+
const rows = useActivationStore((s) => s.rows);
|
|
1698
|
+
const catalog = useActivationStore((s) => s.catalog);
|
|
1699
|
+
const [notice, setNotice] = React5.useState(null);
|
|
1700
|
+
const { allowed, markProps } = useSdkPermission(SDK_PERMISSIONS.trialManage);
|
|
1701
|
+
React5.useEffect(() => {
|
|
1702
|
+
noteTrialDaysDivergence(config, loaded?.trialDays);
|
|
1703
|
+
}, [config, loaded?.trialDays]);
|
|
1704
|
+
const journey = React5.useMemo(
|
|
1705
|
+
() => loaded ?? (journeyError && tenantId && declaration.declared ? clientJourney({ tenantId, trialDays: config.trialDays, rows, declaration }) : null),
|
|
1706
|
+
[loaded, journeyError, tenantId, declaration, config.trialDays, rows]
|
|
1707
|
+
);
|
|
1708
|
+
const minimums = React5.useMemo(() => countMinimums(journey, rows, declaration), [journey, rows, declaration]);
|
|
1709
|
+
const lines = React5.useMemo(() => minimumLines(catalog, rows, journey, declaration), [catalog, rows, journey, declaration]);
|
|
1710
|
+
const doneSignature = minimums.done.join("|");
|
|
1711
|
+
const first = React5.useRef(true);
|
|
1712
|
+
React5.useEffect(() => {
|
|
1713
|
+
if (first.current) {
|
|
1714
|
+
first.current = false;
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
void load();
|
|
1718
|
+
}, [doneSignature, load]);
|
|
1719
|
+
const ready = !!journey && (journey.minimums.ready || minimums.ready);
|
|
1720
|
+
const trialDays = journey?.trialDays || config.trialDays;
|
|
1721
|
+
const available = !!journey && journey.phase === "setup" && !journey.trialStartedAt;
|
|
1722
|
+
const missing = React5.useMemo(() => {
|
|
1723
|
+
if (ready) return null;
|
|
1724
|
+
const line = lines.find((l) => !l.done);
|
|
1725
|
+
if (!line) return null;
|
|
1726
|
+
const title = line.step?.title ?? (line.slot ? t(`gettingStarted.settings.minimums.${line.slot}`) : line.key);
|
|
1727
|
+
const what = line.slot ? t(`gettingStarted.minimum.${line.slot}`) : title;
|
|
1728
|
+
return { key: line.key, title, text: t("gettingStarted.action.missing", { what }), route: line.step?.route };
|
|
1729
|
+
}, [ready, lines, t]);
|
|
1730
|
+
const start = React5.useCallback(async () => {
|
|
1731
|
+
setNotice(null);
|
|
1732
|
+
let result;
|
|
1733
|
+
try {
|
|
1734
|
+
result = await startTrial();
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
setNotice(isMissingClock(e) ? t("gettingStarted.start.noClock") : t("gettingStarted.start.error"));
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
if (result.ok) {
|
|
1740
|
+
toast.success(t("gettingStarted.start.done", { date: formatTrialDate(result.trialEndsAt) }));
|
|
1741
|
+
return result;
|
|
1742
|
+
}
|
|
1743
|
+
const still = ["menu", "hours", "testOrder"].filter((id) => !result.minimums[id]);
|
|
1744
|
+
const what = still.map((id) => t(`gettingStarted.minimum.${id}`)).join(t("gettingStarted.minimum.join"));
|
|
1745
|
+
setNotice(t("gettingStarted.start.missing", { what }));
|
|
1746
|
+
return result;
|
|
1747
|
+
}, [startTrial, t]);
|
|
1748
|
+
return {
|
|
1749
|
+
available,
|
|
1750
|
+
ready,
|
|
1751
|
+
starting,
|
|
1752
|
+
trialDays,
|
|
1753
|
+
label: t("gettingStarted.cta.ready", { days: trialDays }),
|
|
1754
|
+
projectedEnd: formatTrialDate(new Date(Date.now() + trialDays * DAY_MS2).toISOString()),
|
|
1755
|
+
journey,
|
|
1756
|
+
minimums,
|
|
1757
|
+
lines,
|
|
1758
|
+
missing,
|
|
1759
|
+
notice,
|
|
1760
|
+
start,
|
|
1761
|
+
allowed,
|
|
1762
|
+
markProps
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
function StartTrialAction({ onStarted, onNavigate, confirm = "inline", className }) {
|
|
1766
|
+
const t = useTranslation();
|
|
1767
|
+
const state = useStartTrial();
|
|
1768
|
+
const [confirming, setConfirming] = React5.useState(false);
|
|
1769
|
+
if (!state.available) return null;
|
|
1770
|
+
const go = (route2) => {
|
|
1771
|
+
if (onNavigate?.(route2) === false) return;
|
|
1772
|
+
adminNavigateTo(route2);
|
|
1773
|
+
};
|
|
1774
|
+
const run = async () => {
|
|
1775
|
+
const result = await state.start();
|
|
1776
|
+
setConfirming(false);
|
|
1777
|
+
if (result?.ok) onStarted?.(result);
|
|
1778
|
+
};
|
|
1779
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2", className), "data-tour": "getting-started.start-trial", children: [
|
|
1780
|
+
state.missing && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: state.missing.text }),
|
|
1781
|
+
state.notice && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive", children: state.notice }),
|
|
1782
|
+
confirming && /* @__PURE__ */ jsx("p", { role: "status", className: "text-sm text-foreground", children: t("gettingStarted.confirm.body", { date: state.projectedEnd }) }),
|
|
1783
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", children: state.missing ? state.missing.route ? /* @__PURE__ */ jsx(Button, { type: "button", onClick: () => go(state.missing.route), className: "min-h-[44px]", children: state.missing.title }) : null : confirming ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1784
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => setConfirming(false), className: "min-h-[44px]", children: t("gettingStarted.confirm.cancel") }),
|
|
1785
|
+
/* @__PURE__ */ jsx(Button, { type: "button", onClick: () => {
|
|
1786
|
+
void run();
|
|
1787
|
+
}, disabled: state.starting || !state.allowed, ...state.markProps, className: "min-h-[44px]", children: state.starting ? t("gettingStarted.confirm.starting") : t("gettingStarted.confirm.ok") })
|
|
1788
|
+
] }) : /* @__PURE__ */ jsx(
|
|
1789
|
+
Button,
|
|
1790
|
+
{
|
|
1791
|
+
type: "button",
|
|
1792
|
+
onClick: () => confirm === "inline" ? setConfirming(true) : void run(),
|
|
1793
|
+
disabled: state.starting || !state.allowed,
|
|
1794
|
+
...state.markProps,
|
|
1795
|
+
className: "min-h-[44px]",
|
|
1796
|
+
children: state.starting ? t("gettingStarted.confirm.starting") : state.label
|
|
1797
|
+
}
|
|
1798
|
+
) })
|
|
1799
|
+
] });
|
|
1800
|
+
}
|
|
1801
|
+
var MINIMIZED_KEY = "fayz:getting-started:cta-minimized";
|
|
1802
|
+
function countChecklist(catalog, rows, declaration) {
|
|
1803
|
+
if (!catalog.length) return { done: 0, total: 0 };
|
|
1804
|
+
return countJourney(catalog, rows, declaration);
|
|
1805
|
+
}
|
|
1806
|
+
function useMatchMedia(query) {
|
|
1807
|
+
const [matches, setMatches] = React5.useState(() => typeof window !== "undefined" && window.matchMedia?.(query).matches);
|
|
1808
|
+
React5.useEffect(() => {
|
|
1809
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
1810
|
+
const mq = window.matchMedia(query);
|
|
1811
|
+
const on = () => setMatches(mq.matches);
|
|
1812
|
+
on();
|
|
1813
|
+
mq.addEventListener("change", on);
|
|
1814
|
+
return () => mq.removeEventListener("change", on);
|
|
1815
|
+
}, [query]);
|
|
1816
|
+
return !!matches;
|
|
1817
|
+
}
|
|
1818
|
+
function useContentLeft() {
|
|
1819
|
+
const [left, setLeft] = React5.useState(0);
|
|
1820
|
+
React5.useEffect(() => {
|
|
1821
|
+
if (typeof document === "undefined") return;
|
|
1822
|
+
const measure = () => setLeft(document.querySelector("main")?.getBoundingClientRect().left ?? 0);
|
|
1823
|
+
measure();
|
|
1824
|
+
window.addEventListener("resize", measure);
|
|
1825
|
+
const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
|
|
1826
|
+
observer?.observe(document.body);
|
|
1827
|
+
return () => {
|
|
1828
|
+
window.removeEventListener("resize", measure);
|
|
1829
|
+
observer?.disconnect();
|
|
1830
|
+
};
|
|
1831
|
+
}, []);
|
|
1832
|
+
return left;
|
|
1833
|
+
}
|
|
1834
|
+
function useMinimized() {
|
|
1835
|
+
const [minimized, setState] = React5.useState(() => {
|
|
1836
|
+
try {
|
|
1837
|
+
return globalThis.localStorage?.getItem(MINIMIZED_KEY) === "1";
|
|
1838
|
+
} catch {
|
|
1839
|
+
return false;
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
const set = React5.useCallback((next) => {
|
|
1843
|
+
setState(next);
|
|
1844
|
+
try {
|
|
1845
|
+
globalThis.localStorage?.setItem(MINIMIZED_KEY, next ? "1" : "0");
|
|
1846
|
+
} catch {
|
|
1847
|
+
}
|
|
1848
|
+
}, []);
|
|
1849
|
+
return [minimized, set];
|
|
1850
|
+
}
|
|
1851
|
+
function StartTrialCta() {
|
|
1852
|
+
useEnsureJourney();
|
|
1853
|
+
const trial = useStartTrial();
|
|
1854
|
+
const declaration = useJourneyDeclaration();
|
|
1855
|
+
const loaded = useGettingStartedStore((s) => s.journey);
|
|
1856
|
+
const requestContract = useGettingStartedStore((s) => s.requestContract);
|
|
1857
|
+
const rows = useActivationStore((s) => s.rows);
|
|
1858
|
+
const catalog = useActivationStore((s) => s.catalog);
|
|
1859
|
+
const checklist = React5.useMemo(() => countChecklist(catalog, rows, declaration), [catalog, rows, declaration]);
|
|
1860
|
+
const { start, minimums, allowed } = trial;
|
|
1861
|
+
React5.useEffect(() => {
|
|
1862
|
+
publishTrialSource({
|
|
1863
|
+
journey: loaded,
|
|
1864
|
+
readyCount: minimums.count,
|
|
1865
|
+
readyTotal: minimums.total,
|
|
1866
|
+
// A role the host does not let start the trial gets the clock without the buttons.
|
|
1867
|
+
onStart: allowed ? async () => {
|
|
1868
|
+
await start();
|
|
1869
|
+
} : void 0,
|
|
1870
|
+
onRequestContract: allowed ? async () => {
|
|
1871
|
+
await requestContract();
|
|
1872
|
+
} : void 0
|
|
1873
|
+
});
|
|
1874
|
+
}, [loaded, minimums.count, minimums.total, start, requestContract, allowed]);
|
|
1875
|
+
React5.useEffect(() => () => clearTrialSource(), []);
|
|
1876
|
+
if (!trial.available) return null;
|
|
1877
|
+
if (typeof document === "undefined") return null;
|
|
1878
|
+
return createPortal(
|
|
1879
|
+
/* @__PURE__ */ jsx(FloatingCta, { trial, checklist, catalog, rows, declaration }),
|
|
1880
|
+
document.body
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
function FloatingCta({ trial, checklist, catalog, rows, declaration }) {
|
|
1884
|
+
const t = useTranslation();
|
|
1885
|
+
const { ready, trialDays, minimums, starting, notice, projectedEnd } = trial;
|
|
1886
|
+
const mobile = useMatchMedia("(max-width: 767px)");
|
|
1887
|
+
const contentLeft = useContentLeft();
|
|
1888
|
+
const [minimized, setMinimized] = useMinimized();
|
|
1889
|
+
const [listOpen, setListOpen] = React5.useState(false);
|
|
1890
|
+
const [confirmOpen, setConfirmOpen] = React5.useState(false);
|
|
1891
|
+
const pillRef = React5.useRef(null);
|
|
1892
|
+
const ringDone = checklist.total ? checklist.done : minimums.count;
|
|
1893
|
+
const ringTotal = checklist.total || minimums.total;
|
|
1894
|
+
const ringLabel = t("gettingStarted.cta.ring", { done: ringDone, total: ringTotal });
|
|
1895
|
+
const remaining = minimums.total - minimums.count;
|
|
1896
|
+
const remainingText = remaining === 1 ? t("gettingStarted.cta.remaining.one") : t("gettingStarted.cta.remaining.many", { count: remaining });
|
|
1897
|
+
const readyText = t("gettingStarted.cta.ready", { days: trialDays });
|
|
1898
|
+
const pillLabel = ready ? readyText : `${t("gettingStarted.cta.title")} \xB7 ${remainingText}`;
|
|
1899
|
+
const openList = () => {
|
|
1900
|
+
const rail = useRightRailStore.getState();
|
|
1901
|
+
if (rail.mounted && rail.panels.some((p) => p.id === "tasks")) rail.openPanel("tasks");
|
|
1902
|
+
else setListOpen(true);
|
|
1903
|
+
};
|
|
1904
|
+
const handleStart = async () => {
|
|
1905
|
+
const result = await trial.start();
|
|
1906
|
+
setConfirmOpen(false);
|
|
1907
|
+
if (result?.ok) {
|
|
1908
|
+
const rect = pillRef.current?.getBoundingClientRect();
|
|
1909
|
+
fireConfetti(rect ? { origin: { x: (rect.left + rect.width / 2) / window.innerWidth, y: rect.top / window.innerHeight } } : void 0);
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
const pillClass = cn(
|
|
1913
|
+
"group inline-flex h-11 items-center gap-2.5 rounded-full border text-sm font-medium shadow-md transition-colors",
|
|
1914
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
1915
|
+
ready ? "border-primary bg-primary text-primary-foreground hover:bg-primary/90" : "border-border bg-card text-foreground hover:bg-muted",
|
|
1916
|
+
mobile ? "w-full justify-between rounded-none border-x-0 px-4" : "px-3 pr-4"
|
|
1917
|
+
);
|
|
1918
|
+
const pill = /* @__PURE__ */ jsxs(
|
|
1919
|
+
"button",
|
|
1920
|
+
{
|
|
1921
|
+
ref: pillRef,
|
|
1922
|
+
type: "button",
|
|
1923
|
+
"aria-label": pillLabel,
|
|
1924
|
+
"aria-haspopup": ready ? "dialog" : "dialog",
|
|
1925
|
+
"aria-expanded": ready ? confirmOpen : listOpen,
|
|
1926
|
+
"data-tour": "getting-started.cta",
|
|
1927
|
+
onClick: () => ready && trial.allowed ? setConfirmOpen(true) : openList(),
|
|
1928
|
+
...ready ? trial.markProps : {},
|
|
1929
|
+
className: pillClass,
|
|
1930
|
+
children: [
|
|
1931
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-2.5", children: [
|
|
1932
|
+
/* @__PURE__ */ jsx(ProgressRing, { value: ringDone, max: ringTotal, label: ringLabel, complete: ready, onPrimary: ready }),
|
|
1933
|
+
ready ? /* @__PURE__ */ jsx("span", { className: "truncate", children: readyText }) : /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
|
|
1934
|
+
/* @__PURE__ */ jsx("span", { children: t("gettingStarted.cta.title") }),
|
|
1935
|
+
/* @__PURE__ */ jsx("span", { className: "mx-1.5 text-muted-foreground", "aria-hidden": "true", children: "\xB7" }),
|
|
1936
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: remainingText })
|
|
1937
|
+
] })
|
|
1938
|
+
] }),
|
|
1939
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: cn("h-4 w-4 shrink-0", ready ? "text-primary-foreground/80" : "text-muted-foreground"), "aria-hidden": "true" })
|
|
1940
|
+
]
|
|
1941
|
+
}
|
|
1942
|
+
);
|
|
1943
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1944
|
+
/* @__PURE__ */ jsxs(
|
|
1945
|
+
"div",
|
|
1946
|
+
{
|
|
1947
|
+
className: cn(
|
|
1948
|
+
"pointer-events-none fixed z-40 flex flex-col items-start gap-2",
|
|
1949
|
+
// Above the tab bar AND the mascot button that rises out of it: at
|
|
1950
|
+
// 3.5rem the pill's text sat right under the mascot.
|
|
1951
|
+
mobile ? "inset-x-0 bottom-[calc(env(safe-area-inset-bottom)+6.25rem)]" : "bottom-6"
|
|
1952
|
+
),
|
|
1953
|
+
style: mobile ? void 0 : { left: contentLeft + 24 },
|
|
1954
|
+
children: [
|
|
1955
|
+
notice && /* @__PURE__ */ jsx("p", { role: "status", className: "pointer-events-auto mx-4 max-w-sm rounded-card border border-border bg-card px-3 py-2 text-xs text-foreground shadow-sm md:mx-0", children: notice }),
|
|
1956
|
+
mobile && minimized ? /* @__PURE__ */ jsx(
|
|
1957
|
+
Button,
|
|
1958
|
+
{
|
|
1959
|
+
type: "button",
|
|
1960
|
+
variant: "bare",
|
|
1961
|
+
size: "bare",
|
|
1962
|
+
"aria-label": t("gettingStarted.cta.expand"),
|
|
1963
|
+
onClick: () => setMinimized(false),
|
|
1964
|
+
className: "pointer-events-auto ml-4 inline-flex h-11 w-11 items-center justify-center rounded-full border border-border bg-card shadow-md",
|
|
1965
|
+
children: /* @__PURE__ */ jsx(ProgressRing, { value: ringDone, max: ringTotal, size: 20, label: ringLabel, complete: ready })
|
|
1966
|
+
}
|
|
1967
|
+
) : /* @__PURE__ */ jsxs("div", { className: cn("pointer-events-auto flex items-center", mobile && "w-full bg-card"), children: [
|
|
1968
|
+
pill,
|
|
1969
|
+
mobile && /* @__PURE__ */ jsx(
|
|
1970
|
+
Button,
|
|
1971
|
+
{
|
|
1972
|
+
type: "button",
|
|
1973
|
+
variant: "bare",
|
|
1974
|
+
size: "bare",
|
|
1975
|
+
"aria-label": t("gettingStarted.cta.minimize"),
|
|
1976
|
+
onClick: () => setMinimized(true),
|
|
1977
|
+
className: cn("inline-flex h-11 w-11 shrink-0 items-center justify-center", ready ? "bg-primary text-primary-foreground" : "text-muted-foreground"),
|
|
1978
|
+
children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4", "aria-hidden": "true" })
|
|
1979
|
+
}
|
|
1980
|
+
)
|
|
1981
|
+
] })
|
|
1982
|
+
]
|
|
1983
|
+
}
|
|
1984
|
+
),
|
|
1985
|
+
listOpen && mobile && /* @__PURE__ */ jsx(BottomSheet, { title: t("gettingStarted.cta.title"), onClose: () => setListOpen(false), children: /* @__PURE__ */ jsx(StepList, { catalog, rows, lines: trial.lines, declaration, onNavigate: () => setListOpen(false) }) }),
|
|
1986
|
+
!mobile && /* @__PURE__ */ jsx(Sheet, { open: listOpen, onOpenChange: setListOpen, children: /* @__PURE__ */ jsxs(SheetContent, { children: [
|
|
1987
|
+
/* @__PURE__ */ jsx(SheetHeader, { children: /* @__PURE__ */ jsxs(SheetTitle, { className: "flex items-center gap-2", children: [
|
|
1988
|
+
/* @__PURE__ */ jsx(Rocket, { className: "h-4 w-4 text-primary", "aria-hidden": "true" }),
|
|
1989
|
+
t("gettingStarted.cta.title")
|
|
1990
|
+
] }) }),
|
|
1991
|
+
/* @__PURE__ */ jsx(StepList, { catalog, rows, lines: trial.lines, declaration, onNavigate: () => setListOpen(false) })
|
|
1992
|
+
] }) }),
|
|
1993
|
+
/* @__PURE__ */ jsx(
|
|
1994
|
+
ConfirmDialog,
|
|
1995
|
+
{
|
|
1996
|
+
open: confirmOpen,
|
|
1997
|
+
title: t("gettingStarted.confirm.title", { days: trialDays }),
|
|
1998
|
+
description: t("gettingStarted.confirm.body", { date: projectedEnd }),
|
|
1999
|
+
confirmLabel: starting ? t("gettingStarted.confirm.starting") : t("gettingStarted.confirm.ok"),
|
|
2000
|
+
cancelLabel: t("gettingStarted.confirm.cancel"),
|
|
2001
|
+
loading: starting,
|
|
2002
|
+
onConfirm: () => {
|
|
2003
|
+
void handleStart();
|
|
2004
|
+
},
|
|
2005
|
+
onCancel: () => setConfirmOpen(false)
|
|
2006
|
+
}
|
|
2007
|
+
)
|
|
2008
|
+
] });
|
|
2009
|
+
}
|
|
2010
|
+
function StepList({ catalog, rows, lines, declaration, onNavigate }) {
|
|
2011
|
+
const t = useTranslation();
|
|
2012
|
+
const journeySteps2 = React5.useMemo(() => {
|
|
2013
|
+
const { launch, later } = selectJourneySteps(catalog, declaration);
|
|
2014
|
+
return [...launch, ...later];
|
|
2015
|
+
}, [catalog, declaration]);
|
|
2016
|
+
const minimumKeys = new Set(lines.map((l) => l.key));
|
|
2017
|
+
const minimumSteps = lines.map((l) => ({
|
|
2018
|
+
key: l.key,
|
|
2019
|
+
title: l.step?.title ?? (l.slot ? t(`gettingStarted.settings.minimums.${l.slot}`) : l.key),
|
|
2020
|
+
route: l.step?.route,
|
|
2021
|
+
done: l.done
|
|
2022
|
+
}));
|
|
2023
|
+
const laterSteps = journeySteps2.filter((s) => !minimumKeys.has(s.key)).map((s) => ({ key: s.key, title: s.title, route: s.route, done: rowDone(rows, s.key) }));
|
|
2024
|
+
const firstOpen = [...minimumSteps, ...laterSteps].find((s) => !s.done)?.key;
|
|
2025
|
+
const row = (step) => {
|
|
2026
|
+
const now = step.key === firstOpen;
|
|
2027
|
+
const state = step.done ? t("gettingStarted.cta.sheet.done") : now ? t("gettingStarted.cta.sheet.now") : t("gettingStarted.cta.sheet.open");
|
|
2028
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
2029
|
+
"button",
|
|
2030
|
+
{
|
|
2031
|
+
type: "button",
|
|
2032
|
+
"aria-label": `${step.title}, ${state}`,
|
|
2033
|
+
onClick: () => {
|
|
2034
|
+
if (step.route) adminNavigateTo(step.route);
|
|
2035
|
+
onNavigate();
|
|
2036
|
+
},
|
|
2037
|
+
className: cn(
|
|
2038
|
+
"flex min-h-[44px] w-full items-center gap-3 rounded-button px-2 text-left text-sm transition-colors hover:bg-muted",
|
|
2039
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
2040
|
+
now && "bg-accent"
|
|
2041
|
+
),
|
|
2042
|
+
children: [
|
|
2043
|
+
step.done ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0 text-success", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Circle, { className: cn("h-3 w-3 shrink-0", now ? "fill-primary text-primary" : "text-muted-foreground"), "aria-hidden": "true" }),
|
|
2044
|
+
/* @__PURE__ */ jsx("span", { className: cn("min-w-0 flex-1 truncate", step.done && "text-muted-foreground line-through"), children: step.title }),
|
|
2045
|
+
now && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary-foreground", children: t("gettingStarted.cta.sheet.now") }),
|
|
2046
|
+
step.route && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5 text-xs text-muted-foreground", children: [
|
|
2047
|
+
step.done ? t("gettingStarted.cta.sheet.review") : t("gettingStarted.cta.sheet.configure"),
|
|
2048
|
+
/* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
|
|
2049
|
+
] })
|
|
2050
|
+
]
|
|
2051
|
+
}
|
|
2052
|
+
) }, step.key);
|
|
2053
|
+
};
|
|
2054
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4 py-2", children: [
|
|
2055
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: t("gettingStarted.cta.ring", { done: minimumSteps.filter((s) => s.done).length + laterSteps.filter((s) => s.done).length, total: minimumSteps.length + laterSteps.length }) }),
|
|
2056
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2057
|
+
/* @__PURE__ */ jsx("h4", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t("gettingStarted.cta.sheet.minimums") }),
|
|
2058
|
+
/* @__PURE__ */ jsx("ol", { className: "space-y-0.5", children: minimumSteps.map(row) })
|
|
2059
|
+
] }),
|
|
2060
|
+
laterSteps.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
2061
|
+
/* @__PURE__ */ jsx("h4", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t("gettingStarted.cta.sheet.later") }),
|
|
2062
|
+
/* @__PURE__ */ jsx("ol", { className: "space-y-0.5", children: laterSteps.map(row) })
|
|
2063
|
+
] })
|
|
2064
|
+
] });
|
|
2065
|
+
}
|
|
2066
|
+
var HOUR_MS = 36e5;
|
|
2067
|
+
var DAY_MS3 = 864e5;
|
|
2068
|
+
function trialEndsToday(trialEndsAt, now = /* @__PURE__ */ new Date()) {
|
|
2069
|
+
if (!trialEndsAt) return false;
|
|
2070
|
+
const ends = Date.parse(trialEndsAt);
|
|
2071
|
+
if (Number.isNaN(ends)) return false;
|
|
2072
|
+
const remaining = ends - now.getTime();
|
|
2073
|
+
return remaining > 0 && remaining < DAY_MS3;
|
|
2074
|
+
}
|
|
2075
|
+
function formatEndTime(iso, locale = getCurrentLocale()) {
|
|
2076
|
+
if (!iso) return "";
|
|
2077
|
+
const date = new Date(iso);
|
|
2078
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
2079
|
+
try {
|
|
2080
|
+
return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).format(date);
|
|
2081
|
+
} catch {
|
|
2082
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
var D0_DISMISS_KEY = "fayz:getting-started:d0-dismissed-until";
|
|
2086
|
+
var D0_DISMISS_HOURS = 8;
|
|
2087
|
+
function isD0Dismissed(now = /* @__PURE__ */ new Date(), storage = safeStorage()) {
|
|
2088
|
+
try {
|
|
2089
|
+
const until = Number(storage?.getItem(D0_DISMISS_KEY) ?? 0);
|
|
2090
|
+
return Number.isFinite(until) && until > now.getTime();
|
|
2091
|
+
} catch {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function dismissD0(now = /* @__PURE__ */ new Date(), storage = safeStorage()) {
|
|
2096
|
+
try {
|
|
2097
|
+
storage?.setItem(D0_DISMISS_KEY, String(now.getTime() + D0_DISMISS_HOURS * HOUR_MS));
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function safeStorage() {
|
|
2102
|
+
try {
|
|
2103
|
+
return globalThis.localStorage;
|
|
2104
|
+
} catch {
|
|
2105
|
+
return void 0;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function TrialEndBanner() {
|
|
2109
|
+
useEnsureJourney();
|
|
2110
|
+
const t = useTranslation();
|
|
2111
|
+
const config = useGettingStartedConfig();
|
|
2112
|
+
const journey = useGettingStartedStore((s) => s.journey);
|
|
2113
|
+
const requestContract = useGettingStartedStore((s) => s.requestContract);
|
|
2114
|
+
const { allowed, markProps } = useSdkPermission(SDK_PERMISSIONS.trialManage);
|
|
2115
|
+
const [mode, setMode] = React5.useState("idle");
|
|
2116
|
+
const [busy, setBusy] = React5.useState(false);
|
|
2117
|
+
const [phone, setPhone] = React5.useState("");
|
|
2118
|
+
const [notice, setNotice] = React5.useState(null);
|
|
2119
|
+
const [d0Hidden, setD0Hidden] = React5.useState(() => isD0Dismissed());
|
|
2120
|
+
const requestedAt = journey?.wantsContractAt;
|
|
2121
|
+
React5.useEffect(() => {
|
|
2122
|
+
if (requestedAt) setMode("done");
|
|
2123
|
+
}, [requestedAt]);
|
|
2124
|
+
if (!journey) return null;
|
|
2125
|
+
if (journey.phase === "trial") {
|
|
2126
|
+
if (d0Hidden || !trialEndsToday(journey.trialEndsAt)) return null;
|
|
2127
|
+
return /* @__PURE__ */ jsxs(
|
|
2128
|
+
"section",
|
|
2129
|
+
{
|
|
2130
|
+
role: "region",
|
|
2131
|
+
"aria-label": t("gettingStarted.d0.region"),
|
|
2132
|
+
className: "flex w-full flex-wrap items-center gap-x-3 gap-y-1.5 rounded-card border border-destructive/40 bg-destructive/10 px-3 py-1.5 text-xs text-foreground",
|
|
2133
|
+
children: [
|
|
2134
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-4 w-4 shrink-0 text-destructive", "aria-hidden": "true" }),
|
|
2135
|
+
/* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1", children: t("gettingStarted.d0.title", { time: formatEndTime(journey.trialEndsAt) }) }),
|
|
2136
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2137
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: () => adminNavigateTo(config.routes.subscription), children: t("gettingStarted.d0.subscribe") }),
|
|
2138
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => adminNavigateTo(config.routes.subscription), children: t("gettingStarted.d0.seeChanges") }),
|
|
2139
|
+
/* @__PURE__ */ jsx(
|
|
2140
|
+
Button,
|
|
2141
|
+
{
|
|
2142
|
+
type: "button",
|
|
2143
|
+
size: "bare",
|
|
2144
|
+
variant: "bare",
|
|
2145
|
+
"aria-label": t("gettingStarted.d0.dismiss"),
|
|
2146
|
+
title: t("gettingStarted.d0.dismiss"),
|
|
2147
|
+
onClick: () => {
|
|
2148
|
+
dismissD0();
|
|
2149
|
+
setD0Hidden(true);
|
|
2150
|
+
},
|
|
2151
|
+
className: "inline-flex h-7 w-7 items-center justify-center rounded-button text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
2152
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
|
|
2153
|
+
}
|
|
2154
|
+
)
|
|
2155
|
+
] })
|
|
2156
|
+
]
|
|
2157
|
+
}
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
if (journey.phase !== "expired") return null;
|
|
2161
|
+
const submit = async () => {
|
|
2162
|
+
setBusy(true);
|
|
2163
|
+
setNotice(null);
|
|
2164
|
+
try {
|
|
2165
|
+
const result = await requestContract(phone ? `whatsapp:${phone}` : void 0);
|
|
2166
|
+
if (result.alreadyRequestedToday) setNotice(t("gettingStarted.banner.rateLimited"));
|
|
2167
|
+
setMode("done");
|
|
2168
|
+
} catch {
|
|
2169
|
+
setNotice(t("gettingStarted.banner.error"));
|
|
2170
|
+
} finally {
|
|
2171
|
+
setBusy(false);
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
return /* @__PURE__ */ jsxs(
|
|
2175
|
+
"section",
|
|
2176
|
+
{
|
|
2177
|
+
role: "region",
|
|
2178
|
+
"aria-label": t("gettingStarted.banner.region"),
|
|
2179
|
+
className: "flex w-full flex-wrap items-center gap-x-3 gap-y-2 rounded-card border border-primary bg-card px-3 py-2 text-sm text-foreground",
|
|
2180
|
+
children: [
|
|
2181
|
+
/* @__PURE__ */ jsx(Lock, { className: "h-4 w-4 shrink-0 text-primary", "aria-hidden": "true" }),
|
|
2182
|
+
mode === "done" ? /* @__PURE__ */ jsxs("p", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
|
|
2183
|
+
/* @__PURE__ */ jsx(Check, { className: "h-4 w-4 shrink-0 text-success", "aria-hidden": "true" }),
|
|
2184
|
+
/* @__PURE__ */ jsx("span", { children: t("gettingStarted.banner.done") })
|
|
2185
|
+
] }) : mode === "phone" ? /* @__PURE__ */ jsxs(
|
|
2186
|
+
"form",
|
|
2187
|
+
{
|
|
2188
|
+
onSubmit: (e) => {
|
|
2189
|
+
e.preventDefault();
|
|
2190
|
+
void submit();
|
|
2191
|
+
},
|
|
2192
|
+
className: "flex min-w-0 flex-1 flex-wrap items-center gap-2",
|
|
2193
|
+
children: [
|
|
2194
|
+
/* @__PURE__ */ jsx("span", { children: t("gettingStarted.banner.phonePrompt") }),
|
|
2195
|
+
/* @__PURE__ */ jsx(PhoneField, { value: phone, onChange: setPhone, "aria-label": t("gettingStarted.banner.phoneLabel"), className: "w-44", autoFocus: true }),
|
|
2196
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: busy || !allowed, ...markProps, children: busy ? t("gettingStarted.banner.sending") : t("gettingStarted.banner.confirm") }),
|
|
2197
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setMode("idle"), disabled: busy, children: t("gettingStarted.banner.back") })
|
|
2198
|
+
]
|
|
2199
|
+
}
|
|
2200
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2201
|
+
/* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1", children: t("gettingStarted.banner.title", { date: formatTrialDate(journey.trialEndsAt) }) }),
|
|
2202
|
+
/* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-2 sm:w-auto", children: [
|
|
2203
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "outline", className: "flex-1 sm:flex-none", onClick: () => adminNavigateTo(config.routes.subscription), children: t("gettingStarted.banner.seeChanges") }),
|
|
2204
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", className: "flex-1 sm:flex-none", onClick: () => setMode("phone"), disabled: !allowed, ...markProps, children: t("gettingStarted.banner.contract") })
|
|
2205
|
+
] })
|
|
2206
|
+
] }),
|
|
2207
|
+
notice && /* @__PURE__ */ jsx("p", { role: "status", className: "basis-full text-xs text-muted-foreground", children: notice })
|
|
2208
|
+
]
|
|
2209
|
+
}
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
var DAY_MS4 = 864e5;
|
|
2213
|
+
function latch(key2) {
|
|
2214
|
+
const state = useActivationStore.getState();
|
|
2215
|
+
const row = state.rows?.find((r) => r.sourceKey === key2);
|
|
2216
|
+
if (row && row.status === "todo") void state.complete(key2, "sensor");
|
|
2217
|
+
}
|
|
2218
|
+
function reopen(key2) {
|
|
2219
|
+
const state = useActivationStore.getState();
|
|
2220
|
+
if (!state.rows?.some((r) => r.sourceKey === key2 && r.status === "done")) return;
|
|
2221
|
+
useActivationStore.setState({
|
|
2222
|
+
rows: state.rows.map((r) => r.sourceKey === key2 ? { ...r, status: "todo", completedAt: null } : r)
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
function StoreLiveCard({ className, variant = "card" }) {
|
|
2226
|
+
const rail = variant === "rail";
|
|
2227
|
+
useEnsureJourney();
|
|
2228
|
+
const t = useTranslation();
|
|
2229
|
+
const config = useGettingStartedConfig();
|
|
2230
|
+
const journey = useGettingStartedStore((s) => s.journey);
|
|
2231
|
+
const journeyError = useGettingStartedStore((s) => s.error);
|
|
2232
|
+
const rows = useActivationStore((s) => s.rows);
|
|
2233
|
+
const catalog = useActivationStore((s) => s.catalog);
|
|
2234
|
+
const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
2235
|
+
const storeSettings = useOrganizationStore((s) => s.currentOrg?.settings?.store);
|
|
2236
|
+
const selection = React5.useMemo(() => selectJourneySteps(catalog), [catalog]);
|
|
2237
|
+
const counted = React5.useMemo(() => [...selection.launch, ...selection.later], [selection]);
|
|
2238
|
+
const steps = React5.useMemo(
|
|
2239
|
+
() => toStepInputs([...counted, ...selection.extra], rows, latch, reopen),
|
|
2240
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- storeSettings only re-arms the sensors
|
|
2241
|
+
[counted, selection.extra, rows, storeSettings]
|
|
2242
|
+
);
|
|
2243
|
+
const onNavigate = React5.useCallback((path) => adminNavigateTo(path), []);
|
|
2244
|
+
const { completed, resolved } = useOnboardingProgress(steps, onNavigate);
|
|
2245
|
+
const total = counted.length;
|
|
2246
|
+
const done = counted.filter((s) => completed[s.key]).length;
|
|
2247
|
+
const minimums = React5.useMemo(() => countMinimums(journey, rows), [journey, rows]);
|
|
2248
|
+
const effective = journey ?? (journeyError && tenantId ? clientJourney({ tenantId, trialDays: config.trialDays, rows }) : null);
|
|
2249
|
+
const phase = effective?.phase ?? "setup";
|
|
2250
|
+
const inSetup = phase === "setup";
|
|
2251
|
+
const now = inSetup ? nowStepKey(selection, completed) : void 0;
|
|
2252
|
+
const runtime = useRememberJourneyRuntime();
|
|
2253
|
+
const cardRef = React5.useRef(null);
|
|
2254
|
+
const describe = () => ({
|
|
2255
|
+
title: t("gettingStarted.card.title"),
|
|
2256
|
+
body: phase === "trial" ? tl(
|
|
2257
|
+
`${done} of ${total} steps done. The trial runs until ${formatTrialDate(effective?.trialEndsAt)}; what is left waits here, at your pace.`,
|
|
2258
|
+
`${done} de ${total} passos prontos. O teste vai at\xE9 ${formatTrialDate(effective?.trialEndsAt)}; o que falta espera aqui, no seu ritmo.`
|
|
2259
|
+
) : minimums.ready ? tl(
|
|
2260
|
+
`${done} of ${total} steps done \u2014 everything the trial needs is in. The button is right below.`,
|
|
2261
|
+
`${done} de ${total} passos prontos \u2014 tudo o que o teste precisa j\xE1 est\xE1 aqui. O bot\xE3o fica logo abaixo.`
|
|
2262
|
+
) : tl(
|
|
2263
|
+
`${done} of ${total} steps done. Each line opens its screen.`,
|
|
2264
|
+
`${done} de ${total} passos prontos. Cada linha abre a tela dela.`
|
|
2265
|
+
)
|
|
2266
|
+
});
|
|
2267
|
+
const drawn = !!rows && rows.length > 0 && total > 0;
|
|
2268
|
+
const claimed = React5.useMemo(
|
|
2269
|
+
() => drawn ? [...counted, ...selection.extra].map((s) => s.key) : [],
|
|
2270
|
+
[drawn, counted, selection.extra]
|
|
2271
|
+
);
|
|
2272
|
+
useClaimOnboardingTasks(claimed);
|
|
2273
|
+
if (!drawn) return null;
|
|
2274
|
+
const ringLabel = t("gettingStarted.card.aria", { done, total });
|
|
2275
|
+
const subtitle = phase === "trial" ? t("gettingStarted.card.subtitle.trial", { date: formatTrialDate(effective?.trialEndsAt) }) : phase === "expired" ? t("gettingStarted.card.subtitle.expired", { date: formatTrialDate(effective?.trialEndsAt) }) : phase === "converted" ? t("gettingStarted.card.subtitle.converted") : minimums.ready ? t("gettingStarted.card.subtitle.ready") : minimums.missing.length === 1 ? t("gettingStarted.card.subtitle.setup.one") : t("gettingStarted.card.subtitle.setup.many", { count: minimums.missing.length });
|
|
2276
|
+
const afterNavigate = rail ? closeRightRailOnPhone : void 0;
|
|
2277
|
+
return /* @__PURE__ */ jsxs(
|
|
2278
|
+
Card,
|
|
2279
|
+
{
|
|
2280
|
+
ref: cardRef,
|
|
2281
|
+
"data-tour": rail ? void 0 : CARD_ANCHOR,
|
|
2282
|
+
"data-variant": variant,
|
|
2283
|
+
className,
|
|
2284
|
+
"aria-busy": !resolved || void 0,
|
|
2285
|
+
children: [
|
|
2286
|
+
!rail && /* @__PURE__ */ jsx(CardTour, { cardRef, describe, autostart: inSetup && minimums.ready, runtime }),
|
|
2287
|
+
/* @__PURE__ */ jsx(CardHeader, { className: cn("pb-2", rail && "p-3"), children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
2288
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center", children: [
|
|
2289
|
+
/* @__PURE__ */ jsx(ProgressRing, { value: done, max: total, size: 40, label: ringLabel, complete: done === total }),
|
|
2290
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 text-[10px] text-muted-foreground", children: t("gettingStarted.card.steps") })
|
|
2291
|
+
] }),
|
|
2292
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2293
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold leading-tight", children: [
|
|
2294
|
+
/* @__PURE__ */ jsx(Rocket, { className: "h-4 w-4 text-primary", "aria-hidden": "true" }),
|
|
2295
|
+
t("gettingStarted.card.title")
|
|
2296
|
+
] }),
|
|
2297
|
+
/* @__PURE__ */ jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", "aria-live": "polite", children: subtitle }),
|
|
2298
|
+
!journey && journeyError && /* @__PURE__ */ jsxs("p", { className: "mt-1 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
2299
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0 text-warning", "aria-hidden": "true" }),
|
|
2300
|
+
t("gettingStarted.card.noClock")
|
|
2301
|
+
] })
|
|
2302
|
+
] })
|
|
2303
|
+
] }) }),
|
|
2304
|
+
/* @__PURE__ */ jsxs(CardContent, { className: cn("space-y-3 pt-0", rail && "px-2 pb-3"), children: [
|
|
2305
|
+
/* @__PURE__ */ jsxs("ol", { "aria-label": ringLabel, className: "space-y-0.5", children: [
|
|
2306
|
+
selection.launch.length > 0 && !selection.fallback && /* @__PURE__ */ jsx("li", { className: "flex items-center gap-2 px-2 pb-1 pt-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", "aria-hidden": "true", children: t("gettingStarted.card.group.launch") }),
|
|
2307
|
+
selection.launch.map((step) => /* @__PURE__ */ jsx(StepRow, { step, done: !!completed[step.key], now: step.key === now, afterNavigate }, step.key)),
|
|
2308
|
+
selection.later.length > 0 && /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 px-2 pb-1 pt-3 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", role: "separator", children: [
|
|
2309
|
+
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": "true" }),
|
|
2310
|
+
t("gettingStarted.card.group.later"),
|
|
2311
|
+
/* @__PURE__ */ jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": "true" })
|
|
2312
|
+
] }),
|
|
2313
|
+
selection.later.map((step) => /* @__PURE__ */ jsx(StepRow, { step, done: !!completed[step.key], now: step.key === now, afterNavigate }, step.key)),
|
|
2314
|
+
selection.extra.map((step) => /* @__PURE__ */ jsx(StepRow, { step, done: !!completed[step.key], now: false, optional: true, afterNavigate }, step.key))
|
|
2315
|
+
] }),
|
|
2316
|
+
inSetup && /* @__PURE__ */ jsx(SetupActions, { journey: effective, minimums, nowKey: now }),
|
|
2317
|
+
phase === "trial" && /* @__PURE__ */ jsx("div", { className: "border-t border-border pt-3", children: /* @__PURE__ */ jsxs(
|
|
2318
|
+
Button,
|
|
2319
|
+
{
|
|
2320
|
+
type: "button",
|
|
2321
|
+
size: "sm",
|
|
2322
|
+
variant: "ghost",
|
|
2323
|
+
className: "min-h-[44px] sm:min-h-9",
|
|
2324
|
+
onClick: () => {
|
|
2325
|
+
startJourneyTour({ force: true, moment: "manual", runtime });
|
|
2326
|
+
},
|
|
2327
|
+
children: [
|
|
2328
|
+
/* @__PURE__ */ jsx(Compass, { className: "h-4 w-4 text-primary", "aria-hidden": "true" }),
|
|
2329
|
+
tl("Take the tour again", "Fazer o tour de novo")
|
|
2330
|
+
]
|
|
2331
|
+
}
|
|
2332
|
+
) })
|
|
2333
|
+
] })
|
|
2334
|
+
]
|
|
2335
|
+
}
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
function StepRow({ step, done, now, optional, afterNavigate }) {
|
|
2339
|
+
const t = useTranslation();
|
|
2340
|
+
const go = () => {
|
|
2341
|
+
if (step.route) {
|
|
2342
|
+
adminNavigateTo(step.route);
|
|
2343
|
+
afterNavigate?.();
|
|
2344
|
+
} else if (!done) void startGuidedRun(step.key);
|
|
2345
|
+
};
|
|
2346
|
+
return /* @__PURE__ */ jsx(
|
|
2347
|
+
OnboardingStepRow,
|
|
2348
|
+
{
|
|
2349
|
+
title: step.title,
|
|
2350
|
+
done,
|
|
2351
|
+
now,
|
|
2352
|
+
nowLabel: t("gettingStarted.cta.sheet.now"),
|
|
2353
|
+
note: optional ? t("gettingStarted.card.optional") : void 0,
|
|
2354
|
+
stateLabel: done ? t("gettingStarted.cta.sheet.done") : now ? t("gettingStarted.cta.sheet.now") : t("gettingStarted.cta.sheet.open"),
|
|
2355
|
+
action: step.route ? done ? t("gettingStarted.cta.sheet.review") : t("gettingStarted.cta.sheet.configure") : void 0,
|
|
2356
|
+
onClick: go
|
|
2357
|
+
}
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
function CardTour({ cardRef, describe, autostart, runtime }) {
|
|
2361
|
+
useTourDescription(DESCRIBE_CARD_ACTION, describe);
|
|
2362
|
+
useJourneyTourAutostart(cardRef, autostart, runtime);
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
function SetupActions({ journey, minimums, nowKey }) {
|
|
2366
|
+
const t = useTranslation();
|
|
2367
|
+
const config = useGettingStartedConfig();
|
|
2368
|
+
const starting = useGettingStartedStore((s) => s.starting);
|
|
2369
|
+
const startTrial = useGettingStartedStore((s) => s.startTrial);
|
|
2370
|
+
const { allowed, markProps } = useSdkPermission(SDK_PERMISSIONS.trialManage);
|
|
2371
|
+
const [confirmOpen, setConfirmOpen] = React5.useState(false);
|
|
2372
|
+
const [notice, setNotice] = React5.useState(null);
|
|
2373
|
+
const anchor = React5.useRef(null);
|
|
2374
|
+
const trialDays = journey?.trialDays || config.trialDays;
|
|
2375
|
+
const ready = journey?.minimums.ready || minimums.ready;
|
|
2376
|
+
const missingWords = (ids) => ids.map((id) => t(`gettingStarted.minimum.${id}`)).join(t("gettingStarted.minimum.join"));
|
|
2377
|
+
const handleStart = async () => {
|
|
2378
|
+
setNotice(null);
|
|
2379
|
+
let result;
|
|
2380
|
+
try {
|
|
2381
|
+
result = await startTrial();
|
|
2382
|
+
} catch (e) {
|
|
2383
|
+
setConfirmOpen(false);
|
|
2384
|
+
setNotice(isMissingClock(e) ? t("gettingStarted.start.noClock") : t("gettingStarted.start.error"));
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
setConfirmOpen(false);
|
|
2388
|
+
if (result.ok) {
|
|
2389
|
+
const rect = anchor.current?.getBoundingClientRect();
|
|
2390
|
+
fireConfetti(rect ? { origin: { x: (rect.left + rect.width / 2) / window.innerWidth, y: rect.top / window.innerHeight } } : void 0);
|
|
2391
|
+
toast.success(t("gettingStarted.start.done", { date: formatTrialDate(result.trialEndsAt) }));
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
setNotice(t("gettingStarted.start.missing", { what: missingWords(MINIMUM_IDS.filter((id) => !result.minimums[id])) }));
|
|
2395
|
+
};
|
|
2396
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-t border-border pt-3", children: [
|
|
2397
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
2398
|
+
ready ? /* @__PURE__ */ jsx(Button, { ref: anchor, type: "button", size: "sm", className: "min-h-[44px] sm:min-h-9", onClick: () => setConfirmOpen(true), disabled: starting || !allowed, ...markProps, children: starting ? t("gettingStarted.confirm.starting") : t("gettingStarted.cta.ready", { days: trialDays }) }) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("gettingStarted.start.missingLabel", { what: missingWords(minimums.missing) }) }),
|
|
2399
|
+
nowKey && /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: ready ? "ghost" : "outline", className: "min-h-[44px] sm:min-h-9", onClick: () => {
|
|
2400
|
+
void startGuidedRun(nowKey);
|
|
2401
|
+
}, children: [
|
|
2402
|
+
/* @__PURE__ */ jsx(WandSparkles, { className: "h-4 w-4 text-primary", "aria-hidden": "true" }),
|
|
2403
|
+
t("gettingStarted.card.withAssistant")
|
|
2404
|
+
] })
|
|
2405
|
+
] }),
|
|
2406
|
+
notice && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-xs text-foreground", children: notice }),
|
|
2407
|
+
/* @__PURE__ */ jsx(
|
|
2408
|
+
ConfirmDialog,
|
|
2409
|
+
{
|
|
2410
|
+
open: confirmOpen,
|
|
2411
|
+
title: t("gettingStarted.confirm.title", { days: trialDays }),
|
|
2412
|
+
description: t("gettingStarted.confirm.body", { date: formatTrialDate(new Date(Date.now() + trialDays * DAY_MS4).toISOString()) }),
|
|
2413
|
+
confirmLabel: starting ? t("gettingStarted.confirm.starting") : t("gettingStarted.confirm.ok"),
|
|
2414
|
+
cancelLabel: t("gettingStarted.confirm.cancel"),
|
|
2415
|
+
loading: starting,
|
|
2416
|
+
onConfirm: () => {
|
|
2417
|
+
void handleStart();
|
|
2418
|
+
},
|
|
2419
|
+
onCancel: () => setConfirmOpen(false)
|
|
2420
|
+
}
|
|
2421
|
+
)
|
|
2422
|
+
] });
|
|
2423
|
+
}
|
|
2424
|
+
var DEFAULT_RANGE = { open: "11:00", close: "23:00" };
|
|
2425
|
+
var DAY_MS5 = 864e5;
|
|
2426
|
+
var TEMPLATES = {
|
|
2427
|
+
weekdays: ["mon", "tue", "wed", "thu", "fri", "sat"],
|
|
2428
|
+
everyday: [...WEEKDAYS],
|
|
2429
|
+
weekend: ["sat", "sun"]
|
|
2430
|
+
};
|
|
2431
|
+
function hoursTemplates(label) {
|
|
2432
|
+
return Object.keys(TEMPLATES).map((id) => {
|
|
2433
|
+
const hours = {};
|
|
2434
|
+
for (const day of TEMPLATES[id]) hours[day] = [{ ...DEFAULT_RANGE }];
|
|
2435
|
+
return { id, label: label(id), hours };
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
function toStoreHours(value) {
|
|
2439
|
+
const week = normalizeHours(value);
|
|
2440
|
+
const out = {};
|
|
2441
|
+
for (const day of WEEKDAYS) {
|
|
2442
|
+
const shifts = week[day] ?? [];
|
|
2443
|
+
if (shifts.length > 0 && isHoursValue({ [day]: shifts })) out[day] = shifts.map((s) => ({ open: s.open, close: s.close }));
|
|
2444
|
+
}
|
|
2445
|
+
return out;
|
|
2446
|
+
}
|
|
2447
|
+
function TrialSettingsTab() {
|
|
2448
|
+
useEnsureJourney();
|
|
2449
|
+
const t = useTranslation();
|
|
2450
|
+
const settings = useOrganizationStore((s) => s.currentOrg?.settings);
|
|
2451
|
+
const { hours, methods } = React5.useMemo(() => readStoreSettings(settings), [settings]);
|
|
2452
|
+
const [state, setState] = React5.useState({ saving: 0, error: null, offline: false, viaRow: false });
|
|
2453
|
+
const save = React5.useCallback(async (key2, value) => {
|
|
2454
|
+
setState((s) => ({ ...s, saving: s.saving + 1, error: null }));
|
|
2455
|
+
const result = await saveStoreSetting(key2, value);
|
|
2456
|
+
setState((s) => ({
|
|
2457
|
+
saving: Math.max(0, s.saving - 1),
|
|
2458
|
+
error: result.ok ? null : result.reason === "error" ? result.message ?? "" : s.error,
|
|
2459
|
+
offline: !result.ok && result.reason === "offline",
|
|
2460
|
+
viaRow: result.ok ? result.via === "row" : s.viaRow
|
|
2461
|
+
}));
|
|
2462
|
+
}, []);
|
|
2463
|
+
const saveHours = (next) => {
|
|
2464
|
+
void save("hours", next);
|
|
2465
|
+
};
|
|
2466
|
+
const saveMethods = (next) => {
|
|
2467
|
+
void save("payment_methods", next);
|
|
2468
|
+
};
|
|
2469
|
+
const templates = React5.useMemo(() => hoursTemplates((id) => t(`gettingStarted.settings.hours.template.${id}`)), [t]);
|
|
2470
|
+
const hoursLabels = React5.useMemo(() => ({
|
|
2471
|
+
days: Object.fromEntries(WEEKDAYS.map((day) => [day, t(`gettingStarted.settings.day.${day}`)]))
|
|
2472
|
+
}), [t]);
|
|
2473
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
2474
|
+
/* @__PURE__ */ jsx(JourneyCard, {}),
|
|
2475
|
+
state.offline && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
2476
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-warning", "aria-hidden": "true" }),
|
|
2477
|
+
t("gettingStarted.settings.offline")
|
|
2478
|
+
] }),
|
|
2479
|
+
state.error && /* @__PURE__ */ jsxs("p", { role: "alert", className: "flex items-center gap-2 text-xs text-foreground", children: [
|
|
2480
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 text-destructive", "aria-hidden": "true" }),
|
|
2481
|
+
t("gettingStarted.settings.saveError", { message: state.error })
|
|
2482
|
+
] }),
|
|
2483
|
+
/* @__PURE__ */ jsx(SettingsGroup, { title: t("gettingStarted.settings.hours.title"), description: t("gettingStarted.settings.hours.description"), children: /* @__PURE__ */ jsx(
|
|
2484
|
+
HoursField,
|
|
2485
|
+
{
|
|
2486
|
+
className: "py-2",
|
|
2487
|
+
"aria-label": t("gettingStarted.settings.hours.title"),
|
|
2488
|
+
value: hours,
|
|
2489
|
+
templates,
|
|
2490
|
+
labels: hoursLabels,
|
|
2491
|
+
onChange: (next) => saveHours(toStoreHours(next))
|
|
2492
|
+
}
|
|
2493
|
+
) }),
|
|
2494
|
+
/* @__PURE__ */ jsx(SettingsGroup, { title: t("gettingStarted.settings.payments.title"), description: t("gettingStarted.settings.payments.description"), children: /* @__PURE__ */ jsx("ul", { className: "divide-y divide-border", children: PAYMENT_METHOD_IDS.map((method) => {
|
|
2495
|
+
const checked = methods.includes(method);
|
|
2496
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", { className: "flex min-h-[44px] cursor-pointer items-center gap-3 py-2 text-sm", children: [
|
|
2497
|
+
/* @__PURE__ */ jsx(
|
|
2498
|
+
Checkbox,
|
|
2499
|
+
{
|
|
2500
|
+
checked,
|
|
2501
|
+
onChange: (next) => saveMethods(next ? [...methods, method] : methods.filter((m) => m !== method))
|
|
2502
|
+
}
|
|
2503
|
+
),
|
|
2504
|
+
/* @__PURE__ */ jsx("span", { children: t(`gettingStarted.settings.payments.${method}`) })
|
|
2505
|
+
] }) }, method);
|
|
2506
|
+
}) }) }),
|
|
2507
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", "aria-live": "polite", children: state.saving > 0 ? t("gettingStarted.settings.saving") : !state.error && !state.offline ? t("gettingStarted.settings.saved") : "" })
|
|
2508
|
+
] });
|
|
2509
|
+
}
|
|
2510
|
+
function JourneyCard() {
|
|
2511
|
+
const t = useTranslation();
|
|
2512
|
+
const config = useGettingStartedConfig();
|
|
2513
|
+
const journey = useGettingStartedStore((s) => s.journey);
|
|
2514
|
+
const loading = useGettingStartedStore((s) => s.loading);
|
|
2515
|
+
const error = useGettingStartedStore((s) => s.error);
|
|
2516
|
+
const starting = useGettingStartedStore((s) => s.starting);
|
|
2517
|
+
const load = useGettingStartedStore((s) => s.load);
|
|
2518
|
+
const startTrial = useGettingStartedStore((s) => s.startTrial);
|
|
2519
|
+
const requestContract = useGettingStartedStore((s) => s.requestContract);
|
|
2520
|
+
const { allowed, markProps } = useSdkPermission(SDK_PERMISSIONS.trialManage);
|
|
2521
|
+
const tenantId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
2522
|
+
const rows = useActivationStore((s) => s.rows);
|
|
2523
|
+
const [confirmOpen, setConfirmOpen] = React5.useState(false);
|
|
2524
|
+
const [note, setNote] = React5.useState("");
|
|
2525
|
+
const [busy, setBusy] = React5.useState(false);
|
|
2526
|
+
const [notice, setNotice] = React5.useState(null);
|
|
2527
|
+
const serverless = !journey && !!error && !loading;
|
|
2528
|
+
const effective = journey ?? (serverless && tenantId ? clientJourney({ tenantId, trialDays: config.trialDays, rows }) : null);
|
|
2529
|
+
const runtime = useRememberJourneyRuntime();
|
|
2530
|
+
useTourDescription(DESCRIBE_TRIAL_TAB_ACTION, () => ({
|
|
2531
|
+
title: t(`gettingStarted.settings.phase.${effective?.phase ?? "setup"}`),
|
|
2532
|
+
body: effective?.phase === "trial" ? tl(
|
|
2533
|
+
`Your trial runs until ${formatTrialDate(effective.trialEndsAt)}. Hours and payment methods are edited right below; the plans live here too.`,
|
|
2534
|
+
`Seu teste vai at\xE9 ${formatTrialDate(effective.trialEndsAt)}. Hor\xE1rios e formas de pagamento se editam logo abaixo; os planos tamb\xE9m ficam aqui.`
|
|
2535
|
+
) : tl(
|
|
2536
|
+
"Hours, payment methods and the trial clock live in this tab. When everything is ready, the trial starts from here.",
|
|
2537
|
+
"Hor\xE1rios, formas de pagamento e o rel\xF3gio do teste moram nesta aba. Com tudo pronto, o teste come\xE7a daqui."
|
|
2538
|
+
)
|
|
2539
|
+
}));
|
|
2540
|
+
if (!effective) {
|
|
2541
|
+
return /* @__PURE__ */ jsx("div", { className: "rounded-card border border-border bg-card p-4 text-sm text-muted-foreground", children: loading ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
2542
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": "true" }),
|
|
2543
|
+
t("gettingStarted.settings.loading")
|
|
2544
|
+
] }) : error ? /* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-center gap-2", children: [
|
|
2545
|
+
t("gettingStarted.settings.loadError"),
|
|
2546
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => {
|
|
2547
|
+
void load(tenantId);
|
|
2548
|
+
}, children: t("gettingStarted.settings.retry") })
|
|
2549
|
+
] }) : null });
|
|
2550
|
+
}
|
|
2551
|
+
const minimums = countMinimums(journey, rows);
|
|
2552
|
+
const ready = effective.minimums.ready || minimums.ready;
|
|
2553
|
+
const missing = ready ? [] : MINIMUM_IDS.filter((id) => !(effective.minimums[id] || minimums.done.includes(id)));
|
|
2554
|
+
const trialDays = effective.trialDays || config.trialDays;
|
|
2555
|
+
const missingWords = (ids) => ids.map((id) => t(`gettingStarted.minimum.${id}`)).join(t("gettingStarted.minimum.join"));
|
|
2556
|
+
const handleStart = async () => {
|
|
2557
|
+
setNotice(null);
|
|
2558
|
+
try {
|
|
2559
|
+
const result = await startTrial();
|
|
2560
|
+
setConfirmOpen(false);
|
|
2561
|
+
if (!result.ok) {
|
|
2562
|
+
const still = MINIMUM_IDS.filter((id) => !result.minimums[id]);
|
|
2563
|
+
setNotice(t("gettingStarted.start.missing", { what: missingWords(still) }));
|
|
2564
|
+
}
|
|
2565
|
+
} catch (e) {
|
|
2566
|
+
setConfirmOpen(false);
|
|
2567
|
+
setNotice(isMissingClock(e) ? t("gettingStarted.start.noClock") : t("gettingStarted.start.error"));
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
const handleContract = async () => {
|
|
2571
|
+
setBusy(true);
|
|
2572
|
+
setNotice(null);
|
|
2573
|
+
try {
|
|
2574
|
+
const result = await requestContract(note.trim() || void 0);
|
|
2575
|
+
setNotice(result.alreadyRequestedToday ? t("gettingStarted.banner.rateLimited") : t("gettingStarted.settings.contract.done"));
|
|
2576
|
+
} catch {
|
|
2577
|
+
setNotice(t("gettingStarted.banner.error"));
|
|
2578
|
+
} finally {
|
|
2579
|
+
setBusy(false);
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
const phase = effective.phase;
|
|
2583
|
+
const headline = t(`gettingStarted.settings.phase.${phase}`);
|
|
2584
|
+
const line = phase === "setup" ? ready ? t("gettingStarted.settings.phase.setup.ready") : missing.length === 1 ? t("gettingStarted.settings.phase.setup.line.one") : t("gettingStarted.settings.phase.setup.line.many", { count: missing.length }) : phase === "trial" ? t("gettingStarted.settings.phase.trial.line", { date: formatTrialDate(effective.trialEndsAt) }) : phase === "expired" ? t("gettingStarted.settings.phase.expired.line", { date: formatTrialDate(effective.trialEndsAt) }) : t("gettingStarted.settings.phase.converted.line");
|
|
2585
|
+
const Icon = phase === "converted" ? Crown : phase === "expired" ? Lock : Clock;
|
|
2586
|
+
return /* @__PURE__ */ jsxs("div", { "data-tour": TRIAL_TAB_ANCHOR, className: "rounded-card border border-border bg-card p-4", children: [
|
|
2587
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
|
|
2588
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [
|
|
2589
|
+
/* @__PURE__ */ jsx(Icon, { className: cn("mt-0.5 h-5 w-5 shrink-0", phase === "expired" ? "text-destructive" : "text-primary"), "aria-hidden": "true" }),
|
|
2590
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
2591
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: headline }),
|
|
2592
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: line }),
|
|
2593
|
+
phase === "setup" && effective.setupDeadlineAt && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: t("gettingStarted.settings.phase.setup.deadline", { date: formatTrialDate(effective.setupDeadlineAt) }) }),
|
|
2594
|
+
serverless && /* @__PURE__ */ jsxs("p", { className: "mt-1 flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
2595
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "h-3.5 w-3.5 shrink-0 text-warning", "aria-hidden": "true" }),
|
|
2596
|
+
t("gettingStarted.settings.phase.setup.offline"),
|
|
2597
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => {
|
|
2598
|
+
void load(tenantId);
|
|
2599
|
+
}, children: t("gettingStarted.settings.retry") })
|
|
2600
|
+
] })
|
|
2601
|
+
] })
|
|
2602
|
+
] }),
|
|
2603
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
2604
|
+
phase === "setup" && ready && /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: () => setConfirmOpen(true), disabled: starting || !allowed, ...markProps, children: starting ? t("gettingStarted.confirm.starting") : t("gettingStarted.confirm.ok") }),
|
|
2605
|
+
phase === "setup" && !ready && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: t("gettingStarted.start.missingLabel", { what: missingWords(missing) }) }),
|
|
2606
|
+
(phase === "trial" || phase === "expired") && /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "outline", onClick: () => adminNavigateTo(config.routes.subscription), children: t("gettingStarted.settings.plans") }),
|
|
2607
|
+
(phase === "setup" || phase === "trial") && /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => {
|
|
2608
|
+
startJourneyTour({ force: true, moment: "manual", runtime });
|
|
2609
|
+
}, children: [
|
|
2610
|
+
/* @__PURE__ */ jsx(Compass, { className: "h-4 w-4 text-primary", "aria-hidden": "true" }),
|
|
2611
|
+
tl("Take the tour again", "Fazer o tour de novo")
|
|
2612
|
+
] })
|
|
2613
|
+
] })
|
|
2614
|
+
] }),
|
|
2615
|
+
phase === "setup" && /* @__PURE__ */ jsxs("ul", { className: "mt-4 space-y-1", "aria-label": t("gettingStarted.settings.minimums.title"), children: [
|
|
2616
|
+
MINIMUM_IDS.map((id) => {
|
|
2617
|
+
const done = effective.minimums[id] || minimums.done.includes(id);
|
|
2618
|
+
return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 text-sm", children: [
|
|
2619
|
+
done ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-success", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Circle, { className: "h-3 w-3 text-muted-foreground", "aria-hidden": "true" }),
|
|
2620
|
+
/* @__PURE__ */ jsx("span", { className: cn(done && "text-muted-foreground line-through"), children: t(`gettingStarted.settings.minimums.${id}`) }),
|
|
2621
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: done ? t("gettingStarted.settings.minimums.done") : t("gettingStarted.settings.minimums.pending") })
|
|
2622
|
+
] }, id);
|
|
2623
|
+
}),
|
|
2624
|
+
/* @__PURE__ */ jsx("li", { className: "pt-1 text-xs text-muted-foreground", children: t("gettingStarted.settings.minimums.description") })
|
|
2625
|
+
] }),
|
|
2626
|
+
(phase === "trial" || phase === "expired") && !effective.wantsContractAt && /* @__PURE__ */ jsxs(
|
|
2627
|
+
"form",
|
|
2628
|
+
{
|
|
2629
|
+
onSubmit: (e) => {
|
|
2630
|
+
e.preventDefault();
|
|
2631
|
+
void handleContract();
|
|
2632
|
+
},
|
|
2633
|
+
className: "mt-4 flex flex-wrap items-center gap-2",
|
|
2634
|
+
children: [
|
|
2635
|
+
/* @__PURE__ */ jsx(
|
|
2636
|
+
Input,
|
|
2637
|
+
{
|
|
2638
|
+
value: note,
|
|
2639
|
+
onChange: (e) => setNote(e.target.value),
|
|
2640
|
+
placeholder: t("gettingStarted.settings.contract.note"),
|
|
2641
|
+
maxLength: 500,
|
|
2642
|
+
className: "w-full sm:w-72"
|
|
2643
|
+
}
|
|
2644
|
+
),
|
|
2645
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", size: "sm", disabled: busy || !allowed, ...markProps, children: busy ? t("gettingStarted.banner.sending") : t("gettingStarted.banner.contract") })
|
|
2646
|
+
]
|
|
2647
|
+
}
|
|
2648
|
+
),
|
|
2649
|
+
(phase === "trial" || phase === "expired") && effective.wantsContractAt && !notice && /* @__PURE__ */ jsxs("p", { className: "mt-3 flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
2650
|
+
/* @__PURE__ */ jsx(Check, { className: "h-4 w-4 text-success", "aria-hidden": "true" }),
|
|
2651
|
+
t("gettingStarted.settings.contract.done")
|
|
2652
|
+
] }),
|
|
2653
|
+
notice && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-3 text-xs text-foreground", children: notice }),
|
|
2654
|
+
/* @__PURE__ */ jsx(
|
|
2655
|
+
ConfirmDialog,
|
|
2656
|
+
{
|
|
2657
|
+
open: confirmOpen,
|
|
2658
|
+
title: t("gettingStarted.confirm.title", { days: trialDays }),
|
|
2659
|
+
description: t("gettingStarted.confirm.body", { date: formatTrialDate(new Date(Date.now() + trialDays * DAY_MS5).toISOString()) }),
|
|
2660
|
+
confirmLabel: starting ? t("gettingStarted.confirm.starting") : t("gettingStarted.confirm.ok"),
|
|
2661
|
+
cancelLabel: t("gettingStarted.confirm.cancel"),
|
|
2662
|
+
loading: starting,
|
|
2663
|
+
onConfirm: () => {
|
|
2664
|
+
void handleStart();
|
|
2665
|
+
},
|
|
2666
|
+
onCancel: () => setConfirmOpen(false)
|
|
2667
|
+
}
|
|
2668
|
+
)
|
|
2669
|
+
] });
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/locales/en.ts
|
|
2673
|
+
var en = {
|
|
2674
|
+
"gettingStarted.name": "Getting started",
|
|
2675
|
+
"gettingStarted.settings.tab": "Free trial",
|
|
2676
|
+
"gettingStarted.cta.title": "Your store, live",
|
|
2677
|
+
"gettingStarted.cta.remaining.one": "1 step left before the trial",
|
|
2678
|
+
"gettingStarted.cta.remaining.many": "{{count}} steps left before the trial",
|
|
2679
|
+
"gettingStarted.cta.ready": "Start trial \xB7 {{days}} days",
|
|
2680
|
+
"gettingStarted.cta.ring": "Your store, live: {{done}} of {{total}} store steps",
|
|
2681
|
+
"gettingStarted.cta.openList": "Open the list of steps",
|
|
2682
|
+
"gettingStarted.cta.minimize": "Minimize",
|
|
2683
|
+
"gettingStarted.cta.expand": "Show the store progress",
|
|
2684
|
+
"gettingStarted.cta.sheet.minimums": "To start the trial",
|
|
2685
|
+
"gettingStarted.cta.sheet.later": "Whenever you want",
|
|
2686
|
+
"gettingStarted.cta.sheet.review": "Review",
|
|
2687
|
+
"gettingStarted.cta.sheet.configure": "Set up",
|
|
2688
|
+
"gettingStarted.cta.sheet.done": "done",
|
|
2689
|
+
"gettingStarted.cta.sheet.open": "to do",
|
|
2690
|
+
"gettingStarted.cta.sheet.now": "now",
|
|
2691
|
+
"gettingStarted.confirm.title": "Start the {{days}}-day trial?",
|
|
2692
|
+
"gettingStarted.confirm.body": "From now on the clock runs: until {{date}}. Everything unlocked, no card. It cannot be paused.",
|
|
2693
|
+
"gettingStarted.confirm.cancel": "Not yet",
|
|
2694
|
+
"gettingStarted.confirm.ok": "Start trial",
|
|
2695
|
+
"gettingStarted.confirm.starting": "Starting\u2026",
|
|
2696
|
+
"gettingStarted.start.done": "Trial started. Everything is unlocked until {{date}}.",
|
|
2697
|
+
"gettingStarted.start.error": "Could not start. Nothing changed \u2014 try again.",
|
|
2698
|
+
"gettingStarted.start.missing": "Still missing {{what}} to start. The list is up to date.",
|
|
2699
|
+
"gettingStarted.start.missingLabel": "Missing: {{what}}",
|
|
2700
|
+
"gettingStarted.action.missing": "Still missing {{what}} to start the trial.",
|
|
2701
|
+
"gettingStarted.start.refused.no_trial_window": "This account has no trial window to start. Contact support.",
|
|
2702
|
+
"gettingStarted.start.refused.already_converted": "This account is already a subscriber \u2014 the trial does not apply.",
|
|
2703
|
+
"gettingStarted.start.refused.already_paid": "This account is already on a paid plan \u2014 the trial does not apply.",
|
|
2704
|
+
"gettingStarted.minimum.menu": "a menu with at least one product",
|
|
2705
|
+
"gettingStarted.minimum.hours": "opening hours",
|
|
2706
|
+
"gettingStarted.minimum.testOrder": "a test order",
|
|
2707
|
+
"gettingStarted.minimum.join": " and ",
|
|
2708
|
+
"gettingStarted.banner.region": "End of the trial period",
|
|
2709
|
+
"gettingStarted.banner.title": "Your trial ended on {{date}}. The store and your data stay; what belongs to the Pro plan is locked.",
|
|
2710
|
+
"gettingStarted.banner.seeChanges": "See what changes",
|
|
2711
|
+
"gettingStarted.banner.contract": "I want to subscribe",
|
|
2712
|
+
"gettingStarted.banner.phonePrompt": "Someone from Faya calls you today. Confirm your WhatsApp:",
|
|
2713
|
+
"gettingStarted.banner.phoneLabel": "WhatsApp for contact",
|
|
2714
|
+
"gettingStarted.banner.confirm": "Confirm",
|
|
2715
|
+
"gettingStarted.banner.sending": "Sending\u2026",
|
|
2716
|
+
"gettingStarted.banner.back": "Back",
|
|
2717
|
+
"gettingStarted.banner.done": "Deal. We call you within 1 business day. Meanwhile, nothing is lost.",
|
|
2718
|
+
"gettingStarted.banner.error": "Could not register it. Try again.",
|
|
2719
|
+
"gettingStarted.banner.rateLimited": "We already got your request today. We will call you.",
|
|
2720
|
+
"gettingStarted.settings.phase.setup": "Setting up",
|
|
2721
|
+
"gettingStarted.settings.phase.setup.line.one": "1 step left to start the trial",
|
|
2722
|
+
"gettingStarted.settings.phase.setup.line.many": "{{count}} steps left to start the trial",
|
|
2723
|
+
"gettingStarted.settings.phase.setup.ready": "Ready to start the trial",
|
|
2724
|
+
"gettingStarted.settings.phase.setup.deadline": "If you do not start by {{date}}, the trial starts on its own.",
|
|
2725
|
+
"gettingStarted.settings.phase.trial": "On trial",
|
|
2726
|
+
"gettingStarted.settings.phase.trial.line": "Everything unlocked until {{date}}.",
|
|
2727
|
+
"gettingStarted.settings.phase.expired": "Your trial ended",
|
|
2728
|
+
"gettingStarted.settings.phase.expired.line": "Ended on {{date}}. The store and your data stay.",
|
|
2729
|
+
"gettingStarted.settings.phase.converted": "Plan active",
|
|
2730
|
+
"gettingStarted.settings.phase.converted.line": "Thank you for staying with us.",
|
|
2731
|
+
"gettingStarted.settings.loading": "Loading your journey\u2026",
|
|
2732
|
+
"gettingStarted.settings.loadError": "Could not load your journey. Try again.",
|
|
2733
|
+
"gettingStarted.settings.retry": "Try again",
|
|
2734
|
+
"gettingStarted.settings.plans": "See plans",
|
|
2735
|
+
"gettingStarted.settings.contract.note": "Anything you want to tell us? (optional)",
|
|
2736
|
+
"gettingStarted.settings.contract.done": "Deal. We call you within 1 business day.",
|
|
2737
|
+
"gettingStarted.settings.minimums.title": "To start the trial",
|
|
2738
|
+
"gettingStarted.settings.minimums.description": "Bank, WhatsApp and printer block nothing \u2014 they wait for whenever you want.",
|
|
2739
|
+
"gettingStarted.settings.minimums.menu": "A menu with at least one product",
|
|
2740
|
+
"gettingStarted.settings.minimums.hours": "Opening hours",
|
|
2741
|
+
"gettingStarted.settings.minimums.testOrder": "A test order",
|
|
2742
|
+
"gettingStarted.settings.minimums.done": "done",
|
|
2743
|
+
"gettingStarted.settings.minimums.pending": "to do",
|
|
2744
|
+
"gettingStarted.settings.hours.title": "Opening hours",
|
|
2745
|
+
"gettingStarted.settings.hours.description": "When the store opens and closes. The digital menu shows open/closed from them.",
|
|
2746
|
+
"gettingStarted.settings.hours.template.weekdays": "Mon\u2013Sat, 11am to 11pm",
|
|
2747
|
+
"gettingStarted.settings.hours.template.everyday": "Every day, 11am to 11pm",
|
|
2748
|
+
"gettingStarted.settings.hours.template.weekend": "Weekends only, 11am to 11pm",
|
|
2749
|
+
"gettingStarted.settings.day.mon": "Monday",
|
|
2750
|
+
"gettingStarted.settings.day.tue": "Tuesday",
|
|
2751
|
+
"gettingStarted.settings.day.wed": "Wednesday",
|
|
2752
|
+
"gettingStarted.settings.day.thu": "Thursday",
|
|
2753
|
+
"gettingStarted.settings.day.fri": "Friday",
|
|
2754
|
+
"gettingStarted.settings.day.sat": "Saturday",
|
|
2755
|
+
"gettingStarted.settings.day.sun": "Sunday",
|
|
2756
|
+
"gettingStarted.settings.payments.title": "Payment methods",
|
|
2757
|
+
"gettingStarted.settings.payments.description": "What the store accepts at the counter and on delivery. Card terminal and integrated Pix live in Connectors, whenever you want.",
|
|
2758
|
+
"gettingStarted.settings.payments.pix": "Pix",
|
|
2759
|
+
"gettingStarted.settings.payments.cash": "Cash",
|
|
2760
|
+
"gettingStarted.settings.payments.card": "Card on the terminal",
|
|
2761
|
+
"gettingStarted.settings.payments.meal-voucher": "Meal voucher",
|
|
2762
|
+
"gettingStarted.settings.payments.online": "Online payment",
|
|
2763
|
+
"gettingStarted.settings.saving": "Saving\u2026",
|
|
2764
|
+
"gettingStarted.settings.saved": "Saved",
|
|
2765
|
+
"gettingStarted.settings.saveError": "Could not save: {{message}}",
|
|
2766
|
+
"gettingStarted.settings.offline": "No database connection: what you change here is not saved.",
|
|
2767
|
+
"gettingStarted.card.title": "Your store, live",
|
|
2768
|
+
"gettingStarted.card.steps": "steps",
|
|
2769
|
+
"gettingStarted.card.aria": "Your store, live: {{done}} of {{total}} steps done",
|
|
2770
|
+
"gettingStarted.card.subtitle.setup.one": "1 step left before the trial",
|
|
2771
|
+
"gettingStarted.card.subtitle.setup.many": "{{count}} steps left before the trial",
|
|
2772
|
+
"gettingStarted.card.subtitle.ready": "Ready to start the trial",
|
|
2773
|
+
"gettingStarted.card.subtitle.trial": "On trial until {{date}}",
|
|
2774
|
+
"gettingStarted.card.subtitle.expired": "Your trial ended on {{date}}",
|
|
2775
|
+
"gettingStarted.card.subtitle.converted": "Plan active",
|
|
2776
|
+
"gettingStarted.card.group.launch": "To start the trial",
|
|
2777
|
+
"gettingStarted.card.group.later": "Whenever you want",
|
|
2778
|
+
"gettingStarted.card.optional": "optional",
|
|
2779
|
+
"gettingStarted.card.withAssistant": "Do it with the assistant",
|
|
2780
|
+
"gettingStarted.card.noClock": "The server has no trial clock yet; progress here is computed on the spot.",
|
|
2781
|
+
"gettingStarted.start.noClock": "The server has no trial clock yet; tell support.",
|
|
2782
|
+
"gettingStarted.settings.phase.setup.offline": "No trial clock on the server: phase and steps are computed here. Tell support.",
|
|
2783
|
+
"gettingStarted.d0.region": "Last day of the trial",
|
|
2784
|
+
"gettingStarted.d0.title": "Last day of the trial \u2014 it ends today at {{time}}. After that the Pro plan locks; your data stays.",
|
|
2785
|
+
"gettingStarted.d0.subscribe": "Subscribe",
|
|
2786
|
+
"gettingStarted.d0.seeChanges": "See what changes",
|
|
2787
|
+
"gettingStarted.d0.dismiss": "Dismiss for today",
|
|
2788
|
+
// The assistant's cards and trace: chat.tool.<name> is the title, chat.field.<tool>.<param> the field label.
|
|
2789
|
+
"chat.tool.readTrialJourney": "See the store setup",
|
|
2790
|
+
"chat.tool.proposeStoreHours": "Draft opening hours",
|
|
2791
|
+
"chat.tool.saveStoreHours": "Save opening hours",
|
|
2792
|
+
"chat.tool.savePaymentMethods": "Save payment methods",
|
|
2793
|
+
"chat.tool.saveStoreProfile": "Save store profile",
|
|
2794
|
+
"chat.tool.startTrial": "Start the free trial",
|
|
2795
|
+
"chat.tool.requestContract": "Ask to subscribe",
|
|
2796
|
+
"chat.tool.exploreWithDemoData": "Create a sample menu",
|
|
2797
|
+
"chat.field.saveStoreHours.week": "Days",
|
|
2798
|
+
"chat.field.saveStoreHours.summary": "Summary",
|
|
2799
|
+
"chat.field.saveStoreHours.template": "Template",
|
|
2800
|
+
"chat.field.savePaymentMethods.methods": "Methods",
|
|
2801
|
+
"chat.field.savePaymentMethods.summary": "Summary",
|
|
2802
|
+
"chat.field.saveStoreProfile.serviceModes": "Service modes",
|
|
2803
|
+
"chat.field.saveStoreProfile.tables": "Tables",
|
|
2804
|
+
"chat.field.saveStoreProfile.marketplaceApps": "Delivery apps",
|
|
2805
|
+
"chat.field.saveStoreProfile.deliveryMethod": "Delivery pricing",
|
|
2806
|
+
"chat.field.saveStoreProfile.operation": "Kind of place",
|
|
2807
|
+
"chat.field.saveStoreProfile.welcomeArticle": "Article",
|
|
2808
|
+
"chat.field.saveStoreProfile.summary": "Summary",
|
|
2809
|
+
"chat.field.requestContract.note": "Note"
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
// src/locales/pt-BR.ts
|
|
2813
|
+
var ptBR = {
|
|
2814
|
+
"gettingStarted.name": "Primeiros passos",
|
|
2815
|
+
"gettingStarted.settings.tab": "Teste gr\xE1tis",
|
|
2816
|
+
"gettingStarted.cta.title": "Sua loja no ar",
|
|
2817
|
+
"gettingStarted.cta.remaining.one": "Falta 1 passo para o teste",
|
|
2818
|
+
"gettingStarted.cta.remaining.many": "Faltam {{count}} passos para o teste",
|
|
2819
|
+
"gettingStarted.cta.ready": "Iniciar teste \xB7 {{days}} dias",
|
|
2820
|
+
"gettingStarted.cta.ring": "Sua loja no ar: {{done}} de {{total}} passos da loja",
|
|
2821
|
+
"gettingStarted.cta.openList": "Abrir a lista de passos",
|
|
2822
|
+
"gettingStarted.cta.minimize": "Minimizar",
|
|
2823
|
+
"gettingStarted.cta.expand": "Mostrar o progresso da loja",
|
|
2824
|
+
"gettingStarted.cta.sheet.minimums": "Para iniciar o teste",
|
|
2825
|
+
"gettingStarted.cta.sheet.later": "Quando quiser",
|
|
2826
|
+
"gettingStarted.cta.sheet.review": "Revisar",
|
|
2827
|
+
"gettingStarted.cta.sheet.configure": "Configurar",
|
|
2828
|
+
"gettingStarted.cta.sheet.done": "feito",
|
|
2829
|
+
"gettingStarted.cta.sheet.open": "a fazer",
|
|
2830
|
+
"gettingStarted.cta.sheet.now": "agora",
|
|
2831
|
+
"gettingStarted.confirm.title": "Come\xE7ar os {{days}} dias de teste?",
|
|
2832
|
+
"gettingStarted.confirm.body": "A partir de agora o prazo corre: at\xE9 {{date}}. Tudo liberado, sem cart\xE3o. N\xE3o d\xE1 para pausar.",
|
|
2833
|
+
"gettingStarted.confirm.cancel": "Ainda n\xE3o",
|
|
2834
|
+
"gettingStarted.confirm.ok": "Iniciar teste",
|
|
2835
|
+
"gettingStarted.confirm.starting": "Iniciando\u2026",
|
|
2836
|
+
"gettingStarted.start.done": "Teste iniciado. At\xE9 {{date}} voc\xEA tem tudo liberado.",
|
|
2837
|
+
"gettingStarted.start.error": "N\xE3o consegui iniciar. Nada mudou \u2014 tenta de novo.",
|
|
2838
|
+
"gettingStarted.start.missing": "Falta {{what}} para iniciar. J\xE1 atualizei a lista.",
|
|
2839
|
+
"gettingStarted.start.missingLabel": "Falta: {{what}}",
|
|
2840
|
+
"gettingStarted.action.missing": "Falta {{what}} para iniciar o teste.",
|
|
2841
|
+
"gettingStarted.start.refused.no_trial_window": "Esta conta n\xE3o tem per\xEDodo de teste para iniciar. Fale com o suporte.",
|
|
2842
|
+
"gettingStarted.start.refused.already_converted": "Esta conta j\xE1 \xE9 assinante \u2014 o teste n\xE3o se aplica.",
|
|
2843
|
+
"gettingStarted.start.refused.already_paid": "Esta conta j\xE1 est\xE1 num plano pago \u2014 o teste n\xE3o se aplica.",
|
|
2844
|
+
"gettingStarted.minimum.menu": "card\xE1pio com ao menos um produto",
|
|
2845
|
+
"gettingStarted.minimum.hours": "hor\xE1rio de funcionamento",
|
|
2846
|
+
"gettingStarted.minimum.testOrder": "um pedido de teste",
|
|
2847
|
+
"gettingStarted.minimum.join": " e ",
|
|
2848
|
+
"gettingStarted.banner.region": "Fim do per\xEDodo de teste",
|
|
2849
|
+
"gettingStarted.banner.title": "Seu teste terminou em {{date}}. A loja e seus dados continuam; o que \xE9 do plano Pro ficou com cadeado.",
|
|
2850
|
+
"gettingStarted.banner.seeChanges": "Ver o que muda",
|
|
2851
|
+
"gettingStarted.banner.contract": "Quero contratar",
|
|
2852
|
+
"gettingStarted.banner.phonePrompt": "Algu\xE9m da Faya te chama hoje. Confirma o WhatsApp:",
|
|
2853
|
+
"gettingStarted.banner.phoneLabel": "WhatsApp para contato",
|
|
2854
|
+
"gettingStarted.banner.confirm": "Confirmar",
|
|
2855
|
+
"gettingStarted.banner.sending": "Enviando\u2026",
|
|
2856
|
+
"gettingStarted.banner.back": "Voltar",
|
|
2857
|
+
"gettingStarted.banner.done": "Combinado. A gente te chama em at\xE9 1 dia \xFAtil. Enquanto isso, nada se perde.",
|
|
2858
|
+
"gettingStarted.banner.error": "N\xE3o consegui registrar. Tenta de novo.",
|
|
2859
|
+
"gettingStarted.banner.rateLimited": "J\xE1 recebemos seu pedido hoje. A gente te chama.",
|
|
2860
|
+
"gettingStarted.settings.phase.setup": "Em configura\xE7\xE3o",
|
|
2861
|
+
"gettingStarted.settings.phase.setup.line.one": "Falta 1 passo para iniciar o teste",
|
|
2862
|
+
"gettingStarted.settings.phase.setup.line.many": "Faltam {{count}} passos para iniciar o teste",
|
|
2863
|
+
"gettingStarted.settings.phase.setup.ready": "Pronto para iniciar o teste",
|
|
2864
|
+
"gettingStarted.settings.phase.setup.deadline": "Se n\xE3o iniciar at\xE9 {{date}}, o teste come\xE7a sozinho.",
|
|
2865
|
+
"gettingStarted.settings.phase.trial": "Em teste",
|
|
2866
|
+
"gettingStarted.settings.phase.trial.line": "Tudo liberado at\xE9 {{date}}.",
|
|
2867
|
+
"gettingStarted.settings.phase.expired": "Seu teste terminou",
|
|
2868
|
+
"gettingStarted.settings.phase.expired.line": "Terminou em {{date}}. A loja e seus dados continuam.",
|
|
2869
|
+
"gettingStarted.settings.phase.converted": "Plano ativo",
|
|
2870
|
+
"gettingStarted.settings.phase.converted.line": "Obrigado por ficar com a gente.",
|
|
2871
|
+
"gettingStarted.settings.loading": "Carregando a sua jornada\u2026",
|
|
2872
|
+
"gettingStarted.settings.loadError": "N\xE3o consegui carregar a sua jornada. Tenta de novo.",
|
|
2873
|
+
"gettingStarted.settings.retry": "Tentar de novo",
|
|
2874
|
+
"gettingStarted.settings.plans": "Ver planos",
|
|
2875
|
+
"gettingStarted.settings.contract.note": "Quer dizer algo para a gente? (opcional)",
|
|
2876
|
+
"gettingStarted.settings.contract.done": "Combinado. A gente te chama em at\xE9 1 dia \xFAtil.",
|
|
2877
|
+
"gettingStarted.settings.minimums.title": "Para iniciar o teste",
|
|
2878
|
+
"gettingStarted.settings.minimums.description": "Banco, WhatsApp e impressora n\xE3o travam nada \u2014 ficam para quando quiser.",
|
|
2879
|
+
"gettingStarted.settings.minimums.menu": "Card\xE1pio com ao menos um produto",
|
|
2880
|
+
"gettingStarted.settings.minimums.hours": "Hor\xE1rio de funcionamento",
|
|
2881
|
+
"gettingStarted.settings.minimums.testOrder": "Um pedido de teste",
|
|
2882
|
+
"gettingStarted.settings.minimums.done": "feito",
|
|
2883
|
+
"gettingStarted.settings.minimums.pending": "a fazer",
|
|
2884
|
+
"gettingStarted.settings.hours.title": "Hor\xE1rios",
|
|
2885
|
+
"gettingStarted.settings.hours.description": "Quando a casa abre e fecha. O card\xE1pio digital mostra aberto/fechado por eles.",
|
|
2886
|
+
"gettingStarted.settings.hours.template.weekdays": "Seg\u2013s\xE1b, 11h \xE0s 23h",
|
|
2887
|
+
"gettingStarted.settings.hours.template.everyday": "Todos os dias, 11h \xE0s 23h",
|
|
2888
|
+
"gettingStarted.settings.hours.template.weekend": "S\xF3 fim de semana, 11h \xE0s 23h",
|
|
2889
|
+
"gettingStarted.settings.day.mon": "Segunda",
|
|
2890
|
+
"gettingStarted.settings.day.tue": "Ter\xE7a",
|
|
2891
|
+
"gettingStarted.settings.day.wed": "Quarta",
|
|
2892
|
+
"gettingStarted.settings.day.thu": "Quinta",
|
|
2893
|
+
"gettingStarted.settings.day.fri": "Sexta",
|
|
2894
|
+
"gettingStarted.settings.day.sat": "S\xE1bado",
|
|
2895
|
+
"gettingStarted.settings.day.sun": "Domingo",
|
|
2896
|
+
"gettingStarted.settings.payments.title": "Formas de pagamento",
|
|
2897
|
+
"gettingStarted.settings.payments.description": "O que a casa aceita no balc\xE3o e na entrega. Maquininha e Pix integrados ficam em Conectores, quando quiser.",
|
|
2898
|
+
"gettingStarted.settings.payments.pix": "Pix",
|
|
2899
|
+
"gettingStarted.settings.payments.cash": "Dinheiro",
|
|
2900
|
+
"gettingStarted.settings.payments.card": "Cart\xE3o na maquininha",
|
|
2901
|
+
"gettingStarted.settings.payments.meal-voucher": "Vale-refei\xE7\xE3o",
|
|
2902
|
+
"gettingStarted.settings.payments.online": "Pagamento online",
|
|
2903
|
+
"gettingStarted.settings.saving": "Salvando\u2026",
|
|
2904
|
+
"gettingStarted.settings.saved": "Salvo",
|
|
2905
|
+
"gettingStarted.settings.saveError": "N\xE3o consegui salvar: {{message}}",
|
|
2906
|
+
"gettingStarted.settings.offline": "Sem conex\xE3o com o banco: o que voc\xEA mudar aqui n\xE3o fica gravado.",
|
|
2907
|
+
"gettingStarted.card.title": "Sua loja no ar",
|
|
2908
|
+
"gettingStarted.card.steps": "passos",
|
|
2909
|
+
"gettingStarted.card.aria": "Sua loja no ar, {{done}} de {{total}} passos feitos",
|
|
2910
|
+
"gettingStarted.card.subtitle.setup.one": "Falta 1 passo para iniciar o teste",
|
|
2911
|
+
"gettingStarted.card.subtitle.setup.many": "Faltam {{count}} passos para iniciar o teste",
|
|
2912
|
+
"gettingStarted.card.subtitle.ready": "Pronto para iniciar o teste",
|
|
2913
|
+
"gettingStarted.card.subtitle.trial": "Em teste at\xE9 {{date}}",
|
|
2914
|
+
"gettingStarted.card.subtitle.expired": "Seu teste terminou em {{date}}",
|
|
2915
|
+
"gettingStarted.card.subtitle.converted": "Plano ativo",
|
|
2916
|
+
"gettingStarted.card.group.launch": "Para iniciar o teste",
|
|
2917
|
+
"gettingStarted.card.group.later": "Quando quiser",
|
|
2918
|
+
"gettingStarted.card.optional": "opcional",
|
|
2919
|
+
"gettingStarted.card.withAssistant": "Fazer com o assistente",
|
|
2920
|
+
"gettingStarted.card.noClock": "O servidor ainda n\xE3o tem o rel\xF3gio do teste; o progresso aqui \xE9 calculado na hora.",
|
|
2921
|
+
"gettingStarted.start.noClock": "O servidor ainda n\xE3o tem o rel\xF3gio do teste; avise o suporte.",
|
|
2922
|
+
"gettingStarted.settings.phase.setup.offline": "Sem o rel\xF3gio do teste no servidor: fase e passos calculados aqui. Avise o suporte.",
|
|
2923
|
+
"gettingStarted.d0.region": "\xDAltimo dia do teste",
|
|
2924
|
+
"gettingStarted.d0.title": "\xDAltimo dia do teste \u2014 termina hoje \xE0s {{time}}. Depois, o que \xE9 do plano Pro fica com cadeado; seus dados ficam.",
|
|
2925
|
+
"gettingStarted.d0.subscribe": "Assinar",
|
|
2926
|
+
"gettingStarted.d0.seeChanges": "Ver o que muda",
|
|
2927
|
+
"gettingStarted.d0.dismiss": "Dispensar por hoje",
|
|
2928
|
+
// The assistant's cards and trace: chat.tool.<name> is the title, chat.field.<tool>.<param> the field label.
|
|
2929
|
+
"chat.tool.readTrialJourney": "Ver a configura\xE7\xE3o da loja",
|
|
2930
|
+
"chat.tool.proposeStoreHours": "Montar hor\xE1rio de funcionamento",
|
|
2931
|
+
"chat.tool.saveStoreHours": "Gravar hor\xE1rio de funcionamento",
|
|
2932
|
+
"chat.tool.savePaymentMethods": "Gravar formas de pagamento",
|
|
2933
|
+
"chat.tool.saveStoreProfile": "Gravar perfil da casa",
|
|
2934
|
+
"chat.tool.startTrial": "Iniciar o teste gr\xE1tis",
|
|
2935
|
+
"chat.tool.requestContract": "Pedir para contratar",
|
|
2936
|
+
"chat.tool.exploreWithDemoData": "Criar card\xE1pio de exemplo",
|
|
2937
|
+
"chat.field.saveStoreHours.week": "Dias",
|
|
2938
|
+
"chat.field.saveStoreHours.summary": "Resumo",
|
|
2939
|
+
"chat.field.saveStoreHours.template": "Modelo",
|
|
2940
|
+
"chat.field.savePaymentMethods.methods": "Formas",
|
|
2941
|
+
"chat.field.savePaymentMethods.summary": "Resumo",
|
|
2942
|
+
"chat.field.saveStoreProfile.serviceModes": "Como atende",
|
|
2943
|
+
"chat.field.saveStoreProfile.tables": "Mesas",
|
|
2944
|
+
"chat.field.saveStoreProfile.marketplaceApps": "Apps de entrega",
|
|
2945
|
+
"chat.field.saveStoreProfile.deliveryMethod": "Cobran\xE7a da entrega",
|
|
2946
|
+
"chat.field.saveStoreProfile.operation": "Tipo de casa",
|
|
2947
|
+
"chat.field.saveStoreProfile.welcomeArticle": "Artigo",
|
|
2948
|
+
"chat.field.saveStoreProfile.summary": "Resumo",
|
|
2949
|
+
"chat.field.requestContract.note": "Observa\xE7\xE3o"
|
|
2950
|
+
};
|
|
2951
|
+
|
|
2952
|
+
// src/locales/index.ts
|
|
2953
|
+
var gettingStartedLocales = {
|
|
2954
|
+
en,
|
|
2955
|
+
"pt-BR": ptBR
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
export { CARD_ANCHOR, DEFAULT_ROUTES, DELIVERY_METHODS, GettingStartedError, JOURNEY_TOUR_ID, MINIMUM_IDS, OPERATIONS, PROFILE_KEYS, PROGRAM_ID, ProgressRing, RPC, SAVE_PROFILE_TOOL, SENSOR_RULED_KEYS, SERVICE_MODES, STORE_PROFILE_STEP_KEY, STORE_SETTINGS_SAVED_EVENT, StartTrialAction, StartTrialCta, StoreLiveCard, T, TABLE_BANDS, TRIAL_TAB_ANCHOR, TrialEndBanner, TrialSettingsTab, WEEKDAYS, buildJourneyClosingStop, buildJourneyTour, buildOwnJourneyTour, buildStoreLiveProgram, businessProfileStoreProfileProvider, clientJourney, configureStoreProfile, configureStoreSettings, countJourney, countMinimums, createMockGettingStartedProvider, createSupabaseGettingStartedProvider, currentJourney, declareJourney, derivePhase, formatEndTime, formatTrialDate, getActiveConfig, getStoreProfileProvider, getStoreSettingsProvider, gettingStartedAiTools, gettingStartedAiToolsFor, gettingStartedLocales, hasOpeningHours, hasPaymentMethods, isForbidden, isMissingClock, journeyTour, loadStoreSettings, minimumLines, minimumSlotOf, noteTrialDaysDivergence, nowStepKey, readStoreSettings, registerGettingStartedToolHandlers, resolveConfig, resolveRoutes, rowDone, saveStoreSetting, selectJourneySteps, setActiveConfig, setHostJourneyPrograms, setJourneyOverride, startJourneyTour, storeProfileAnswered, storeProfileFrom, storeProfileFromAnswers, storeProfileToAnswers, storeSetupRouting, supabaseStoreSettingsProvider, toStepInputs, toTrialSource, trialEndsToday, useJourneyDeclaration, useJourneyStore, useJourneyTourStore, useStartTrial, withGettingStartedContext, withJourneyDeclaration, withoutStoreProfileStep };
|
|
2959
|
+
//# sourceMappingURL=chunk-PP3YVZGM.js.map
|
|
2960
|
+
//# sourceMappingURL=chunk-PP3YVZGM.js.map
|