sahab-utils 1.10.0 → 2.0.0
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/dist/constants/types.d.ts +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ts/carCatalogue.d.ts +156 -0
- package/dist/ts/carCatalogue.js +138 -0
- package/dist/ts/staffAccess.d.ts +2 -1
- package/dist/ts/staffAccess.js +3 -0
- package/package.json +2 -2
|
@@ -25,6 +25,16 @@ export type Car = {
|
|
|
25
25
|
plateLetters: string;
|
|
26
26
|
plateNumber: string;
|
|
27
27
|
imageUrl?: string;
|
|
28
|
+
/** 'customer' when the customer chose the size of a model Sahab does not know yet; set only by the server. */
|
|
29
|
+
sizeSource?: 'catalogue' | 'customer';
|
|
30
|
+
/** The make/model key (`carModelKey`), stored so staff can find every car of one model. */
|
|
31
|
+
modelKey?: string;
|
|
32
|
+
verifiedBySahab?: boolean;
|
|
33
|
+
verifiedSize?: CarSize;
|
|
34
|
+
verifiedModelKey?: string;
|
|
35
|
+
verifiedAt?: TimestampType;
|
|
36
|
+
verifiedBy?: string;
|
|
37
|
+
sizeBeforeFix?: CarSize;
|
|
28
38
|
};
|
|
29
39
|
export type ClientLocation = {
|
|
30
40
|
locationId: string;
|
|
@@ -835,6 +845,8 @@ export type AppSettings = {
|
|
|
835
845
|
pushTransport?: PushTransport;
|
|
836
846
|
/** Kill switch for staff push broadcasts. Off unless true; test sends to the dev team still work. */
|
|
837
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;
|
|
838
850
|
};
|
|
839
851
|
export type OrderReview = {
|
|
840
852
|
ratingId: string;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ __exportStar(require("./ts/staffAccess"), exports);
|
|
|
20
20
|
__exportStar(require("./ts/broadcast"), exports);
|
|
21
21
|
__exportStar(require("./ts/capacity"), exports);
|
|
22
22
|
__exportStar(require("./ts/calcs"), exports);
|
|
23
|
+
__exportStar(require("./ts/carCatalogue"), exports);
|
|
23
24
|
__exportStar(require("./ts/cash"), exports);
|
|
24
25
|
__exportStar(require("./ts/cashback"), exports);
|
|
25
26
|
__exportStar(require("./ts/gifts"), exports);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Car, CarSize, TimestampType } from '../constants/types';
|
|
2
|
+
/** Longest model a customer may type under "Other". */
|
|
3
|
+
export declare const CAR_MODEL_MAX_LENGTH = 40;
|
|
4
|
+
/** Longest make, colour or other free text a car request may carry. */
|
|
5
|
+
export declare const CAR_NAME_MAX_LENGTH = 60;
|
|
6
|
+
export declare const CAR_MODEL_STATUSES: readonly ["active", "pending", "declined", "alias"];
|
|
7
|
+
export type CarModelStatus = (typeof CAR_MODEL_STATUSES)[number];
|
|
8
|
+
/** The staff review queues: models awaiting a decision, declined ones, and active ones whose cars are still unverified on orders. */
|
|
9
|
+
export declare const CAR_REVIEW_QUEUES: readonly ["pending", "declined", "needs_fix"];
|
|
10
|
+
export type CarReviewQueue = (typeof CAR_REVIEW_QUEUES)[number];
|
|
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
|
+
export declare const MAX_ALIASES = 29;
|
|
13
|
+
export type CarPickerEntry = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
size: CarSize;
|
|
17
|
+
};
|
|
18
|
+
/** `car_makes/{makeKey}`. `T` is the timestamp type of whichever Firestore SDK reads or writes it. */
|
|
19
|
+
export type CarMakeDoc<T = TimestampType> = {
|
|
20
|
+
en: string;
|
|
21
|
+
ar?: string;
|
|
22
|
+
picker: CarPickerEntry[];
|
|
23
|
+
source: 'seed' | 'approved';
|
|
24
|
+
createdAt?: T;
|
|
25
|
+
updatedAt?: T;
|
|
26
|
+
updatedBy?: string;
|
|
27
|
+
};
|
|
28
|
+
/** `car_makes/{makeKey}/car_models/{modelKey}`: a model of any status, or an alias pointing at the active model it stands for. */
|
|
29
|
+
export type CarModelDoc<T = TimestampType> = {
|
|
30
|
+
makeKey: string;
|
|
31
|
+
makeEn: string;
|
|
32
|
+
status: CarModelStatus;
|
|
33
|
+
name: string;
|
|
34
|
+
size: CarSize;
|
|
35
|
+
aliasKeys?: string[];
|
|
36
|
+
aliases?: string[];
|
|
37
|
+
aliasOf?: string;
|
|
38
|
+
source: 'seed' | 'other' | 'order';
|
|
39
|
+
count: number;
|
|
40
|
+
ordersCount: number;
|
|
41
|
+
sizesSeen: Record<string, number>;
|
|
42
|
+
unverifiedOrdersCount: number;
|
|
43
|
+
createdAt: T;
|
|
44
|
+
decidedAt?: T;
|
|
45
|
+
decidedBy?: string;
|
|
46
|
+
updatedAt?: T;
|
|
47
|
+
updatedBy?: string;
|
|
48
|
+
sizeUpdatedAt?: T;
|
|
49
|
+
};
|
|
50
|
+
export type CarModelSource = CarModelDoc['source'];
|
|
51
|
+
export type CarModelDecision = 'approve' | 'decline' | 'reopen';
|
|
52
|
+
export declare const CAR_QUEUE_SORTS: readonly ["count", "orders", "recent"];
|
|
53
|
+
export type CarQueueSort = (typeof CAR_QUEUE_SORTS)[number];
|
|
54
|
+
/** A model as the staff catalogue lists it; `modelId` is its document id. */
|
|
55
|
+
export type CarCatalogueEntry = {
|
|
56
|
+
modelId: string;
|
|
57
|
+
name: string;
|
|
58
|
+
aliases: string[];
|
|
59
|
+
size: CarSize;
|
|
60
|
+
source: CarModelSource;
|
|
61
|
+
};
|
|
62
|
+
/** One row of a staff queue. For `needs_fix`, `requestedSize` is the model's current catalogue size and `typedModel` its name. */
|
|
63
|
+
export type CarQueueRow = {
|
|
64
|
+
modelKey: string;
|
|
65
|
+
makeEn: string;
|
|
66
|
+
typedModel: string;
|
|
67
|
+
requestedSize: CarSize;
|
|
68
|
+
count: number;
|
|
69
|
+
ordersCount: number;
|
|
70
|
+
unverifiedOrdersCount: number;
|
|
71
|
+
sizesSeen: Record<string, number>;
|
|
72
|
+
source: CarModelSource;
|
|
73
|
+
status: CarReviewQueue;
|
|
74
|
+
createdAt: string | null;
|
|
75
|
+
decidedAt: string | null;
|
|
76
|
+
decidedBy?: string;
|
|
77
|
+
};
|
|
78
|
+
/** What the customer app sends to save a car; the server decides the size of a known model. */
|
|
79
|
+
export type SaveCarRequest = {
|
|
80
|
+
carId?: string;
|
|
81
|
+
manufacturer: string;
|
|
82
|
+
year: string;
|
|
83
|
+
color: string;
|
|
84
|
+
plateLetters: string;
|
|
85
|
+
plateNumber: string;
|
|
86
|
+
imageUrl?: string;
|
|
87
|
+
/** A model picked from the list. */
|
|
88
|
+
modelId?: string;
|
|
89
|
+
/** A model typed under "Other", with the size the customer chose for it. */
|
|
90
|
+
model?: string;
|
|
91
|
+
size?: number;
|
|
92
|
+
};
|
|
93
|
+
/** The size names and what each covers, as the back office shows them (the customer app reads the same wording from its dictionary). */
|
|
94
|
+
export declare const CAR_SIZE_INFO: Record<CarSize, {
|
|
95
|
+
label: {
|
|
96
|
+
en: string;
|
|
97
|
+
ar: string;
|
|
98
|
+
};
|
|
99
|
+
description: {
|
|
100
|
+
en: string;
|
|
101
|
+
ar: string;
|
|
102
|
+
};
|
|
103
|
+
}>;
|
|
104
|
+
export declare const isCarSize: (value: unknown) => value is CarSize;
|
|
105
|
+
/** One spelling for a car name: case, accents, Arabic letter variants, Arabic digits, spaces and punctuation all gone. Empty when nothing usable is left. */
|
|
106
|
+
export declare function normalizeCarText(text: unknown): string;
|
|
107
|
+
/** `makeKey__modelKey`: the one place that shape is written. */
|
|
108
|
+
export declare const joinModelKey: (makeKey: string, modelKey: string) => string;
|
|
109
|
+
/** The key a make and model share across cars, review items and fixes; empty when either part is empty. */
|
|
110
|
+
export declare function carModelKey(make: unknown, model: unknown): string;
|
|
111
|
+
export type TypedCarModel = {
|
|
112
|
+
ok: true;
|
|
113
|
+
text: string;
|
|
114
|
+
key: string;
|
|
115
|
+
} | {
|
|
116
|
+
ok: false;
|
|
117
|
+
error: 'empty' | 'too_long';
|
|
118
|
+
};
|
|
119
|
+
/** A model typed under "Other", ready to use: trimmed, within the length cap and with a non-empty key. */
|
|
120
|
+
export declare function parseTypedCarModel(make: unknown, text: unknown): TypedCarModel;
|
|
121
|
+
/** Verified only while the car still has the size and model Sahab checked; any edit by the customer makes it unverified again. */
|
|
122
|
+
export declare function isCarVerified(car: Partial<Car> | undefined): boolean;
|
|
123
|
+
/** The active models of a make as the customer picker lists them: sorted by name, never an alias, pending or declined model. */
|
|
124
|
+
export declare function buildPicker(models: ({
|
|
125
|
+
id: string;
|
|
126
|
+
} & Pick<CarModelDoc, 'status' | 'name' | 'size'>)[]): CarPickerEntry[];
|
|
127
|
+
/** The picker with one active model added, or replaced if it is already there. */
|
|
128
|
+
export declare function withPickerEntry(picker: CarPickerEntry[] | undefined, entry: CarPickerEntry): CarPickerEntry[];
|
|
129
|
+
/** Every key a model's cars may carry: its own and those of the aliases merged into it. */
|
|
130
|
+
export declare function modelKeysOf(model: {
|
|
131
|
+
id: string;
|
|
132
|
+
aliasKeys?: string[];
|
|
133
|
+
}): string[];
|
|
134
|
+
/** `makeKey__modelKey` as its two parts, or undefined when it is not that shape. */
|
|
135
|
+
export declare function splitModelKey(key: unknown): {
|
|
136
|
+
makeKey: string;
|
|
137
|
+
modelKey: string;
|
|
138
|
+
} | undefined;
|
|
139
|
+
/** The makes list a customer picks from: one small document, so showing it costs a single read. */
|
|
140
|
+
export declare const CAR_CATALOGUE_INDEX_ID = "_index";
|
|
141
|
+
export type CarMakeIndexEntry = {
|
|
142
|
+
makeKey: string;
|
|
143
|
+
en: string;
|
|
144
|
+
ar?: string;
|
|
145
|
+
};
|
|
146
|
+
export type CarMakeIndex = {
|
|
147
|
+
makes: CarMakeIndexEntry[];
|
|
148
|
+
};
|
|
149
|
+
/** The key of the make a customer typed: the catalogue's own key when the text is a known make's English or Arabic name, else the typed text's key. Empty when nothing usable was typed. */
|
|
150
|
+
export declare function resolveMakeKey(index: CarMakeIndexEntry[] | undefined, typedMake: unknown): string;
|
|
151
|
+
export declare const PLATE_LETTERS_MAX = 3;
|
|
152
|
+
export declare const PLATE_NUMBER_MAX = 4;
|
|
153
|
+
/** Plate letters as stored: English capitals only, at most three; Arabic letters become their plate equivalents. */
|
|
154
|
+
export declare function normalizePlateLetters(text: unknown): string;
|
|
155
|
+
/** Plate digits as stored: Western digits only, at most four. */
|
|
156
|
+
export declare function normalizePlateNumber(text: unknown): string;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
exports.normalizeCarText = normalizeCarText;
|
|
5
|
+
exports.carModelKey = carModelKey;
|
|
6
|
+
exports.parseTypedCarModel = parseTypedCarModel;
|
|
7
|
+
exports.isCarVerified = isCarVerified;
|
|
8
|
+
exports.buildPicker = buildPicker;
|
|
9
|
+
exports.withPickerEntry = withPickerEntry;
|
|
10
|
+
exports.modelKeysOf = modelKeysOf;
|
|
11
|
+
exports.splitModelKey = splitModelKey;
|
|
12
|
+
exports.resolveMakeKey = resolveMakeKey;
|
|
13
|
+
exports.normalizePlateLetters = normalizePlateLetters;
|
|
14
|
+
exports.normalizePlateNumber = normalizePlateNumber;
|
|
15
|
+
const types_1 = require("../constants/types");
|
|
16
|
+
/** Longest model a customer may type under "Other". */
|
|
17
|
+
exports.CAR_MODEL_MAX_LENGTH = 40;
|
|
18
|
+
/** Longest make, colour or other free text a car request may carry. */
|
|
19
|
+
exports.CAR_NAME_MAX_LENGTH = 60;
|
|
20
|
+
exports.CAR_MODEL_STATUSES = ['active', 'pending', 'declined', 'alias'];
|
|
21
|
+
/** The staff review queues: models awaiting a decision, declined ones, and active ones whose cars are still unverified on orders. */
|
|
22
|
+
exports.CAR_REVIEW_QUEUES = ['pending', 'declined', 'needs_fix'];
|
|
23
|
+
/** An alias document counts toward this many per model, so a fix can find every key with one `in` query (Firestore allows 30). */
|
|
24
|
+
exports.MAX_ALIASES = 29;
|
|
25
|
+
exports.CAR_QUEUE_SORTS = ['count', 'orders', 'recent'];
|
|
26
|
+
/** The size names and what each covers, as the back office shows them (the customer app reads the same wording from its dictionary). */
|
|
27
|
+
exports.CAR_SIZE_INFO = {
|
|
28
|
+
[types_1.CarSize.X_SMALL]: { label: { en: 'Extra Small', ar: 'صغير جداً' }, description: { en: 'Bikes and similar sizes', ar: 'الدراجات النارية، الدبابات، ومايعادلها' } },
|
|
29
|
+
[types_1.CarSize.SMALL]: { label: { en: 'Small', ar: 'صغير' }, description: { en: 'Sedan, mini, coupe, sport and similar sizes', ar: 'سيارات السيدان، ميني، الكوبيه، السبورت، ومايعادلها' } },
|
|
30
|
+
[types_1.CarSize.MEDIUM]: { label: { en: 'Medium', ar: 'متوسط' }, description: { en: '5 passengers car, small pickup or similar sizes', ar: 'سيارات بسعة 5 ركاب، وانيت صغير، ومايعادلها' } },
|
|
31
|
+
[types_1.CarSize.LARGE]: { label: { en: 'Large', ar: 'كبير' }, description: { en: '7 passengers car, large pickup or similar sizes', ar: 'سيارات بسعة 7 ركاب، وانيت كبير، ومايعادلها' } },
|
|
32
|
+
[types_1.CarSize.X_LARGE]: { label: { en: 'Extra Large', ar: 'كبير جداً' }, description: { en: 'Food trucks, travel trailers and similar sizes', ar: 'الفود ترك، مقطورات السفر، ومايعادلها' } },
|
|
33
|
+
};
|
|
34
|
+
const isCarSize = (value) => Number.isInteger(value) && value >= types_1.CarSize.X_SMALL && value <= types_1.CarSize.X_LARGE;
|
|
35
|
+
exports.isCarSize = isCarSize;
|
|
36
|
+
const ARABIC_DIACRITICS = /[ً-ٰٟـ]/g;
|
|
37
|
+
const LATIN_ACCENTS = /[̀-ͯ]/g;
|
|
38
|
+
const NOT_A_LETTER_OR_DIGIT = /[^a-z0-9ء-ي]/g;
|
|
39
|
+
const westernDigits = (text) => text.replace(/[٠-٩۰-۹]/g, (digit) => String(digit.charCodeAt(0) >= 0x06f0 ? digit.charCodeAt(0) - 0x06f0 : digit.charCodeAt(0) - 0x0660));
|
|
40
|
+
/** One spelling for a car name: case, accents, Arabic letter variants, Arabic digits, spaces and punctuation all gone. Empty when nothing usable is left. */
|
|
41
|
+
function normalizeCarText(text) {
|
|
42
|
+
if (typeof text !== 'string')
|
|
43
|
+
return '';
|
|
44
|
+
const lower = text.normalize('NFD').replace(LATIN_ACCENTS, '').toLowerCase();
|
|
45
|
+
return westernDigits(lower.replace(ARABIC_DIACRITICS, ''))
|
|
46
|
+
.replace(/[أإآٱ]/g, 'ا')
|
|
47
|
+
.replace(/ى/g, 'ي')
|
|
48
|
+
.replace(/ة/g, 'ه')
|
|
49
|
+
.replace(NOT_A_LETTER_OR_DIGIT, '');
|
|
50
|
+
}
|
|
51
|
+
/** `makeKey__modelKey`: the one place that shape is written. */
|
|
52
|
+
const joinModelKey = (makeKey, modelKey) => `${makeKey}__${modelKey}`;
|
|
53
|
+
exports.joinModelKey = joinModelKey;
|
|
54
|
+
/** The key a make and model share across cars, review items and fixes; empty when either part is empty. */
|
|
55
|
+
function carModelKey(make, model) {
|
|
56
|
+
const makeKey = normalizeCarText(make);
|
|
57
|
+
const modelKey = normalizeCarText(model);
|
|
58
|
+
return makeKey && modelKey ? (0, exports.joinModelKey)(makeKey, modelKey) : '';
|
|
59
|
+
}
|
|
60
|
+
/** A model typed under "Other", ready to use: trimmed, within the length cap and with a non-empty key. */
|
|
61
|
+
function parseTypedCarModel(make, text) {
|
|
62
|
+
const trimmed = typeof text === 'string' ? text.trim() : '';
|
|
63
|
+
if (trimmed.length > exports.CAR_MODEL_MAX_LENGTH)
|
|
64
|
+
return { ok: false, error: 'too_long' };
|
|
65
|
+
const key = carModelKey(make, trimmed);
|
|
66
|
+
if (!key)
|
|
67
|
+
return { ok: false, error: 'empty' };
|
|
68
|
+
return { ok: true, text: trimmed, key };
|
|
69
|
+
}
|
|
70
|
+
/** Verified only while the car still has the size and model Sahab checked; any edit by the customer makes it unverified again. */
|
|
71
|
+
function isCarVerified(car) {
|
|
72
|
+
if (!car || car.verifiedBySahab !== true)
|
|
73
|
+
return false;
|
|
74
|
+
if (!(0, exports.isCarSize)(car.verifiedSize) || car.size !== car.verifiedSize)
|
|
75
|
+
return false;
|
|
76
|
+
const key = carModelKey(car.manufacturer, car.model);
|
|
77
|
+
return !!key && key === car.verifiedModelKey;
|
|
78
|
+
}
|
|
79
|
+
/** The active models of a make as the customer picker lists them: sorted by name, never an alias, pending or declined model. */
|
|
80
|
+
function buildPicker(models) {
|
|
81
|
+
return models
|
|
82
|
+
.filter((model) => model.status === 'active')
|
|
83
|
+
.map(({ id, name, size }) => ({ id, name, size }))
|
|
84
|
+
.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
85
|
+
}
|
|
86
|
+
/** The picker with one active model added, or replaced if it is already there. */
|
|
87
|
+
function withPickerEntry(picker, entry) {
|
|
88
|
+
return buildPicker([...(picker !== null && picker !== void 0 ? picker : []).filter((existing) => existing.id !== entry.id), entry].map((item) => (Object.assign(Object.assign({}, item), { status: 'active' }))));
|
|
89
|
+
}
|
|
90
|
+
/** Every key a model's cars may carry: its own and those of the aliases merged into it. */
|
|
91
|
+
function modelKeysOf(model) {
|
|
92
|
+
var _a;
|
|
93
|
+
return [model.id, ...((_a = model.aliasKeys) !== null && _a !== void 0 ? _a : []).filter((key) => key !== model.id)].filter((key, index, all) => all.indexOf(key) === index);
|
|
94
|
+
}
|
|
95
|
+
/** `makeKey__modelKey` as its two parts, or undefined when it is not that shape. */
|
|
96
|
+
function splitModelKey(key) {
|
|
97
|
+
if (typeof key !== 'string')
|
|
98
|
+
return undefined;
|
|
99
|
+
const parts = key.split('__');
|
|
100
|
+
return parts.length === 2 && parts[0] && parts[1] ? { makeKey: parts[0], modelKey: parts[1] } : undefined;
|
|
101
|
+
}
|
|
102
|
+
/** The makes list a customer picks from: one small document, so showing it costs a single read. */
|
|
103
|
+
exports.CAR_CATALOGUE_INDEX_ID = '_index';
|
|
104
|
+
/** The key of the make a customer typed: the catalogue's own key when the text is a known make's English or Arabic name, else the typed text's key. Empty when nothing usable was typed. */
|
|
105
|
+
function resolveMakeKey(index, typedMake) {
|
|
106
|
+
const typedKey = normalizeCarText(typedMake);
|
|
107
|
+
if (!typedKey)
|
|
108
|
+
return '';
|
|
109
|
+
const match = (index !== null && index !== void 0 ? index : []).find((entry) => normalizeCarText(entry.en) === typedKey || (!!entry.ar && normalizeCarText(entry.ar) === typedKey));
|
|
110
|
+
return match ? match.makeKey : typedKey;
|
|
111
|
+
}
|
|
112
|
+
exports.PLATE_LETTERS_MAX = 3;
|
|
113
|
+
exports.PLATE_NUMBER_MAX = 4;
|
|
114
|
+
// The Latin letters Saudi plates carry for each Arabic letter, so a customer can type the plate as it reads.
|
|
115
|
+
const ARABIC_PLATE_LETTERS = {
|
|
116
|
+
'ا': 'A', 'أ': 'A', 'إ': 'A', 'آ': 'A',
|
|
117
|
+
'ب': 'B', 'ح': 'J', 'د': 'D', 'ر': 'R', 'س': 'S', 'ص': 'X', 'ط': 'T', 'ع': 'E',
|
|
118
|
+
'ق': 'G', 'ك': 'K', 'ل': 'L', 'م': 'Z', 'ن': 'N', 'ه': 'H', 'و': 'U', 'ى': 'V', 'ي': 'V',
|
|
119
|
+
};
|
|
120
|
+
/** Plate letters as stored: English capitals only, at most three; Arabic letters become their plate equivalents. */
|
|
121
|
+
function normalizePlateLetters(text) {
|
|
122
|
+
if (typeof text !== 'string')
|
|
123
|
+
return '';
|
|
124
|
+
return text
|
|
125
|
+
.normalize('NFD')
|
|
126
|
+
.replace(/[̀-ًͯ-ٟـ]/g, '')
|
|
127
|
+
.split('')
|
|
128
|
+
.map((char) => { var _a; return (_a = ARABIC_PLATE_LETTERS[char]) !== null && _a !== void 0 ? _a : char.toUpperCase(); })
|
|
129
|
+
.join('')
|
|
130
|
+
.replace(/[^A-Z]/g, '')
|
|
131
|
+
.slice(0, exports.PLATE_LETTERS_MAX);
|
|
132
|
+
}
|
|
133
|
+
/** Plate digits as stored: Western digits only, at most four. */
|
|
134
|
+
function normalizePlateNumber(text) {
|
|
135
|
+
if (typeof text !== 'string')
|
|
136
|
+
return '';
|
|
137
|
+
return westernDigits(text).replace(/[^0-9]/g, '').slice(0, exports.PLATE_NUMBER_MAX);
|
|
138
|
+
}
|
package/dist/ts/staffAccess.d.ts
CHANGED
|
@@ -10,7 +10,8 @@ export declare enum Permissions {
|
|
|
10
10
|
VIEW_MAP = 6,// 6
|
|
11
11
|
VIEW_PROMO = 7,// 7
|
|
12
12
|
VIEW_BANNERS = 8,// 8
|
|
13
|
-
VIEW_INVESTOR_NUMBERS = 9
|
|
13
|
+
VIEW_INVESTOR_NUMBERS = 9,// 9
|
|
14
|
+
VIEW_CAR_MODELS = 10
|
|
14
15
|
}
|
|
15
16
|
export declare const ALL_PERMISSIONS: Permissions[];
|
|
16
17
|
export declare const PERMISSION_LABELS: Record<Permissions, {
|
package/dist/ts/staffAccess.js
CHANGED
|
@@ -16,6 +16,7 @@ var Permissions;
|
|
|
16
16
|
Permissions[Permissions["VIEW_PROMO"] = 7] = "VIEW_PROMO";
|
|
17
17
|
Permissions[Permissions["VIEW_BANNERS"] = 8] = "VIEW_BANNERS";
|
|
18
18
|
Permissions[Permissions["VIEW_INVESTOR_NUMBERS"] = 9] = "VIEW_INVESTOR_NUMBERS";
|
|
19
|
+
Permissions[Permissions["VIEW_CAR_MODELS"] = 10] = "VIEW_CAR_MODELS";
|
|
19
20
|
})(Permissions || (exports.Permissions = Permissions = {}));
|
|
20
21
|
exports.ALL_PERMISSIONS = [
|
|
21
22
|
Permissions.VIEW_KPI,
|
|
@@ -28,6 +29,7 @@ exports.ALL_PERMISSIONS = [
|
|
|
28
29
|
Permissions.VIEW_PROMO,
|
|
29
30
|
Permissions.VIEW_BANNERS,
|
|
30
31
|
Permissions.VIEW_INVESTOR_NUMBERS,
|
|
32
|
+
Permissions.VIEW_CAR_MODELS,
|
|
31
33
|
];
|
|
32
34
|
exports.PERMISSION_LABELS = {
|
|
33
35
|
[Permissions.VIEW_KPI]: { label: 'KPIs', description: 'Business KPIs for a chosen period.' },
|
|
@@ -40,6 +42,7 @@ exports.PERMISSION_LABELS = {
|
|
|
40
42
|
[Permissions.VIEW_PROMO]: { label: 'Promo and offers', description: 'Promo codes, offers, gifts and the wheel.' },
|
|
41
43
|
[Permissions.VIEW_BANNERS]: { label: 'Banners', description: 'The app banners.' },
|
|
42
44
|
[Permissions.VIEW_INVESTOR_NUMBERS]: { label: 'Investor numbers', description: 'Financial and unit-economics figures.' },
|
|
45
|
+
[Permissions.VIEW_CAR_MODELS]: { label: 'Car models', description: 'Review car models customers entered, and fix the size of unverified cars.' },
|
|
43
46
|
};
|
|
44
47
|
/** ADMIN opens every page; STAFF opens only the pages in their list. */
|
|
45
48
|
exports.STAFF_ROLES = ['ADMIN', 'STAFF'];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sahab-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "This package contains types and utils functions",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"prepublishOnly": "npm run build",
|
|
10
10
|
"deploy": "npm run build && npm publish",
|
|
11
|
-
"test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js && node tests/cashback.test.js && node tests/utils.test.js && node tests/wheel.test.js && node tests/calcs.test.js && node tests/orderActivity.test.js && node tests/push.test.js && node tests/broadcast.test.js && node tests/iban.test.js && node tests/cash.test.js && node tests/workingHours.test.js && node tests/payLink.test.js && node tests/offerings.test.js && node tests/capacity.test.js && node tests/tap.test.js && node tests/orderAdjustment.test.js && node tests/adminNotifications.test.js && node tests/staffAccess.test.js"
|
|
11
|
+
"test": "npm run build && node tests/loyalty.test.js && node tests/gifts.test.js && node tests/cashback.test.js && node tests/utils.test.js && node tests/wheel.test.js && node tests/calcs.test.js && node tests/orderActivity.test.js && node tests/push.test.js && node tests/broadcast.test.js && node tests/iban.test.js && node tests/cash.test.js && node tests/workingHours.test.js && node tests/payLink.test.js && node tests/offerings.test.js && node tests/capacity.test.js && node tests/tap.test.js && node tests/orderAdjustment.test.js && node tests/adminNotifications.test.js && node tests/staffAccess.test.js && node tests/carCatalogue.test.js"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|