sahab-utils 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -845,8 +845,6 @@ export type AppSettings = {
845
845
  pushTransport?: PushTransport;
846
846
  /** Kill switch for staff push broadcasts. Off unless true; test sends to the dev team still work. */
847
847
  broadcastEnabled?: boolean;
848
- /** Staff push for an order that used a car Sahab has not verified. Off unless true, so deploying does not send one per model already in use. */
849
- carReviewAlertsEnabled?: boolean;
850
848
  };
851
849
  export type OrderReview = {
852
850
  ratingId: string;
@@ -1,10 +1,10 @@
1
1
  import type { Permissions, StaffRole } from './staffAccess';
2
2
  /** Which alerts a back-office admin gets. The shared contract between sahab-website (the Admin notifications page) and cloudFun. */
3
3
  /** App (push) alerts, each sent to the admin's customer-app account. */
4
- export declare const ADMIN_PUSH_KINDS: readonly ["newOrder", "newCarwash", "newGift", "newClient", "needsAttention", "statements"];
4
+ export declare const ADMIN_PUSH_KINDS: readonly ["newOrder", "newCarwash", "newGift", "newClient", "newCarModel", "needsAttention", "statements"];
5
5
  export type AdminPushKind = (typeof ADMIN_PUSH_KINDS)[number];
6
6
  /** Every switch on the page: the app alerts plus the lead e-mail. */
7
- export declare const ADMIN_NOTIFICATION_KEYS: readonly ["newOrder", "newCarwash", "newGift", "newClient", "needsAttention", "statements", "leadEmails"];
7
+ export declare const ADMIN_NOTIFICATION_KEYS: readonly ["newOrder", "newCarwash", "newGift", "newClient", "newCarModel", "needsAttention", "statements", "leadEmails"];
8
8
  export type AdminNotificationKey = (typeof ADMIN_NOTIFICATION_KEYS)[number];
9
9
  export type AdminNotificationPrefs = Record<AdminNotificationKey, boolean>;
10
10
  export declare const ADMIN_NOTIFICATION_LABELS: Record<AdminNotificationKey, {
@@ -4,7 +4,7 @@ exports.isValidAppUid = exports.isValidAdminEmail = exports.normalizeAdminEmail
4
4
  exports.normalizeAdminNotificationPrefs = normalizeAdminNotificationPrefs;
5
5
  /** Which alerts a back-office admin gets. The shared contract between sahab-website (the Admin notifications page) and cloudFun. */
6
6
  /** App (push) alerts, each sent to the admin's customer-app account. */
7
- exports.ADMIN_PUSH_KINDS = ['newOrder', 'newCarwash', 'newGift', 'newClient', 'needsAttention', 'statements'];
7
+ exports.ADMIN_PUSH_KINDS = ['newOrder', 'newCarwash', 'newGift', 'newClient', 'newCarModel', 'needsAttention', 'statements'];
8
8
  /** Every switch on the page: the app alerts plus the lead e-mail. */
9
9
  exports.ADMIN_NOTIFICATION_KEYS = [...exports.ADMIN_PUSH_KINDS, 'leadEmails'];
10
10
  exports.ADMIN_NOTIFICATION_LABELS = {
@@ -12,6 +12,7 @@ exports.ADMIN_NOTIFICATION_LABELS = {
12
12
  newCarwash: { label: 'New carwash', description: 'A carwash registers or finishes its registration.' },
13
13
  newGift: { label: 'New gift', description: 'A customer buys a gift.' },
14
14
  newClient: { label: 'New customer', description: 'A customer joins the app.' },
15
+ newCarModel: { label: 'Car model to review', description: 'An order uses a car model Sahab has not reviewed yet.' },
15
16
  needsAttention: { label: 'Carwash needs attention', description: 'A carwash changed something that needs a look.' },
16
17
  statements: { label: 'Statements', description: 'The daily and weekly statements are ready.' },
17
18
  leadEmails: { label: 'Lead e-mails', description: 'A visitor sends an enquiry or a carwash request on the website.' },
@@ -10,10 +10,12 @@ export declare const CAR_REVIEW_QUEUES: readonly ["pending", "declined", "needs_
10
10
  export type CarReviewQueue = (typeof CAR_REVIEW_QUEUES)[number];
11
11
  /** An alias document counts toward this many per model, so a fix can find every key with one `in` query (Firestore allows 30). */
12
12
  export declare const MAX_ALIASES = 29;
13
+ /** `ar` is the model's official Arabic name (`nameAr`); aliases are never shown to customers. */
13
14
  export type CarPickerEntry = {
14
15
  id: string;
15
16
  name: string;
16
17
  size: CarSize;
18
+ ar?: string;
17
19
  };
18
20
  /** `car_makes/{makeKey}`. `T` is the timestamp type of whichever Firestore SDK reads or writes it. */
19
21
  export type CarMakeDoc<T = TimestampType> = {
@@ -31,6 +33,8 @@ export type CarModelDoc<T = TimestampType> = {
31
33
  makeEn: string;
32
34
  status: CarModelStatus;
33
35
  name: string;
36
+ /** The one official Arabic name, set by staff. Other Arabic spellings are aliases and stay internal. */
37
+ nameAr?: string;
34
38
  size: CarSize;
35
39
  aliasKeys?: string[];
36
40
  aliases?: string[];
@@ -55,6 +59,7 @@ export type CarQueueSort = (typeof CAR_QUEUE_SORTS)[number];
55
59
  export type CarCatalogueEntry = {
56
60
  modelId: string;
57
61
  name: string;
62
+ nameAr?: string;
58
63
  aliases: string[];
59
64
  size: CarSize;
60
65
  source: CarModelSource;
@@ -123,7 +128,10 @@ export declare function isCarVerified(car: Partial<Car> | undefined): boolean;
123
128
  /** The active models of a make as the customer picker lists them: sorted by name, never an alias, pending or declined model. */
124
129
  export declare function buildPicker(models: ({
125
130
  id: string;
126
- } & Pick<CarModelDoc, 'status' | 'name' | 'size'>)[]): CarPickerEntry[];
131
+ ar?: string;
132
+ } & Pick<CarModelDoc, 'status' | 'name' | 'size' | 'nameAr'>)[]): CarPickerEntry[];
133
+ /** An official Arabic name: trimmed, one line, at most `CAR_NAME_MAX_LENGTH` characters and written in Arabic. */
134
+ export declare const isValidArabicName: (value: unknown) => value is string;
127
135
  /** The picker with one active model added, or replaced if it is already there. */
128
136
  export declare function withPickerEntry(picker: CarPickerEntry[] | undefined, entry: CarPickerEntry): CarPickerEntry[];
129
137
  /** Every key a model's cars may carry: its own and those of the aliases merged into it. */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PLATE_NUMBER_MAX = exports.PLATE_LETTERS_MAX = exports.CAR_CATALOGUE_INDEX_ID = exports.joinModelKey = exports.isCarSize = exports.CAR_SIZE_INFO = exports.CAR_QUEUE_SORTS = exports.MAX_ALIASES = exports.CAR_REVIEW_QUEUES = exports.CAR_MODEL_STATUSES = exports.CAR_NAME_MAX_LENGTH = exports.CAR_MODEL_MAX_LENGTH = void 0;
3
+ exports.PLATE_NUMBER_MAX = exports.PLATE_LETTERS_MAX = exports.CAR_CATALOGUE_INDEX_ID = exports.isValidArabicName = exports.joinModelKey = exports.isCarSize = exports.CAR_SIZE_INFO = exports.CAR_QUEUE_SORTS = exports.MAX_ALIASES = exports.CAR_REVIEW_QUEUES = exports.CAR_MODEL_STATUSES = exports.CAR_NAME_MAX_LENGTH = exports.CAR_MODEL_MAX_LENGTH = void 0;
4
4
  exports.normalizeCarText = normalizeCarText;
5
5
  exports.carModelKey = carModelKey;
6
6
  exports.parseTypedCarModel = parseTypedCarModel;
@@ -80,9 +80,16 @@ function isCarVerified(car) {
80
80
  function buildPicker(models) {
81
81
  return models
82
82
  .filter((model) => model.status === 'active')
83
- .map(({ id, name, size }) => ({ id, name, size }))
83
+ .map(({ id, name, size, nameAr, ar }) => {
84
+ const arabic = nameAr || ar;
85
+ return Object.assign({ id, name, size }, (arabic ? { ar: arabic } : {}));
86
+ })
84
87
  .sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
85
88
  }
89
+ const ARABIC_LETTER = /[\u0600-\u06FF]/;
90
+ /** An official Arabic name: trimmed, one line, at most `CAR_NAME_MAX_LENGTH` characters and written in Arabic. */
91
+ const isValidArabicName = (value) => typeof value === 'string' && value === value.trim() && value.length > 0 && value.length <= exports.CAR_NAME_MAX_LENGTH && !/[\r\n]/.test(value) && ARABIC_LETTER.test(value);
92
+ exports.isValidArabicName = isValidArabicName;
86
93
  /** The picker with one active model added, or replaced if it is already there. */
87
94
  function withPickerEntry(picker, entry) {
88
95
  return buildPicker([...(picker !== null && picker !== void 0 ? picker : []).filter((existing) => existing.id !== entry.id), entry].map((item) => (Object.assign(Object.assign({}, item), { status: 'active' }))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sahab-utils",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "This package contains types and utils functions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",