@blackcode_sa/metaestetics-api 1.12.62 → 1.12.64
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/admin/index.d.mts +4 -2
- package/dist/admin/index.d.ts +4 -2
- package/dist/admin/index.js +4 -45
- package/dist/admin/index.mjs +4 -45
- package/dist/backoffice/index.d.mts +86 -1
- package/dist/backoffice/index.d.ts +86 -1
- package/dist/backoffice/index.js +308 -0
- package/dist/backoffice/index.mjs +306 -0
- package/dist/index.d.mts +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.js +545 -281
- package/dist/index.mjs +867 -603
- package/package.json +119 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -689
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +75 -75
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +40 -40
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +318 -318
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +11 -8
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +395 -395
- package/src/backoffice/services/technology.service.ts +1083 -1070
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +62 -62
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +163 -161
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -163
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2505 -2082
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +13 -13
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +1682 -1682
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +636 -683
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/appointment/index.ts +481 -453
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +489 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +44 -44
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +265 -265
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -273
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +130 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +493 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -216
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +189 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addDoc,
|
|
3
|
-
collection,
|
|
4
|
-
doc,
|
|
5
|
-
getDoc,
|
|
6
|
-
getDocs,
|
|
7
|
-
query,
|
|
8
|
-
updateDoc,
|
|
9
|
-
where,
|
|
10
|
-
limit,
|
|
11
|
-
orderBy,
|
|
12
|
-
startAfter,
|
|
13
|
-
getCountFromServer,
|
|
14
|
-
Query,
|
|
15
|
-
DocumentData,
|
|
16
|
-
QueryConstraint,
|
|
17
|
-
} from "firebase/firestore";
|
|
18
|
-
import { Brand, BRANDS_COLLECTION } from "../types/brand.types";
|
|
19
|
-
import { BaseService } from "../../services/base.service";
|
|
20
|
-
|
|
21
|
-
export class BrandService extends BaseService {
|
|
22
|
-
/**
|
|
23
|
-
* Gets reference to brands collection
|
|
24
|
-
*/
|
|
25
|
-
private getBrandsRef() {
|
|
26
|
-
return collection(this.db, BRANDS_COLLECTION);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Creates a new brand
|
|
31
|
-
*/
|
|
32
|
-
async create(
|
|
33
|
-
brand: Omit<Brand, "id" | "createdAt" | "updatedAt" | "name_lowercase">
|
|
34
|
-
) {
|
|
35
|
-
const now = new Date();
|
|
36
|
-
const newBrand: Omit<Brand, "id"> = {
|
|
37
|
-
...brand,
|
|
38
|
-
name_lowercase: brand.name.toLowerCase(),
|
|
39
|
-
createdAt: now,
|
|
40
|
-
updatedAt: now,
|
|
41
|
-
isActive: true,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const docRef = await addDoc(this.getBrandsRef(), newBrand);
|
|
45
|
-
return { id: docRef.id, ...newBrand };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Gets a paginated list of active brands, optionally filtered by name.
|
|
50
|
-
* @param rowsPerPage - The number of brands to fetch.
|
|
51
|
-
* @param searchTerm - An optional string to filter brand names by (starts-with search).
|
|
52
|
-
* @param lastVisible - An optional document snapshot to use as a cursor for pagination.
|
|
53
|
-
*/
|
|
54
|
-
async getAll(rowsPerPage: number, searchTerm?: string, lastVisible?: any) {
|
|
55
|
-
const constraints: QueryConstraint[] = [
|
|
56
|
-
where("isActive", "==", true),
|
|
57
|
-
orderBy("name_lowercase"),
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
if (searchTerm) {
|
|
61
|
-
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
|
62
|
-
constraints.push(where("name_lowercase", ">=", lowercasedSearchTerm));
|
|
63
|
-
constraints.push(
|
|
64
|
-
where("name_lowercase", "<=", lowercasedSearchTerm + "\uf8ff")
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (lastVisible) {
|
|
69
|
-
constraints.push(startAfter(lastVisible));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
constraints.push(limit(rowsPerPage));
|
|
73
|
-
|
|
74
|
-
const q = query(this.getBrandsRef(), ...constraints);
|
|
75
|
-
const snapshot = await getDocs(q);
|
|
76
|
-
|
|
77
|
-
const brands = snapshot.docs.map(
|
|
78
|
-
(doc) =>
|
|
79
|
-
({
|
|
80
|
-
id: doc.id,
|
|
81
|
-
...doc.data(),
|
|
82
|
-
} as Brand)
|
|
83
|
-
);
|
|
84
|
-
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
85
|
-
|
|
86
|
-
return { brands, lastVisible: newLastVisible };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Gets the total count of active brands, optionally filtered by name.
|
|
91
|
-
* @param searchTerm - An optional string to filter brand names by (starts-with search).
|
|
92
|
-
*/
|
|
93
|
-
async getBrandsCount(searchTerm?: string) {
|
|
94
|
-
const constraints: QueryConstraint[] = [where("isActive", "==", true)];
|
|
95
|
-
|
|
96
|
-
if (searchTerm) {
|
|
97
|
-
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
|
98
|
-
constraints.push(where("name_lowercase", ">=", lowercasedSearchTerm));
|
|
99
|
-
constraints.push(
|
|
100
|
-
where("name_lowercase", "<=", lowercasedSearchTerm + "\uf8ff")
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const q = query(this.getBrandsRef(), ...constraints);
|
|
105
|
-
const snapshot = await getCountFromServer(q);
|
|
106
|
-
return snapshot.data().count;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Gets all active brands for filter dropdowns (not paginated).
|
|
111
|
-
*/
|
|
112
|
-
async getAllForFilter(): Promise<Brand[]> {
|
|
113
|
-
const q = query(
|
|
114
|
-
this.getBrandsRef(),
|
|
115
|
-
where("isActive", "==", true),
|
|
116
|
-
orderBy("name")
|
|
117
|
-
);
|
|
118
|
-
const snapshot = await getDocs(q);
|
|
119
|
-
return snapshot.docs.map(
|
|
120
|
-
(doc) =>
|
|
121
|
-
({
|
|
122
|
-
id: doc.id,
|
|
123
|
-
...doc.data(),
|
|
124
|
-
} as Brand)
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Updates a brand
|
|
130
|
-
*/
|
|
131
|
-
async update(
|
|
132
|
-
brandId: string,
|
|
133
|
-
brand: Partial<Omit<Brand, "id" | "createdAt" | "name_lowercase">>
|
|
134
|
-
) {
|
|
135
|
-
const updateData: { [key: string]: any } = {
|
|
136
|
-
...brand,
|
|
137
|
-
updatedAt: new Date(),
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
if (brand.name) {
|
|
141
|
-
updateData.name_lowercase = brand.name.toLowerCase();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const docRef = doc(this.getBrandsRef(), brandId);
|
|
145
|
-
await updateDoc(docRef, updateData);
|
|
146
|
-
return this.getById(brandId);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Soft deletes a brand
|
|
151
|
-
*/
|
|
152
|
-
async delete(brandId: string) {
|
|
153
|
-
await this.update(brandId, {
|
|
154
|
-
isActive: false,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Gets a brand by ID
|
|
160
|
-
*/
|
|
161
|
-
async getById(brandId: string): Promise<Brand | null> {
|
|
162
|
-
const docRef = doc(this.getBrandsRef(), brandId);
|
|
163
|
-
const docSnap = await getDoc(docRef);
|
|
164
|
-
if (!docSnap.exists()) return null;
|
|
165
|
-
return {
|
|
166
|
-
id: docSnap.id,
|
|
167
|
-
...docSnap.data(),
|
|
168
|
-
} as Brand;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Exports brands to CSV string, suitable for Excel/Sheets.
|
|
173
|
-
* Includes headers and optional UTF-8 BOM.
|
|
174
|
-
* By default exports only active brands (set includeInactive to true to export all).
|
|
175
|
-
*/
|
|
176
|
-
async exportToCsv(options?: {
|
|
177
|
-
includeInactive?: boolean;
|
|
178
|
-
includeBom?: boolean;
|
|
179
|
-
}): Promise<string> {
|
|
180
|
-
const includeInactive = options?.includeInactive ?? false;
|
|
181
|
-
const includeBom = options?.includeBom ?? true;
|
|
182
|
-
|
|
183
|
-
const headers = [
|
|
184
|
-
"id",
|
|
185
|
-
"name",
|
|
186
|
-
"manufacturer",
|
|
187
|
-
"website",
|
|
188
|
-
"description",
|
|
189
|
-
"isActive",
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
const rows: string[] = [];
|
|
193
|
-
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
194
|
-
|
|
195
|
-
const PAGE_SIZE = 1000;
|
|
196
|
-
let cursor: any | undefined;
|
|
197
|
-
|
|
198
|
-
// Build base constraints
|
|
199
|
-
const baseConstraints: QueryConstraint[] = [];
|
|
200
|
-
if (!includeInactive) {
|
|
201
|
-
baseConstraints.push(where("isActive", "==", true));
|
|
202
|
-
}
|
|
203
|
-
baseConstraints.push(orderBy("name_lowercase"));
|
|
204
|
-
|
|
205
|
-
// Page through all results
|
|
206
|
-
// eslint-disable-next-line no-constant-condition
|
|
207
|
-
while (true) {
|
|
208
|
-
const constraints: QueryConstraint[] = [...baseConstraints, limit(PAGE_SIZE)];
|
|
209
|
-
if (cursor) constraints.push(startAfter(cursor));
|
|
210
|
-
|
|
211
|
-
const q = query(this.getBrandsRef(), ...constraints);
|
|
212
|
-
const snapshot = await getDocs(q);
|
|
213
|
-
if (snapshot.empty) break;
|
|
214
|
-
|
|
215
|
-
for (const d of snapshot.docs) {
|
|
216
|
-
const brand = ({ id: d.id, ...d.data() } as unknown) as Brand;
|
|
217
|
-
rows.push(this.brandToCsvRow(brand));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
221
|
-
if (snapshot.size < PAGE_SIZE) break;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const csvBody = rows.join("\r\n");
|
|
225
|
-
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private brandToCsvRow(brand: Brand): string {
|
|
229
|
-
const values = [
|
|
230
|
-
brand.id ?? "",
|
|
231
|
-
brand.name ?? "",
|
|
232
|
-
brand.manufacturer ?? "",
|
|
233
|
-
brand.website ?? "",
|
|
234
|
-
brand.description ?? "",
|
|
235
|
-
String(brand.isActive ?? ""),
|
|
236
|
-
];
|
|
237
|
-
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private formatDateIso(value: any): string {
|
|
241
|
-
// Firestore timestamps may come back as Date or Timestamp; handle both
|
|
242
|
-
if (value instanceof Date) return value.toISOString();
|
|
243
|
-
if (value && typeof value.toDate === "function") {
|
|
244
|
-
const d = value.toDate();
|
|
245
|
-
return d instanceof Date ? d.toISOString() : String(value);
|
|
246
|
-
}
|
|
247
|
-
return String(value ?? "");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private formatCsvValue(value: any): string {
|
|
251
|
-
const str = value === null || value === undefined ? "" : String(value);
|
|
252
|
-
// Escape double quotes by doubling them and wrap in quotes
|
|
253
|
-
const escaped = str.replace(/"/g, '""');
|
|
254
|
-
return `"${escaped}"`;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
addDoc,
|
|
3
|
+
collection,
|
|
4
|
+
doc,
|
|
5
|
+
getDoc,
|
|
6
|
+
getDocs,
|
|
7
|
+
query,
|
|
8
|
+
updateDoc,
|
|
9
|
+
where,
|
|
10
|
+
limit,
|
|
11
|
+
orderBy,
|
|
12
|
+
startAfter,
|
|
13
|
+
getCountFromServer,
|
|
14
|
+
Query,
|
|
15
|
+
DocumentData,
|
|
16
|
+
QueryConstraint,
|
|
17
|
+
} from "firebase/firestore";
|
|
18
|
+
import { Brand, BRANDS_COLLECTION } from "../types/brand.types";
|
|
19
|
+
import { BaseService } from "../../services/base.service";
|
|
20
|
+
|
|
21
|
+
export class BrandService extends BaseService {
|
|
22
|
+
/**
|
|
23
|
+
* Gets reference to brands collection
|
|
24
|
+
*/
|
|
25
|
+
private getBrandsRef() {
|
|
26
|
+
return collection(this.db, BRANDS_COLLECTION);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new brand
|
|
31
|
+
*/
|
|
32
|
+
async create(
|
|
33
|
+
brand: Omit<Brand, "id" | "createdAt" | "updatedAt" | "name_lowercase">
|
|
34
|
+
) {
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const newBrand: Omit<Brand, "id"> = {
|
|
37
|
+
...brand,
|
|
38
|
+
name_lowercase: brand.name.toLowerCase(),
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
isActive: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const docRef = await addDoc(this.getBrandsRef(), newBrand);
|
|
45
|
+
return { id: docRef.id, ...newBrand };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gets a paginated list of active brands, optionally filtered by name.
|
|
50
|
+
* @param rowsPerPage - The number of brands to fetch.
|
|
51
|
+
* @param searchTerm - An optional string to filter brand names by (starts-with search).
|
|
52
|
+
* @param lastVisible - An optional document snapshot to use as a cursor for pagination.
|
|
53
|
+
*/
|
|
54
|
+
async getAll(rowsPerPage: number, searchTerm?: string, lastVisible?: any) {
|
|
55
|
+
const constraints: QueryConstraint[] = [
|
|
56
|
+
where("isActive", "==", true),
|
|
57
|
+
orderBy("name_lowercase"),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (searchTerm) {
|
|
61
|
+
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
|
62
|
+
constraints.push(where("name_lowercase", ">=", lowercasedSearchTerm));
|
|
63
|
+
constraints.push(
|
|
64
|
+
where("name_lowercase", "<=", lowercasedSearchTerm + "\uf8ff")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (lastVisible) {
|
|
69
|
+
constraints.push(startAfter(lastVisible));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constraints.push(limit(rowsPerPage));
|
|
73
|
+
|
|
74
|
+
const q = query(this.getBrandsRef(), ...constraints);
|
|
75
|
+
const snapshot = await getDocs(q);
|
|
76
|
+
|
|
77
|
+
const brands = snapshot.docs.map(
|
|
78
|
+
(doc) =>
|
|
79
|
+
({
|
|
80
|
+
id: doc.id,
|
|
81
|
+
...doc.data(),
|
|
82
|
+
} as Brand)
|
|
83
|
+
);
|
|
84
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
85
|
+
|
|
86
|
+
return { brands, lastVisible: newLastVisible };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Gets the total count of active brands, optionally filtered by name.
|
|
91
|
+
* @param searchTerm - An optional string to filter brand names by (starts-with search).
|
|
92
|
+
*/
|
|
93
|
+
async getBrandsCount(searchTerm?: string) {
|
|
94
|
+
const constraints: QueryConstraint[] = [where("isActive", "==", true)];
|
|
95
|
+
|
|
96
|
+
if (searchTerm) {
|
|
97
|
+
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
|
98
|
+
constraints.push(where("name_lowercase", ">=", lowercasedSearchTerm));
|
|
99
|
+
constraints.push(
|
|
100
|
+
where("name_lowercase", "<=", lowercasedSearchTerm + "\uf8ff")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const q = query(this.getBrandsRef(), ...constraints);
|
|
105
|
+
const snapshot = await getCountFromServer(q);
|
|
106
|
+
return snapshot.data().count;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gets all active brands for filter dropdowns (not paginated).
|
|
111
|
+
*/
|
|
112
|
+
async getAllForFilter(): Promise<Brand[]> {
|
|
113
|
+
const q = query(
|
|
114
|
+
this.getBrandsRef(),
|
|
115
|
+
where("isActive", "==", true),
|
|
116
|
+
orderBy("name")
|
|
117
|
+
);
|
|
118
|
+
const snapshot = await getDocs(q);
|
|
119
|
+
return snapshot.docs.map(
|
|
120
|
+
(doc) =>
|
|
121
|
+
({
|
|
122
|
+
id: doc.id,
|
|
123
|
+
...doc.data(),
|
|
124
|
+
} as Brand)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Updates a brand
|
|
130
|
+
*/
|
|
131
|
+
async update(
|
|
132
|
+
brandId: string,
|
|
133
|
+
brand: Partial<Omit<Brand, "id" | "createdAt" | "name_lowercase">>
|
|
134
|
+
) {
|
|
135
|
+
const updateData: { [key: string]: any } = {
|
|
136
|
+
...brand,
|
|
137
|
+
updatedAt: new Date(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (brand.name) {
|
|
141
|
+
updateData.name_lowercase = brand.name.toLowerCase();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const docRef = doc(this.getBrandsRef(), brandId);
|
|
145
|
+
await updateDoc(docRef, updateData);
|
|
146
|
+
return this.getById(brandId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Soft deletes a brand
|
|
151
|
+
*/
|
|
152
|
+
async delete(brandId: string) {
|
|
153
|
+
await this.update(brandId, {
|
|
154
|
+
isActive: false,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Gets a brand by ID
|
|
160
|
+
*/
|
|
161
|
+
async getById(brandId: string): Promise<Brand | null> {
|
|
162
|
+
const docRef = doc(this.getBrandsRef(), brandId);
|
|
163
|
+
const docSnap = await getDoc(docRef);
|
|
164
|
+
if (!docSnap.exists()) return null;
|
|
165
|
+
return {
|
|
166
|
+
id: docSnap.id,
|
|
167
|
+
...docSnap.data(),
|
|
168
|
+
} as Brand;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Exports brands to CSV string, suitable for Excel/Sheets.
|
|
173
|
+
* Includes headers and optional UTF-8 BOM.
|
|
174
|
+
* By default exports only active brands (set includeInactive to true to export all).
|
|
175
|
+
*/
|
|
176
|
+
async exportToCsv(options?: {
|
|
177
|
+
includeInactive?: boolean;
|
|
178
|
+
includeBom?: boolean;
|
|
179
|
+
}): Promise<string> {
|
|
180
|
+
const includeInactive = options?.includeInactive ?? false;
|
|
181
|
+
const includeBom = options?.includeBom ?? true;
|
|
182
|
+
|
|
183
|
+
const headers = [
|
|
184
|
+
"id",
|
|
185
|
+
"name",
|
|
186
|
+
"manufacturer",
|
|
187
|
+
"website",
|
|
188
|
+
"description",
|
|
189
|
+
"isActive",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const rows: string[] = [];
|
|
193
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
194
|
+
|
|
195
|
+
const PAGE_SIZE = 1000;
|
|
196
|
+
let cursor: any | undefined;
|
|
197
|
+
|
|
198
|
+
// Build base constraints
|
|
199
|
+
const baseConstraints: QueryConstraint[] = [];
|
|
200
|
+
if (!includeInactive) {
|
|
201
|
+
baseConstraints.push(where("isActive", "==", true));
|
|
202
|
+
}
|
|
203
|
+
baseConstraints.push(orderBy("name_lowercase"));
|
|
204
|
+
|
|
205
|
+
// Page through all results
|
|
206
|
+
// eslint-disable-next-line no-constant-condition
|
|
207
|
+
while (true) {
|
|
208
|
+
const constraints: QueryConstraint[] = [...baseConstraints, limit(PAGE_SIZE)];
|
|
209
|
+
if (cursor) constraints.push(startAfter(cursor));
|
|
210
|
+
|
|
211
|
+
const q = query(this.getBrandsRef(), ...constraints);
|
|
212
|
+
const snapshot = await getDocs(q);
|
|
213
|
+
if (snapshot.empty) break;
|
|
214
|
+
|
|
215
|
+
for (const d of snapshot.docs) {
|
|
216
|
+
const brand = ({ id: d.id, ...d.data() } as unknown) as Brand;
|
|
217
|
+
rows.push(this.brandToCsvRow(brand));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
221
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const csvBody = rows.join("\r\n");
|
|
225
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private brandToCsvRow(brand: Brand): string {
|
|
229
|
+
const values = [
|
|
230
|
+
brand.id ?? "",
|
|
231
|
+
brand.name ?? "",
|
|
232
|
+
brand.manufacturer ?? "",
|
|
233
|
+
brand.website ?? "",
|
|
234
|
+
brand.description ?? "",
|
|
235
|
+
String(brand.isActive ?? ""),
|
|
236
|
+
];
|
|
237
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private formatDateIso(value: any): string {
|
|
241
|
+
// Firestore timestamps may come back as Date or Timestamp; handle both
|
|
242
|
+
if (value instanceof Date) return value.toISOString();
|
|
243
|
+
if (value && typeof value.toDate === "function") {
|
|
244
|
+
const d = value.toDate();
|
|
245
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
246
|
+
}
|
|
247
|
+
return String(value ?? "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private formatCsvValue(value: any): string {
|
|
251
|
+
const str = value === null || value === undefined ? "" : String(value);
|
|
252
|
+
// Escape double quotes by doubling them and wrap in quotes
|
|
253
|
+
const escaped = str.replace(/"/g, '""');
|
|
254
|
+
return `"${escaped}"`;
|
|
255
|
+
}
|
|
256
|
+
}
|