falak-app-duo 1.1.3 → 1.1.5
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/bin/falak-app-duo +365 -44
- package/package.json +5 -2
- package/templates/default/package.json +4 -0
- package/templates/default/src/data/demo/appointments.js +7 -0
- package/templates/default/src/data/demo/customers.js +7 -0
- package/templates/default/src/data/demo/events.js +7 -0
- package/templates/default/src/data/demo/invoices.js +7 -0
- package/templates/default/src/data/demo/messages.js +7 -0
- package/templates/default/src/data/demo/orders.js +7 -0
- package/templates/default/src/data/demo/payments.js +7 -0
- package/templates/default/src/data/demo/posts.js +7 -0
- package/templates/default/src/data/demo/products.js +7 -0
- package/templates/default/src/data/demo/projects.js +7 -0
- package/templates/default/src/data/demo/tasks.js +7 -0
- package/templates/default/src/data/demo/users.js +9 -0
- package/templates/default/src/data/demoAccounts.js +135 -0
- package/templates/default/src/data/roles.js +25 -0
- package/templates/default/src/i18n/locales/ar.json +19 -3
- package/templates/default/src/i18n/locales/de.json +19 -3
- package/templates/default/src/i18n/locales/en.json +19 -3
- package/templates/default/src/i18n/locales/es.json +19 -3
- package/templates/default/src/i18n/locales/fr.json +19 -3
- package/templates/default/src/i18n/locales/it.json +19 -3
- package/templates/default/src/i18n/locales/ru.json +19 -3
- package/templates/default/src/main.js +11 -0
- package/templates/default/src/stores/auth.js +5 -0
- package/templates/default/src/views/Profile.vue +13 -0
- package/templates/default/src/views/Register.vue +17 -2
- package/templates/default/vite.config.js +2 -1
package/bin/falak-app-duo
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import fs from "fs-extra";
|
|
@@ -34,6 +35,182 @@ const LOCALE_LABELS = {
|
|
|
34
35
|
|
|
35
36
|
const EXTRA_LOCALE_FILES = ["ar.json", "de.json", "es.json", "fr.json", "it.json", "ru.json"];
|
|
36
37
|
|
|
38
|
+
const ROLE_ORDER = ["master", "super_admin", "admin", "user"];
|
|
39
|
+
const ROLE_META = {
|
|
40
|
+
master: { label: "roles.master.title", badge: "bg-rose-500/10 text-rose-300 ring-rose-400/30" },
|
|
41
|
+
super_admin: { label: "roles.super_admin.title", badge: "bg-amber-500/10 text-amber-300 ring-amber-400/30" },
|
|
42
|
+
admin: { label: "roles.admin.title", badge: "bg-cyan-500/10 text-cyan-300 ring-cyan-400/30" },
|
|
43
|
+
user: { label: "roles.user.title", badge: "bg-emerald-500/10 text-emerald-300 ring-emerald-400/30" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEMO_ENTITIES = [
|
|
47
|
+
"users",
|
|
48
|
+
"products",
|
|
49
|
+
"orders",
|
|
50
|
+
"customers",
|
|
51
|
+
"invoices",
|
|
52
|
+
"payments",
|
|
53
|
+
"events",
|
|
54
|
+
"posts",
|
|
55
|
+
"messages",
|
|
56
|
+
"tasks",
|
|
57
|
+
"projects",
|
|
58
|
+
"appointments",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const DEMO_ROLE_SEED = {
|
|
62
|
+
master: {
|
|
63
|
+
name: "Aya El-Amin",
|
|
64
|
+
email: "master@demo.dev",
|
|
65
|
+
phone: "+201000000001",
|
|
66
|
+
country_code: "EG",
|
|
67
|
+
city: "Cairo",
|
|
68
|
+
channel: "telegram",
|
|
69
|
+
channel_handle: "@aya_master",
|
|
70
|
+
ai_model: "llama-3.3-70b-versatile",
|
|
71
|
+
ai_enabled: true,
|
|
72
|
+
telegram_username: "@aya_master",
|
|
73
|
+
},
|
|
74
|
+
super_admin: {
|
|
75
|
+
name: "Omar El-Sayed",
|
|
76
|
+
email: "superadmin@demo.dev",
|
|
77
|
+
phone: "+966500000002",
|
|
78
|
+
country_code: "SA",
|
|
79
|
+
city: "Riyadh",
|
|
80
|
+
channel: "whatsapp",
|
|
81
|
+
channel_handle: "+966500000002",
|
|
82
|
+
ai_model: "mixtral-8x7b-32768",
|
|
83
|
+
ai_enabled: true,
|
|
84
|
+
telegram_username: "",
|
|
85
|
+
},
|
|
86
|
+
admin: {
|
|
87
|
+
name: "Layla Haddad",
|
|
88
|
+
email: "admin@demo.dev",
|
|
89
|
+
phone: "+971500000003",
|
|
90
|
+
country_code: "AE",
|
|
91
|
+
city: "Dubai",
|
|
92
|
+
channel: "none",
|
|
93
|
+
channel_handle: "",
|
|
94
|
+
ai_model: "llama-3.1-8b-instant",
|
|
95
|
+
ai_enabled: false,
|
|
96
|
+
telegram_username: "",
|
|
97
|
+
},
|
|
98
|
+
user: {
|
|
99
|
+
name: "Karim Nasser",
|
|
100
|
+
email: "user@demo.dev",
|
|
101
|
+
phone: "+120200000004",
|
|
102
|
+
country_code: "US",
|
|
103
|
+
city: "New York",
|
|
104
|
+
channel: "none",
|
|
105
|
+
channel_handle: "",
|
|
106
|
+
ai_model: "gemma2-9b-it",
|
|
107
|
+
ai_enabled: false,
|
|
108
|
+
telegram_username: "",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const DEMO_NOTIFICATIONS = {
|
|
113
|
+
account: { email: true, push: true, sms: false, telegram: false },
|
|
114
|
+
security: { email: true, push: true, sms: true, telegram: false },
|
|
115
|
+
marketing: { email: false, push: false, sms: false, telegram: false },
|
|
116
|
+
updates: { email: true, push: false, sms: false, telegram: false },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function buildRolesModule(roleKeys) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
lines.push("// UserRole support. Generated by the scaffolder: only the roles");
|
|
122
|
+
lines.push("// you picked at scaffold time are kept; an empty ROLES means UserRole");
|
|
123
|
+
lines.push('// support is off. `label` is a vue-i18n key (see i18n/locales/*.json).');
|
|
124
|
+
lines.push("export const ROLES = [");
|
|
125
|
+
for (const key of ROLE_ORDER.filter((k) => roleKeys.includes(k))) {
|
|
126
|
+
lines.push(` { key: "${key}", label: "${ROLE_META[key].label}", badge: "${ROLE_META[key].badge}" },`);
|
|
127
|
+
}
|
|
128
|
+
lines.push("];");
|
|
129
|
+
return lines.join("\n") + "\n";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function demoSettingsFor(roleKey) {
|
|
133
|
+
const seed = roleKey ? DEMO_ROLE_SEED[roleKey] : null;
|
|
134
|
+
return {
|
|
135
|
+
notification_preferences: DEMO_NOTIFICATIONS,
|
|
136
|
+
two_factor_enabled: false,
|
|
137
|
+
ai_agent: {
|
|
138
|
+
enabled: seed ? seed.ai_enabled : false,
|
|
139
|
+
model: seed ? seed.ai_model : "llama-3.3-70b-versatile",
|
|
140
|
+
temperature: 0.7,
|
|
141
|
+
reasoning: false,
|
|
142
|
+
system_prompt: "",
|
|
143
|
+
},
|
|
144
|
+
telegram: { bot_token: "", chat_id: "", username: seed ? seed.telegram_username : "" },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// roleKeys == [] => a single normal demo user (no role on the account).
|
|
149
|
+
function buildDemoAccountsModule(roleKeys) {
|
|
150
|
+
const chosen = ROLE_ORDER.filter((k) => roleKeys.includes(k));
|
|
151
|
+
const accounts = chosen.length
|
|
152
|
+
? chosen.map((key) => {
|
|
153
|
+
const s = DEMO_ROLE_SEED[key];
|
|
154
|
+
return {
|
|
155
|
+
role: key,
|
|
156
|
+
email: s.email,
|
|
157
|
+
password: "password",
|
|
158
|
+
profile: {
|
|
159
|
+
name: s.name,
|
|
160
|
+
email: s.email,
|
|
161
|
+
phone: s.phone,
|
|
162
|
+
country_code: s.country_code,
|
|
163
|
+
city: s.city,
|
|
164
|
+
channel: s.channel,
|
|
165
|
+
channel_handle: s.channel_handle,
|
|
166
|
+
photo: null,
|
|
167
|
+
},
|
|
168
|
+
settings: demoSettingsFor(key),
|
|
169
|
+
};
|
|
170
|
+
})
|
|
171
|
+
: [
|
|
172
|
+
{
|
|
173
|
+
role: null,
|
|
174
|
+
email: "demo@demo.dev",
|
|
175
|
+
password: "password",
|
|
176
|
+
profile: {
|
|
177
|
+
name: "Demo User",
|
|
178
|
+
email: "demo@demo.dev",
|
|
179
|
+
phone: "+201000000009",
|
|
180
|
+
country_code: "EG",
|
|
181
|
+
city: "Cairo",
|
|
182
|
+
channel: "none",
|
|
183
|
+
channel_handle: "",
|
|
184
|
+
photo: null,
|
|
185
|
+
},
|
|
186
|
+
settings: demoSettingsFor(null),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
const lines = [];
|
|
190
|
+
lines.push("// Static demo accounts for testing. Generated by the scaffolder: one");
|
|
191
|
+
lines.push("// account per role you picked, or a single normal demo user when");
|
|
192
|
+
lines.push("// UserRole support is off. Each carries its full profile and default");
|
|
193
|
+
lines.push("// settings so a backend seed (or a first-login init) has everything.");
|
|
194
|
+
lines.push("export const DEMO_ACCOUNTS = [");
|
|
195
|
+
for (const account of accounts) {
|
|
196
|
+
lines.push(" " + JSON.stringify(account) + ",");
|
|
197
|
+
}
|
|
198
|
+
lines.push("];");
|
|
199
|
+
return lines.join("\n") + "\n";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseList(value) {
|
|
203
|
+
return String(value || "")
|
|
204
|
+
.split(",")
|
|
205
|
+
.map((x) => x.trim())
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function keepCanonicalOrder(value, allowed) {
|
|
210
|
+
const picked = parseList(value);
|
|
211
|
+
return allowed.filter((item) => picked.includes(item));
|
|
212
|
+
}
|
|
213
|
+
|
|
37
214
|
function buildI18nModule(locales, defaultLocale) {
|
|
38
215
|
const ordered = ["ar", "en", "es", "de", "fr", "ru", "it"].filter((c) => locales.includes(c));
|
|
39
216
|
const lines = [];
|
|
@@ -81,8 +258,11 @@ program
|
|
|
81
258
|
.name("falak-app")
|
|
82
259
|
.description("Scaffold a Vue 3 app with Ably, Brevo, AES-256 and a Laravel API client baked in.")
|
|
83
260
|
.argument("[project-name]", "Directory to create the project in")
|
|
84
|
-
.option("-y, --yes", "Skip prompts and use defaults (
|
|
261
|
+
.option("-y, --yes", "Skip prompts and use defaults (services + languages only; no roles/demo)")
|
|
85
262
|
.option("--no-rtl", "Exclude Arabic/RTL (English or your chosen LTR languages only)")
|
|
263
|
+
.option("--roles <list>", "Enable UserRole support with the given comma-separated roles (master,super_admin,admin,user)")
|
|
264
|
+
.option("--demo-accounts", "Include static demo accounts (per selected role, or a normal demo user without roles)")
|
|
265
|
+
.option("--demo-data <list>", "Include static demo data for the given comma-separated entities (e.g. products,orders)")
|
|
86
266
|
.parse(process.argv);
|
|
87
267
|
|
|
88
268
|
const [argProjectName] = program.args;
|
|
@@ -119,49 +299,141 @@ async function printBanner() {
|
|
|
119
299
|
async function main() {
|
|
120
300
|
await printBanner();
|
|
121
301
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
302
|
+
const defaults = {
|
|
303
|
+
projectName: argProjectName || "falak-app-duo",
|
|
304
|
+
services: ["ably", "brevo"],
|
|
305
|
+
languages: ["ar"],
|
|
306
|
+
apiBaseUrl: "http://localhost:8000/api",
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const hasFeatureFlags =
|
|
310
|
+
opts.roles !== undefined || opts.demoAccounts !== undefined || opts.demoData !== undefined;
|
|
311
|
+
|
|
312
|
+
// Feature outcomes. Roles/accounts/data only come via explicit flags or
|
|
313
|
+
// interactive prompts — `--yes` keeps the minimal scaffold.
|
|
314
|
+
let roles = opts.yes ? [] : null;
|
|
315
|
+
let demoAccounts = opts.yes ? false : null;
|
|
316
|
+
let demoEntities = opts.yes ? [] : null;
|
|
317
|
+
|
|
318
|
+
let answers;
|
|
319
|
+
if (opts.yes || hasFeatureFlags) {
|
|
320
|
+
answers = { ...defaults };
|
|
321
|
+
} else {
|
|
322
|
+
answers = await prompts(
|
|
323
|
+
[
|
|
324
|
+
{
|
|
325
|
+
type: argProjectName ? null : "text",
|
|
326
|
+
name: "projectName",
|
|
327
|
+
message: "Project name / folder:",
|
|
328
|
+
initial: "falak-app-duo",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
type: "multiselect",
|
|
332
|
+
name: "services",
|
|
333
|
+
message: "Which services do you want wired up?",
|
|
334
|
+
choices: [
|
|
335
|
+
{ title: "Ably (realtime, via Laravel Echo)", value: "ably", selected: true },
|
|
336
|
+
{ title: "Brevo (email/SMS, via backend)", value: "brevo", selected: true },
|
|
337
|
+
],
|
|
338
|
+
hint: "Space to toggle, Enter to confirm",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
type: "multiselect",
|
|
342
|
+
name: "languages",
|
|
343
|
+
message: "Which languages should the app ship with? (English is always included)",
|
|
344
|
+
choices: LANGUAGE_CHOICES.map((c) =>
|
|
345
|
+
c.value === "ar" && !opts.rtl ? { ...c, selected: false } : c
|
|
346
|
+
),
|
|
347
|
+
hint: "Space to toggle, Enter to confirm",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: "text",
|
|
351
|
+
name: "apiBaseUrl",
|
|
352
|
+
message: "Laravel API base URL:",
|
|
353
|
+
initial: "http://localhost:8000/api",
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
{ onCancel: () => process.exit(1) }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---- UserRole support ----
|
|
361
|
+
if (!opts.yes && hasFeatureFlags) {
|
|
362
|
+
roles = opts.roles !== undefined ? keepCanonicalOrder(opts.roles, ROLE_ORDER) : [];
|
|
363
|
+
demoAccounts = Boolean(opts.demoAccounts);
|
|
364
|
+
demoEntities = opts.demoData !== undefined ? keepCanonicalOrder(opts.demoData, DEMO_ENTITIES) : [];
|
|
365
|
+
} else if (!opts.yes) {
|
|
366
|
+
const wantRoles = (
|
|
367
|
+
await prompts(
|
|
368
|
+
{
|
|
369
|
+
type: "toggle",
|
|
370
|
+
name: "v",
|
|
371
|
+
message: "Register accounts with a role (UserRole support)?",
|
|
372
|
+
initial: true,
|
|
373
|
+
active: "yes",
|
|
374
|
+
inactive: "no",
|
|
375
|
+
},
|
|
376
|
+
{ onCancel: () => process.exit(1) }
|
|
377
|
+
)
|
|
378
|
+
).v;
|
|
379
|
+
if (wantRoles) {
|
|
380
|
+
const { r } = await prompts(
|
|
381
|
+
{
|
|
382
|
+
type: "multiselect",
|
|
383
|
+
name: "r",
|
|
384
|
+
message: "Which roles should auth support?",
|
|
385
|
+
choices: ROLE_ORDER.map((k) => ({ title: k, value: k, selected: true })),
|
|
386
|
+
hint: "Space to toggle, Enter to confirm",
|
|
387
|
+
},
|
|
388
|
+
{ onCancel: () => process.exit(1) }
|
|
389
|
+
);
|
|
390
|
+
roles = r || [];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const wantAccounts = (
|
|
394
|
+
await prompts(
|
|
395
|
+
{
|
|
396
|
+
type: "toggle",
|
|
397
|
+
name: "v",
|
|
398
|
+
message: roles.length
|
|
399
|
+
? "Create demo accounts (one per selected role)?"
|
|
400
|
+
: "Create a normal demo account?",
|
|
401
|
+
initial: true,
|
|
402
|
+
active: "yes",
|
|
403
|
+
inactive: "no",
|
|
404
|
+
},
|
|
405
|
+
{ onCancel: () => process.exit(1) }
|
|
406
|
+
)
|
|
407
|
+
).v;
|
|
408
|
+
demoAccounts = Boolean(wantAccounts);
|
|
409
|
+
|
|
410
|
+
const wantData = (
|
|
411
|
+
await prompts(
|
|
412
|
+
{
|
|
413
|
+
type: "toggle",
|
|
414
|
+
name: "v",
|
|
415
|
+
message: "Create static demo data to test with?",
|
|
416
|
+
initial: true,
|
|
417
|
+
active: "yes",
|
|
418
|
+
inactive: "no",
|
|
419
|
+
},
|
|
420
|
+
{ onCancel: () => process.exit(1) }
|
|
421
|
+
)
|
|
422
|
+
).v;
|
|
423
|
+
if (wantData) {
|
|
424
|
+
const { e } = await prompts(
|
|
425
|
+
{
|
|
426
|
+
type: "multiselect",
|
|
427
|
+
name: "e",
|
|
428
|
+
message: "Which demo-data entities should the app ship with?",
|
|
429
|
+
choices: DEMO_ENTITIES.map((k) => ({ title: k, value: k, selected: true })),
|
|
430
|
+
hint: "Space to toggle, Enter to confirm",
|
|
431
|
+
},
|
|
163
432
|
{ onCancel: () => process.exit(1) }
|
|
164
433
|
);
|
|
434
|
+
demoEntities = e || [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
165
437
|
|
|
166
438
|
// The set of locales the app ships: English is always bundled, plus the
|
|
167
439
|
// languages the user picked. `--no-rtl` forces Arabic out (LTR only).
|
|
@@ -237,9 +509,45 @@ async function main() {
|
|
|
237
509
|
await fs.writeFile(appPath, app);
|
|
238
510
|
}
|
|
239
511
|
|
|
512
|
+
// UserRole support: rewrite src/data/roles.js to the picked role set. The
|
|
513
|
+
// file always stays (Register/Profile import it), just emptied when off.
|
|
514
|
+
await fs.writeFile(path.join(targetDir, "src", "data", "roles.js"), buildRolesModule(roles || []));
|
|
515
|
+
|
|
516
|
+
// Demo accounts: static module with one account per role, or a single
|
|
517
|
+
// normal demo user when roles are off. Removed entirely when not wanted.
|
|
518
|
+
const demoAccountsPath = path.join(targetDir, "src", "data", "demoAccounts.js");
|
|
519
|
+
if (demoAccounts) {
|
|
520
|
+
await fs.writeFile(demoAccountsPath, buildDemoAccountsModule(roles || []));
|
|
521
|
+
} else {
|
|
522
|
+
await fs.remove(demoAccountsPath);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Static demo data: keep only the selected entity modules. The whole demo
|
|
526
|
+
// dir is dropped when demo data wasn't picked (nothing imports it).
|
|
527
|
+
const demoDir = path.join(targetDir, "src", "data", "demo");
|
|
528
|
+
for (const entity of DEMO_ENTITIES) {
|
|
529
|
+
if (!demoEntities || !demoEntities.includes(entity)) {
|
|
530
|
+
await fs.remove(path.join(demoDir, `${entity}.js`));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (!demoEntities || (await fs.readdir(demoDir).catch(() => [])).length === 0) {
|
|
534
|
+
await fs.remove(demoDir);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const demoAccountsSummary = demoAccounts
|
|
538
|
+
? roles && roles.length
|
|
539
|
+
? `${roles.length} (${roles.join(", ")})`
|
|
540
|
+
: "1 (normal demo user)"
|
|
541
|
+
: "none";
|
|
542
|
+
|
|
240
543
|
console.log(
|
|
241
544
|
chalk.gray(` Languages wired: ${locales.map((l) => LOCALE_LABELS[l]).join(", ")}${hasRtl ? " (RTL default)" : ""}`)
|
|
242
545
|
);
|
|
546
|
+
console.log(
|
|
547
|
+
chalk.gray(
|
|
548
|
+
` Roles: ${roles && roles.length ? roles.join(", ") : "none"} | Demo accounts: ${demoAccountsSummary} | Demo data: ${demoEntities && demoEntities.length ? demoEntities.join(", ") : "none"}`
|
|
549
|
+
)
|
|
550
|
+
);
|
|
243
551
|
|
|
244
552
|
// Generate a real .env from .env.example with a fresh encryption key
|
|
245
553
|
const envExamplePath = path.join(targetDir, ".env.example");
|
|
@@ -257,7 +565,7 @@ async function main() {
|
|
|
257
565
|
)
|
|
258
566
|
);
|
|
259
567
|
|
|
260
|
-
const runChoice = opts.yes
|
|
568
|
+
const runChoice = opts.yes || hasFeatureFlags
|
|
261
569
|
? "install"
|
|
262
570
|
: (
|
|
263
571
|
await prompts(
|
|
@@ -295,7 +603,20 @@ async function main() {
|
|
|
295
603
|
await runInProject(targetDir, "npm", ["run", "dev"]).catch(() => {});
|
|
296
604
|
}
|
|
297
605
|
|
|
298
|
-
|
|
606
|
+
function isMain() {
|
|
607
|
+
const argv1 = process.argv[1];
|
|
608
|
+
if (!argv1) return false;
|
|
609
|
+
try {
|
|
610
|
+
// Resolve symlinks on both sides: npx/npm exec spawn the bin through the
|
|
611
|
+
// `node_modules/.bin/<name>` symlink, so a raw `argv[1] === import.meta.url`
|
|
612
|
+
// comparison would silently skip main() when run via npx.
|
|
613
|
+
return realpathSync(argv1) === fileURLToPath(import.meta.url);
|
|
614
|
+
} catch {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (isMain()) {
|
|
299
620
|
main().catch((err) => {
|
|
300
621
|
console.error(chalk.red(err));
|
|
301
622
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "falak-app-duo",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Vue 3 boilerplate scaffolder — Ably (realtime), Brevo, AES-256, Arabic/RTL, and a Laravel API client (API key + user token auth). No Firebase, no Groq/Paymob on the frontend (Groq is backend-only; Paymob dropped).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
"ably",
|
|
33
33
|
"realtime"
|
|
34
34
|
],
|
|
35
|
-
"license": "MIT"
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
36
39
|
}
|
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@primeuix/themes": "^1.2.5",
|
|
12
13
|
"ably": "^2.5.0",
|
|
13
14
|
"axios": "^1.7.7",
|
|
14
15
|
"crypto-js": "^4.2.0",
|
|
15
16
|
"laravel-echo": "^1.16.1",
|
|
16
17
|
"pinia": "^2.2.2",
|
|
18
|
+
"primeicons": "^7.0.0",
|
|
19
|
+
"primevue": "^4.5.5",
|
|
17
20
|
"vue": "^3.5.0",
|
|
18
21
|
"vue-i18n": "^9.14.0",
|
|
19
22
|
"vue-router": "^4.4.5"
|
|
20
23
|
},
|
|
21
24
|
"devDependencies": {
|
|
25
|
+
"@vitejs/plugin-basic-ssl": "^1.2.0",
|
|
22
26
|
"@vitejs/plugin-vue": "^5.1.4",
|
|
23
27
|
"autoprefixer": "^10.4.20",
|
|
24
28
|
"postcss": "^8.4.47",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const appointments = [
|
|
2
|
+
{ id: 1, title: "Design review", attendee: "Aya El-Amin", date: "2026-09-15", time: "10:00", status: "confirmed" },
|
|
3
|
+
{ id: 2, title: "Stakeholder sync", attendee: "Nour Farouk", date: "2026-09-15", time: "14:30", status: "confirmed" },
|
|
4
|
+
{ id: 3, title: "Intro call", attendee: "Layla Haddad", date: "2026-09-16", time: "09:15", status: "pending" },
|
|
5
|
+
{ id: 4, title: "User interview", attendee: "Omar El-Sayed", date: "2026-09-17", time: "11:45", status: "confirmed" },
|
|
6
|
+
{ id: 5, title: "Sprint retro", attendee: "Karim Nasser", date: "2026-09-18", time: "16:00", status: "cancelled" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const customers = [
|
|
2
|
+
{ id: 1, name: "Aya El-Amin", email: "aya@example.com", segment: "enterprise", orders: 14, lifetime_value: 3200, created_at: "2025-11-02" },
|
|
3
|
+
{ id: 2, name: "Omar El-Sayed", email: "omar@example.com", segment: "retail", orders: 6, lifetime_value: 880, created_at: "2026-01-24" },
|
|
4
|
+
{ id: 3, name: "Layla Haddad", email: "layla@example.com", segment: "retail", orders: 2, lifetime_value: 428, created_at: "2026-03-18" },
|
|
5
|
+
{ id: 4, name: "Karim Nasser", email: "karim@example.com", segment: "wholesale", orders: 9, lifetime_value: 5410, created_at: "2025-09-15" },
|
|
6
|
+
{ id: 5, name: "Nour Farouk", email: "nour@example.com", segment: "enterprise", orders: 21, lifetime_value: 9800, created_at: "2025-07-30" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const events = [
|
|
2
|
+
{ id: 1, title: "Product Launch 2026", date: "2026-10-12", venue: "Cairo Marriott", capacity: 300, booked: 214, status: "published" },
|
|
3
|
+
{ id: 2, title: "Design Meetup", date: "2026-10-20", venue: "Riyadh Hub", capacity: 120, booked: 98, status: "published" },
|
|
4
|
+
{ id: 3, title: "Founders Breakfast", date: "2026-11-02", venue: "Dubai Mall Lounge", capacity: 40, booked: 40, status: "sold_out" },
|
|
5
|
+
{ id: 4, title: "CS Workshop", date: "2026-11-15", venue: "Online", capacity: 500, booked: 156, status: "draft" },
|
|
6
|
+
{ id: 5, title: "Annual Gala", date: "2026-12-05", venue: "InterContinental", capacity: 800, booked: 512, status: "published" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const invoices = [
|
|
2
|
+
{ id: 2001, number: "INV-2001", customer: "Aya El-Amin", amount: 1240, due_date: "2026-09-30", status: "sent", issued_at: "2026-09-01" },
|
|
3
|
+
{ id: 2002, number: "INV-2002", customer: "Karim Nasser", amount: 3410, due_date: "2026-10-05", status: "partial", issued_at: "2026-09-04" },
|
|
4
|
+
{ id: 2003, number: "INV-2003", customer: "Layla Haddad", amount: 428, due_date: "2026-09-20", status: "overdue", issued_at: "2026-08-05" },
|
|
5
|
+
{ id: 2004, number: "INV-2004", customer: "Nour Farouk", amount: 2280, due_date: "2026-10-12", status: "sent", issued_at: "2026-09-09" },
|
|
6
|
+
{ id: 2005, number: "INV-2005", customer: "Omar El-Sayed", amount: 640, due_date: "2026-10-01", status: "paid", issued_at: "2026-08-28" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const messages = [
|
|
2
|
+
{ id: 1, sender: "Aya El-Amin", channel: "email", preview: "Meeting rescheduled to 3pm…", sent_at: "2026-09-10 09:14", read: true },
|
|
3
|
+
{ id: 2, sender: "Support Bot", channel: "chat", preview: "Your ticket #2918 was resolved", sent_at: "2026-09-10 10:02", read: true },
|
|
4
|
+
{ id: 3, sender: "Omar El-Sayed", channel: "sms", preview: "Can you review the PR when you have a second?", sent_at: "2026-09-11 12:47", read: false },
|
|
5
|
+
{ id: 4, sender: "Finance Team", channel: "email", preview: "October payroll timeline attached", sent_at: "2026-09-11 14:30", read: false },
|
|
6
|
+
{ id: 5, sender: "Layla Haddad", channel: "telegram", preview: "Lunch on Thursday?", sent_at: "2026-09-12 08:05", read: false },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const orders = [
|
|
2
|
+
{ id: 1001, number: "ORD-1001", customer: "Aya El-Amin", status: "paid", items: 3, total: 184, placed_at: "2026-09-01" },
|
|
3
|
+
{ id: 1002, number: "ORD-1002", customer: "Omar El-Sayed", status: "pending", items: 1, total: 89, placed_at: "2026-09-03" },
|
|
4
|
+
{ id: 1003, number: "ORD-1003", customer: "Layla Haddad", status: "shipped", items: 2, total: 428, placed_at: "2026-09-05" },
|
|
5
|
+
{ id: 1004, number: "ORD-1004", customer: "Karim Nasser", status: "cancelled", items: 5, total: 612, placed_at: "2026-09-08" },
|
|
6
|
+
{ id: 1005, number: "ORD-1005", customer: "Nour Farouk", status: "refunded", items: 1, total: 74, placed_at: "2026-09-10" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const payments = [
|
|
2
|
+
{ id: 1, method: "visa", amount: 184, status: "succeeded", reference: "PAY-8821", paid_at: "2026-09-01" },
|
|
3
|
+
{ id: 2, method: "paypal", amount: 428, status: "succeeded", reference: "PAY-8843", paid_at: "2026-09-05" },
|
|
4
|
+
{ id: 3, method: "mastercard", amount: 612, status: "failed", reference: "PAY-8850", paid_at: "2026-09-08" },
|
|
5
|
+
{ id: 4, method: "bank_transfer", amount: 1240, status: "processing", reference: "PAY-8877", paid_at: "2026-09-09" },
|
|
6
|
+
{ id: 5, method: "cash", amount: 74, status: "refunded", reference: "PAY-8892", paid_at: "2026-09-12" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const posts = [
|
|
2
|
+
{ id: 1, title: "Welcome to the new dashboard", author: "Aya El-Amin", status: "published", views: 1240, published_at: "2026-08-01" },
|
|
3
|
+
{ id: 2, title: "How we cut load times by 60%", author: "Omar El-Sayed", status: "published", views: 860, published_at: "2026-08-14" },
|
|
4
|
+
{ id: 3, title: "Roadmap: Q4 2026", author: "Nour Farouk", status: "draft", views: 0, published_at: null },
|
|
5
|
+
{ id: 4, title: "Better onboarding in 5 steps", author: "Layla Haddad", status: "review", views: 0, published_at: null },
|
|
6
|
+
{ id: 5, title: "Security hardening guide", author: "Karim Nasser", status: "published", views: 2230, published_at: "2026-09-02" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const products = [
|
|
2
|
+
{ id: 1, name: "Aurora Lamp", sku: "AUR-001", category: "Home", price: 89, stock: 42, status: "in_stock" },
|
|
3
|
+
{ id: 2, name: "Nimbus Headphones", sku: "NIM-014", category: "Audio", price: 149, stock: 12, status: "in_stock" },
|
|
4
|
+
{ id: 3, name: "Terrace Chair", sku: "TER-022", category: "Furniture", price: 210, stock: 0, status: "out_of_stock" },
|
|
5
|
+
{ id: 4, name: "Lumen Desk", sku: "LUM-008", category: "Furniture", price: 340, stock: 7, status: "in_stock" },
|
|
6
|
+
{ id: 5, name: "Drift Backpack", sku: "DRI-031", category: "Travel", price: 74, stock: 96, status: "low" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const projects = [
|
|
2
|
+
{ id: 1, name: "Atlas Rebrand", owner: "Aya El-Amin", progress: 65, status: "active", deadline: "2026-11-01" },
|
|
3
|
+
{ id: 2, name: "Mobile App v2", owner: "Omar El-Sayed", progress: 28, status: "active", deadline: "2026-12-15" },
|
|
4
|
+
{ id: 3, name: "API V2 Migration", owner: "Karim Nasser", progress: 100, status: "done", deadline: "2026-09-01" },
|
|
5
|
+
{ id: 4, name: "Marketing Site", owner: "Layla Haddad", progress: 0, status: "on_hold", deadline: "2027-01-20" },
|
|
6
|
+
{ id: 5, name: "Automation Playbook", owner: "Nour Farouk", progress: 12, status: "active", deadline: "2026-10-30" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const tasks = [
|
|
2
|
+
{ id: 1, title: "Finalize landing page copy", assignee: "Aya El-Amin", priority: "high", status: "in_progress", due_date: "2026-09-18" },
|
|
3
|
+
{ id: 2, title: "Fix checkout bug #412", assignee: "Omar El-Sayed", priority: "critical", status: "todo", due_date: "2026-09-16" },
|
|
4
|
+
{ id: 3, title: "Update pricing page", assignee: "Layla Haddad", priority: "medium", status: "done", due_date: "2026-09-12" },
|
|
5
|
+
{ id: 4, title: "Migrate database indexes", assignee: "Karim Nasser", priority: "high", status: "todo", due_date: "2026-09-22" },
|
|
6
|
+
{ id: 5, title: "Prepare Q4 roadmap", assignee: "Nour Farouk", priority: "medium", status: "in_progress", due_date: "2026-09-30" },
|
|
7
|
+
];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Static demo data — users. Import in your own code, e.g.
|
|
2
|
+
// `import { users } from "@/data/demo/users";`
|
|
3
|
+
export const users = [
|
|
4
|
+
{ id: 1, name: "Aya El-Amin", email: "aya@example.com", role: "admin", status: "active", joined_at: "2026-01-12" },
|
|
5
|
+
{ id: 2, name: "Omar El-Sayed", email: "omar@example.com", role: "user", status: "active", joined_at: "2026-02-03" },
|
|
6
|
+
{ id: 3, name: "Layla Haddad", email: "layla@example.com", role: "user", status: "pending", joined_at: "2026-03-18" },
|
|
7
|
+
{ id: 4, name: "Karim Nasser", email: "karim@example.com", role: "user", status: "suspended", joined_at: "2026-04-09" },
|
|
8
|
+
{ id: 5, name: "Nour Farouk", email: "nour@example.com", role: "master", status: "active", joined_at: "2026-05-27" },
|
|
9
|
+
];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Static demo accounts for testing. The scaffolder rewrites this file to one
|
|
2
|
+
// account per role you picked (or a single normal demo user when UserRole
|
|
3
|
+
// support is off) and deletes it entirely when demo accounts aren't wanted.
|
|
4
|
+
// Each account carries its full profile and default settings so a backend seed
|
|
5
|
+
// (or a first-login init) has everything it needs.
|
|
6
|
+
export const DEMO_ACCOUNTS = [
|
|
7
|
+
{
|
|
8
|
+
role: "master",
|
|
9
|
+
email: "master@demo.dev",
|
|
10
|
+
password: "password",
|
|
11
|
+
profile: {
|
|
12
|
+
name: "Aya El-Amin",
|
|
13
|
+
email: "master@demo.dev",
|
|
14
|
+
phone: "+201000000001",
|
|
15
|
+
country_code: "EG",
|
|
16
|
+
city: "Cairo",
|
|
17
|
+
channel: "telegram",
|
|
18
|
+
channel_handle: "@aya_master",
|
|
19
|
+
photo: null,
|
|
20
|
+
},
|
|
21
|
+
settings: {
|
|
22
|
+
notification_preferences: {
|
|
23
|
+
account: { email: true, push: true, sms: false, telegram: false },
|
|
24
|
+
security: { email: true, push: true, sms: true, telegram: false },
|
|
25
|
+
marketing: { email: false, push: false, sms: false, telegram: false },
|
|
26
|
+
updates: { email: true, push: false, sms: false, telegram: false },
|
|
27
|
+
},
|
|
28
|
+
two_factor_enabled: false,
|
|
29
|
+
ai_agent: {
|
|
30
|
+
enabled: true,
|
|
31
|
+
model: "llama-3.3-70b-versatile",
|
|
32
|
+
temperature: 0.7,
|
|
33
|
+
reasoning: false,
|
|
34
|
+
system_prompt: "",
|
|
35
|
+
},
|
|
36
|
+
telegram: { bot_token: "", chat_id: "", username: "@aya_master" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
role: "super_admin",
|
|
41
|
+
email: "superadmin@demo.dev",
|
|
42
|
+
password: "password",
|
|
43
|
+
profile: {
|
|
44
|
+
name: "Omar El-Sayed",
|
|
45
|
+
email: "superadmin@demo.dev",
|
|
46
|
+
phone: "+966500000002",
|
|
47
|
+
country_code: "SA",
|
|
48
|
+
city: "Riyadh",
|
|
49
|
+
channel: "whatsapp",
|
|
50
|
+
channel_handle: "+966500000002",
|
|
51
|
+
photo: null,
|
|
52
|
+
},
|
|
53
|
+
settings: {
|
|
54
|
+
notification_preferences: {
|
|
55
|
+
account: { email: true, push: true, sms: false, telegram: false },
|
|
56
|
+
security: { email: true, push: true, sms: true, telegram: false },
|
|
57
|
+
marketing: { email: false, push: false, sms: false, telegram: false },
|
|
58
|
+
updates: { email: true, push: false, sms: false, telegram: false },
|
|
59
|
+
},
|
|
60
|
+
two_factor_enabled: false,
|
|
61
|
+
ai_agent: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
model: "mixtral-8x7b-32768",
|
|
64
|
+
temperature: 0.7,
|
|
65
|
+
reasoning: false,
|
|
66
|
+
system_prompt: "",
|
|
67
|
+
},
|
|
68
|
+
telegram: { bot_token: "", chat_id: "", username: "" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
role: "admin",
|
|
73
|
+
email: "admin@demo.dev",
|
|
74
|
+
password: "password",
|
|
75
|
+
profile: {
|
|
76
|
+
name: "Layla Haddad",
|
|
77
|
+
email: "admin@demo.dev",
|
|
78
|
+
phone: "+971500000003",
|
|
79
|
+
country_code: "AE",
|
|
80
|
+
city: "Dubai",
|
|
81
|
+
channel: "none",
|
|
82
|
+
channel_handle: "",
|
|
83
|
+
photo: null,
|
|
84
|
+
},
|
|
85
|
+
settings: {
|
|
86
|
+
notification_preferences: {
|
|
87
|
+
account: { email: true, push: true, sms: false, telegram: false },
|
|
88
|
+
security: { email: true, push: true, sms: true, telegram: false },
|
|
89
|
+
marketing: { email: false, push: false, sms: false, telegram: false },
|
|
90
|
+
updates: { email: true, push: false, sms: false, telegram: false },
|
|
91
|
+
},
|
|
92
|
+
two_factor_enabled: false,
|
|
93
|
+
ai_agent: {
|
|
94
|
+
enabled: false,
|
|
95
|
+
model: "llama-3.1-8b-instant",
|
|
96
|
+
temperature: 0.5,
|
|
97
|
+
reasoning: false,
|
|
98
|
+
system_prompt: "",
|
|
99
|
+
},
|
|
100
|
+
telegram: { bot_token: "", chat_id: "", username: "" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
role: "user",
|
|
105
|
+
email: "user@demo.dev",
|
|
106
|
+
password: "password",
|
|
107
|
+
profile: {
|
|
108
|
+
name: "Karim Nasser",
|
|
109
|
+
email: "user@demo.dev",
|
|
110
|
+
phone: "+120200000004",
|
|
111
|
+
country_code: "US",
|
|
112
|
+
city: "New York",
|
|
113
|
+
channel: "none",
|
|
114
|
+
channel_handle: "",
|
|
115
|
+
photo: null,
|
|
116
|
+
},
|
|
117
|
+
settings: {
|
|
118
|
+
notification_preferences: {
|
|
119
|
+
account: { email: true, push: true, sms: false, telegram: false },
|
|
120
|
+
security: { email: true, push: true, sms: true, telegram: false },
|
|
121
|
+
marketing: { email: false, push: false, sms: false, telegram: false },
|
|
122
|
+
updates: { email: true, push: false, sms: false, telegram: false },
|
|
123
|
+
},
|
|
124
|
+
two_factor_enabled: false,
|
|
125
|
+
ai_agent: {
|
|
126
|
+
enabled: false,
|
|
127
|
+
model: "gemma2-9b-it",
|
|
128
|
+
temperature: 0.8,
|
|
129
|
+
reasoning: false,
|
|
130
|
+
system_prompt: "",
|
|
131
|
+
},
|
|
132
|
+
telegram: { bot_token: "", chat_id: "", username: "" },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// UserRole support. The scaffolder rewrites this file to keep only the roles
|
|
2
|
+
// you picked at scaffold time; an empty ROLES means UserRole support is off.
|
|
3
|
+
// `label` and `description` are vue-i18n keys (see i18n/locales/*.json).
|
|
4
|
+
export const ROLES = [
|
|
5
|
+
{
|
|
6
|
+
key: "master",
|
|
7
|
+
label: "roles.master.title",
|
|
8
|
+
badge: "bg-rose-500/10 text-rose-300 ring-rose-400/30",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
key: "super_admin",
|
|
12
|
+
label: "roles.super_admin.title",
|
|
13
|
+
badge: "bg-amber-500/10 text-amber-300 ring-amber-400/30",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: "admin",
|
|
17
|
+
label: "roles.admin.title",
|
|
18
|
+
badge: "bg-cyan-500/10 text-cyan-300 ring-cyan-400/30",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "user",
|
|
22
|
+
label: "roles.user.title",
|
|
23
|
+
badge: "bg-emerald-500/10 text-emerald-300 ring-emerald-400/30",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "فلك"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "رئيسي"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "مشرف عام"
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "مدير"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "مستخدم"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "جارٍ التحميل...",
|
|
7
21
|
"saving": "جارٍ الحفظ...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "مقبولة",
|
|
92
106
|
"3": "جيدة",
|
|
93
107
|
"4": "قوية"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "الدور"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "استعادة كلمة المرور",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "رقم واتساب",
|
|
185
200
|
"telegramPlaceholder": "@yourusername",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "الدور"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "الأمان",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "معطل"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "Falak"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "Meister"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "Super-Admin"
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "Admin"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "Benutzer"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "Lädt...",
|
|
7
21
|
"saving": "Speichert...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "Mittel",
|
|
92
106
|
"3": "Gut",
|
|
93
107
|
"4": "Stark"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "Rolle"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "Passwort wiederherstellen",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "WhatsApp-Nummer",
|
|
185
200
|
"telegramPlaceholder": "@deinusername",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "Rolle"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "Sicherheit",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "Deaktiviert"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "Falak"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "Master"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "Super Admin"
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "Admin"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "User"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "Loading...",
|
|
7
21
|
"saving": "Saving...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "Fair",
|
|
92
106
|
"3": "Good",
|
|
93
107
|
"4": "Strong"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "Role"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "Recover password",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "WhatsApp number",
|
|
185
200
|
"telegramPlaceholder": "@yourusername",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "Role"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "Security",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "Disabled"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "Falak"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "Maestro"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "Super Adm."
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "Admin"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "Usuario"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "Cargando...",
|
|
7
21
|
"saving": "Guardando...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "Aceptable",
|
|
92
106
|
"3": "Buena",
|
|
93
107
|
"4": "Fuerte"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "Rol"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "Recuperar contraseña",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "Número de WhatsApp",
|
|
185
200
|
"telegramPlaceholder": "@tuusuario",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "Rol"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "Seguridad",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "Desactivado"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "Falak"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "Maître"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "Super Admin"
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "Admin"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "Utilisateur"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "Chargement...",
|
|
7
21
|
"saving": "Enregistrement...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "Moyen",
|
|
92
106
|
"3": "Bon",
|
|
93
107
|
"4": "Fort"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "Rôle"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "Récupérer le mot de passe",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "Numéro WhatsApp",
|
|
185
200
|
"telegramPlaceholder": "@votrenomdutilisateur",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "Rôle"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "Sécurité",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "Désactivé"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "Falak"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "Master"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "Super Admin"
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "Admin"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "Utente"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "Caricamento...",
|
|
7
21
|
"saving": "Salvataggio...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "Discreta",
|
|
92
106
|
"3": "Buona",
|
|
93
107
|
"4": "Forte"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "Ruolo"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "Recupera password",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "Numero WhatsApp",
|
|
185
200
|
"telegramPlaceholder": "@tuonomeutente",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "Ruolo"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "Sicurezza",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "Disattivo"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
"app": {
|
|
3
3
|
"name": "Falak"
|
|
4
4
|
},
|
|
5
|
+
"roles": {
|
|
6
|
+
"master": {
|
|
7
|
+
"title": "Мастер"
|
|
8
|
+
},
|
|
9
|
+
"super_admin": {
|
|
10
|
+
"title": "Супер админ"
|
|
11
|
+
},
|
|
12
|
+
"admin": {
|
|
13
|
+
"title": "Администратор"
|
|
14
|
+
},
|
|
15
|
+
"user": {
|
|
16
|
+
"title": "Пользователь"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
5
19
|
"common": {
|
|
6
20
|
"loading": "Загрузка...",
|
|
7
21
|
"saving": "Сохранение...",
|
|
@@ -91,7 +105,8 @@
|
|
|
91
105
|
"2": "Средний",
|
|
92
106
|
"3": "Хороший",
|
|
93
107
|
"4": "Надёжный"
|
|
94
|
-
}
|
|
108
|
+
},
|
|
109
|
+
"role": "Роль"
|
|
95
110
|
},
|
|
96
111
|
"recover": {
|
|
97
112
|
"title": "Восстановление пароля",
|
|
@@ -184,7 +199,8 @@
|
|
|
184
199
|
"whatsappHandle": "Номер WhatsApp",
|
|
185
200
|
"telegramPlaceholder": "@yourusername",
|
|
186
201
|
"whatsappPlaceholder": "+201234567890"
|
|
187
|
-
}
|
|
202
|
+
},
|
|
203
|
+
"role": "Роль"
|
|
188
204
|
},
|
|
189
205
|
"security": {
|
|
190
206
|
"title": "Безопасность",
|
|
@@ -338,4 +354,4 @@
|
|
|
338
354
|
"off": "Выключено"
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
|
-
}
|
|
357
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createApp } from "vue";
|
|
2
2
|
import { createPinia } from "pinia";
|
|
3
|
+
import PrimeVue from "primevue/config";
|
|
4
|
+
import Aura from "@primeuix/themes/aura";
|
|
5
|
+
import "primeicons/primeicons.css";
|
|
3
6
|
import App from "./App.vue";
|
|
4
7
|
import router from "./router";
|
|
5
8
|
import i18n from "./i18n";
|
|
@@ -7,6 +10,14 @@ import "./assets/main.css";
|
|
|
7
10
|
|
|
8
11
|
const app = createApp(App);
|
|
9
12
|
|
|
13
|
+
app.use(PrimeVue, {
|
|
14
|
+
theme: {
|
|
15
|
+
preset: Aura,
|
|
16
|
+
options: {
|
|
17
|
+
cssLayer: false,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
10
21
|
app.use(createPinia());
|
|
11
22
|
app.use(router);
|
|
12
23
|
app.use(i18n);
|
|
@@ -19,6 +19,11 @@ export const useAuthStore = defineStore("auth", {
|
|
|
19
19
|
userToken: loadToken(),
|
|
20
20
|
user: null,
|
|
21
21
|
}),
|
|
22
|
+
getters: {
|
|
23
|
+
// Role is part of the user account; comes from the API user payload
|
|
24
|
+
// (or was picked at registration when UserRole support is enabled).
|
|
25
|
+
role: (state) => state.user?.role || null,
|
|
26
|
+
},
|
|
22
27
|
actions: {
|
|
23
28
|
setToken(token) {
|
|
24
29
|
this.userToken = token;
|
|
@@ -4,10 +4,13 @@ import { useI18n } from "vue-i18n";
|
|
|
4
4
|
import { useAuthStore } from "@/stores/auth";
|
|
5
5
|
import GlassCard from "@/components/GlassCard.vue";
|
|
6
6
|
import { countries, citiesFor } from "@/data/locations";
|
|
7
|
+
import { ROLES } from "@/data/roles";
|
|
7
8
|
|
|
8
9
|
const { t } = useI18n();
|
|
9
10
|
const auth = useAuthStore();
|
|
10
11
|
|
|
12
|
+
const roleInfo = computed(() => ROLES.find((r) => r.key === auth.user?.role));
|
|
13
|
+
|
|
11
14
|
const form = ref({
|
|
12
15
|
name: "",
|
|
13
16
|
email: "",
|
|
@@ -78,6 +81,16 @@ onMounted(async () => {
|
|
|
78
81
|
<div class="mb-8 animate-fade-in-up motion-reduce:animate-none">
|
|
79
82
|
<div class="section-head">
|
|
80
83
|
<h1 class="text-2xl font-bold text-white">{{ t("profile.title") }}</h1>
|
|
84
|
+
<span
|
|
85
|
+
v-if="roleInfo"
|
|
86
|
+
class="mt-2 inline-flex w-fit items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wide ring-1"
|
|
87
|
+
:class="roleInfo.badge"
|
|
88
|
+
>
|
|
89
|
+
<span class="relative inline-flex h-1.5 w-1.5">
|
|
90
|
+
<span class="relative inline-flex h-1.5 w-1.5 rounded-full bg-current opacity-80"></span>
|
|
91
|
+
</span>
|
|
92
|
+
{{ t(roleInfo.label) }}
|
|
93
|
+
</span>
|
|
81
94
|
</div>
|
|
82
95
|
<p class="mt-1 text-sm text-gray-400">{{ t("profile.subtitle") }}</p>
|
|
83
96
|
</div>
|
|
@@ -6,6 +6,7 @@ import { useAuthStore } from "@/stores/auth";
|
|
|
6
6
|
import AuthLayout from "@/components/AuthLayout.vue";
|
|
7
7
|
import SocialIcon from "@/components/SocialIcon.vue";
|
|
8
8
|
import { useRipple } from "@/composables/useRipple";
|
|
9
|
+
import { ROLES } from "@/data/roles";
|
|
9
10
|
|
|
10
11
|
const { t, locale } = useI18n();
|
|
11
12
|
const router = useRouter();
|
|
@@ -20,6 +21,9 @@ const showPassword = ref(false);
|
|
|
20
21
|
const error = ref("");
|
|
21
22
|
const loading = ref(false);
|
|
22
23
|
const socialBusy = ref("");
|
|
24
|
+
// When UserRole support is on, new accounts carry a role; ROLES is empty when
|
|
25
|
+
// the feature was not picked at scaffold time.
|
|
26
|
+
const role = ref(ROLES[0]?.key || "");
|
|
23
27
|
|
|
24
28
|
const { ripples, spawnRipple, removeRipple } = useRipple();
|
|
25
29
|
|
|
@@ -45,14 +49,16 @@ async function submit() {
|
|
|
45
49
|
}
|
|
46
50
|
loading.value = true;
|
|
47
51
|
try {
|
|
48
|
-
|
|
52
|
+
const payload = {
|
|
49
53
|
name: name.value,
|
|
50
54
|
identifier: email.value.trim(),
|
|
51
55
|
email: email.value.trim(),
|
|
52
56
|
phone: phone.value.trim(),
|
|
53
57
|
password: password.value,
|
|
54
58
|
locale: locale.value,
|
|
55
|
-
}
|
|
59
|
+
};
|
|
60
|
+
if (ROLES.length) payload.role = role.value;
|
|
61
|
+
await auth.register(payload);
|
|
56
62
|
router.push({ name: "home" });
|
|
57
63
|
} catch (e) {
|
|
58
64
|
error.value = t("register.error");
|
|
@@ -117,6 +123,15 @@ async function social(provider) {
|
|
|
117
123
|
/>
|
|
118
124
|
</label>
|
|
119
125
|
|
|
126
|
+
<label v-if="ROLES.length > 1" class="block">
|
|
127
|
+
<span class="field-label">{{ t("register.role") }}</span>
|
|
128
|
+
<select v-model="role" class="glass-select">
|
|
129
|
+
<option v-for="r in ROLES" :key="r.key" :value="r.key">
|
|
130
|
+
{{ t(r.label) }}
|
|
131
|
+
</option>
|
|
132
|
+
</select>
|
|
133
|
+
</label>
|
|
134
|
+
|
|
120
135
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
121
136
|
<label class="block">
|
|
122
137
|
<span class="field-label">{{ t("register.email") }}</span>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import vue from "@vitejs/plugin-vue";
|
|
3
|
+
import basicSsl from "@vitejs/plugin-basic-ssl";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
|
|
8
9
|
export default defineConfig({
|
|
9
|
-
plugins: [vue()],
|
|
10
|
+
plugins: [vue(), basicSsl()],
|
|
10
11
|
resolve: {
|
|
11
12
|
alias: {
|
|
12
13
|
"@": path.resolve(__dirname, "src"),
|