@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,395 +1,395 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addDoc,
|
|
3
|
-
collection,
|
|
4
|
-
collectionGroup,
|
|
5
|
-
deleteDoc,
|
|
6
|
-
doc,
|
|
7
|
-
DocumentData,
|
|
8
|
-
getCountFromServer,
|
|
9
|
-
getDoc,
|
|
10
|
-
getDocs,
|
|
11
|
-
limit,
|
|
12
|
-
orderBy,
|
|
13
|
-
query,
|
|
14
|
-
setDoc,
|
|
15
|
-
startAfter,
|
|
16
|
-
updateDoc,
|
|
17
|
-
where,
|
|
18
|
-
} from "firebase/firestore";
|
|
19
|
-
import {
|
|
20
|
-
Subcategory,
|
|
21
|
-
SUBCATEGORIES_COLLECTION,
|
|
22
|
-
} from "../types/subcategory.types";
|
|
23
|
-
import { BaseService } from "../../services/base.service";
|
|
24
|
-
import { CATEGORIES_COLLECTION } from "../types/category.types";
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Servis za upravljanje podkategorijama procedura.
|
|
28
|
-
* Podkategorije su drugi nivo organizacije i pripadaju određenoj kategoriji.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* const subcategoryService = new SubcategoryService();
|
|
32
|
-
*
|
|
33
|
-
* // Kreiranje nove podkategorije
|
|
34
|
-
* const subcategory = await subcategoryService.create(categoryId, {
|
|
35
|
-
* name: "Anti-Wrinkle",
|
|
36
|
-
* description: "Treatments targeting facial wrinkles"
|
|
37
|
-
* });
|
|
38
|
-
*/
|
|
39
|
-
export class SubcategoryService extends BaseService {
|
|
40
|
-
/**
|
|
41
|
-
* Vraća referencu na Firestore kolekciju podkategorija za određenu kategoriju
|
|
42
|
-
* @param categoryId - ID roditeljske kategorije
|
|
43
|
-
*/
|
|
44
|
-
private getSubcategoriesRef(categoryId: string) {
|
|
45
|
-
return collection(
|
|
46
|
-
this.db,
|
|
47
|
-
CATEGORIES_COLLECTION,
|
|
48
|
-
categoryId,
|
|
49
|
-
SUBCATEGORIES_COLLECTION
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Kreira novu podkategoriju u okviru kategorije
|
|
55
|
-
* @param categoryId - ID kategorije kojoj će pripadati nova podkategorija
|
|
56
|
-
* @param subcategory - Podaci za novu podkategoriju
|
|
57
|
-
* @returns Kreirana podkategorija sa generisanim ID-em
|
|
58
|
-
*/
|
|
59
|
-
async create(
|
|
60
|
-
categoryId: string,
|
|
61
|
-
subcategory: Omit<Subcategory, "id" | "createdAt" | "updatedAt">
|
|
62
|
-
) {
|
|
63
|
-
const now = new Date();
|
|
64
|
-
const newSubcategory: Omit<Subcategory, "id"> = {
|
|
65
|
-
...subcategory,
|
|
66
|
-
categoryId,
|
|
67
|
-
createdAt: now,
|
|
68
|
-
updatedAt: now,
|
|
69
|
-
isActive: true,
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const docRef = await addDoc(
|
|
73
|
-
this.getSubcategoriesRef(categoryId),
|
|
74
|
-
newSubcategory
|
|
75
|
-
);
|
|
76
|
-
return { id: docRef.id, ...newSubcategory };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Returns counts of subcategories for all categories.
|
|
81
|
-
* @param active - Whether to count active or inactive subcategories.
|
|
82
|
-
* @returns A record mapping category ID to subcategory count.
|
|
83
|
-
*/
|
|
84
|
-
async getSubcategoryCounts(active = true) {
|
|
85
|
-
const categoriesRef = collection(this.db, CATEGORIES_COLLECTION);
|
|
86
|
-
const categoriesSnapshot = await getDocs(categoriesRef);
|
|
87
|
-
const counts: Record<string, number> = {};
|
|
88
|
-
|
|
89
|
-
for (const categoryDoc of categoriesSnapshot.docs) {
|
|
90
|
-
const categoryId = categoryDoc.id;
|
|
91
|
-
const subcategoriesRef = this.getSubcategoriesRef(categoryId);
|
|
92
|
-
const q = query(subcategoriesRef, where("isActive", "==", active));
|
|
93
|
-
const snapshot = await getCountFromServer(q);
|
|
94
|
-
counts[categoryId] = snapshot.data().count;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return counts;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Vraća sve aktivne podkategorije za određenu kategoriju sa paginacijom
|
|
102
|
-
* @param categoryId - ID kategorije čije podkategorije tražimo
|
|
103
|
-
* @param options - Pagination options
|
|
104
|
-
* @returns Lista aktivnih podkategorija i poslednji vidljiv dokument
|
|
105
|
-
*/
|
|
106
|
-
async getAllByCategoryId(
|
|
107
|
-
categoryId: string,
|
|
108
|
-
options: {
|
|
109
|
-
active?: boolean;
|
|
110
|
-
limit?: number;
|
|
111
|
-
lastVisible?: DocumentData;
|
|
112
|
-
} = {}
|
|
113
|
-
) {
|
|
114
|
-
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
115
|
-
const constraints = [
|
|
116
|
-
where("isActive", "==", active),
|
|
117
|
-
orderBy("name"),
|
|
118
|
-
queryLimit ? limit(queryLimit) : undefined,
|
|
119
|
-
lastVisible ? startAfter(lastVisible) : undefined,
|
|
120
|
-
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
121
|
-
|
|
122
|
-
const q = query(this.getSubcategoriesRef(categoryId), ...constraints);
|
|
123
|
-
|
|
124
|
-
const querySnapshot = await getDocs(q);
|
|
125
|
-
const subcategories = querySnapshot.docs.map(
|
|
126
|
-
(doc) =>
|
|
127
|
-
({
|
|
128
|
-
id: doc.id,
|
|
129
|
-
...doc.data(),
|
|
130
|
-
} as Subcategory)
|
|
131
|
-
);
|
|
132
|
-
const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
|
|
133
|
-
return { subcategories, lastVisible: newLastVisible };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Vraća sve podkategorije sa paginacijom koristeći collection group query.
|
|
138
|
-
* NOTE: This query requires a composite index in Firestore on the 'subcategories' collection group.
|
|
139
|
-
* The index should be on 'isActive' (ascending) and 'name' (ascending).
|
|
140
|
-
* Firestore will provide a link to create this index in the console error if it's missing.
|
|
141
|
-
* @param options - Pagination options
|
|
142
|
-
* @returns Lista podkategorija i poslednji vidljiv dokument
|
|
143
|
-
*/
|
|
144
|
-
async getAll(
|
|
145
|
-
options: {
|
|
146
|
-
active?: boolean;
|
|
147
|
-
limit?: number;
|
|
148
|
-
lastVisible?: DocumentData;
|
|
149
|
-
} = {}
|
|
150
|
-
) {
|
|
151
|
-
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
152
|
-
const constraints = [
|
|
153
|
-
where("isActive", "==", active),
|
|
154
|
-
orderBy("name"),
|
|
155
|
-
queryLimit ? limit(queryLimit) : undefined,
|
|
156
|
-
lastVisible ? startAfter(lastVisible) : undefined,
|
|
157
|
-
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
158
|
-
|
|
159
|
-
const q = query(
|
|
160
|
-
collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
|
|
161
|
-
...constraints
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const querySnapshot = await getDocs(q);
|
|
165
|
-
const subcategories = querySnapshot.docs.map(
|
|
166
|
-
(doc) =>
|
|
167
|
-
({
|
|
168
|
-
id: doc.id,
|
|
169
|
-
...doc.data(),
|
|
170
|
-
} as Subcategory)
|
|
171
|
-
);
|
|
172
|
-
const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
|
|
173
|
-
return { subcategories, lastVisible: newLastVisible };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Vraća sve subkategorije za određenu kategoriju za potrebe filtera (bez paginacije)
|
|
178
|
-
* @param categoryId - ID kategorije čije subkategorije tražimo
|
|
179
|
-
* @returns Lista svih aktivnih subkategorija
|
|
180
|
-
*/
|
|
181
|
-
async getAllForFilterByCategoryId(categoryId: string) {
|
|
182
|
-
const q = query(
|
|
183
|
-
this.getSubcategoriesRef(categoryId),
|
|
184
|
-
where("isActive", "==", true)
|
|
185
|
-
);
|
|
186
|
-
const querySnapshot = await getDocs(q);
|
|
187
|
-
return querySnapshot.docs.map(
|
|
188
|
-
(doc) =>
|
|
189
|
-
({
|
|
190
|
-
id: doc.id,
|
|
191
|
-
...doc.data(),
|
|
192
|
-
} as Subcategory)
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Vraća sve subkategorije za potrebe filtera (bez paginacije)
|
|
198
|
-
* @returns Lista svih aktivnih subkategorija
|
|
199
|
-
*/
|
|
200
|
-
async getAllForFilter() {
|
|
201
|
-
const q = query(
|
|
202
|
-
collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
|
|
203
|
-
where("isActive", "==", true)
|
|
204
|
-
);
|
|
205
|
-
const querySnapshot = await getDocs(q);
|
|
206
|
-
return querySnapshot.docs.map(
|
|
207
|
-
(doc) =>
|
|
208
|
-
({
|
|
209
|
-
id: doc.id,
|
|
210
|
-
...doc.data(),
|
|
211
|
-
} as Subcategory)
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Ažurira postojeću podkategoriju
|
|
217
|
-
* @param categoryId - ID kategorije kojoj pripada podkategorija
|
|
218
|
-
* @param subcategoryId - ID podkategorije koja se ažurira
|
|
219
|
-
* @param subcategory - Novi podaci za podkategoriju
|
|
220
|
-
* @returns Ažurirana podkategorija
|
|
221
|
-
*/
|
|
222
|
-
async update(
|
|
223
|
-
categoryId: string,
|
|
224
|
-
subcategoryId: string,
|
|
225
|
-
subcategory: Partial<Omit<Subcategory, "id" | "createdAt">>
|
|
226
|
-
) {
|
|
227
|
-
const newCategoryId = subcategory.categoryId;
|
|
228
|
-
|
|
229
|
-
if (newCategoryId && newCategoryId !== categoryId) {
|
|
230
|
-
// Category has changed, move the document
|
|
231
|
-
const oldDocRef = doc(
|
|
232
|
-
this.getSubcategoriesRef(categoryId),
|
|
233
|
-
subcategoryId
|
|
234
|
-
);
|
|
235
|
-
const docSnap = await getDoc(oldDocRef);
|
|
236
|
-
|
|
237
|
-
if (!docSnap.exists()) {
|
|
238
|
-
throw new Error("Subcategory to update does not exist.");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const existingData = docSnap.data();
|
|
242
|
-
const newData: Omit<Subcategory, "id"> = {
|
|
243
|
-
...(existingData as Omit<
|
|
244
|
-
Subcategory,
|
|
245
|
-
"id" | "createdAt" | "updatedAt"
|
|
246
|
-
>),
|
|
247
|
-
...subcategory,
|
|
248
|
-
categoryId: newCategoryId, // Ensure categoryId is updated
|
|
249
|
-
createdAt: existingData.createdAt, // Preserve original creation date
|
|
250
|
-
updatedAt: new Date(),
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const newDocRef = doc(
|
|
254
|
-
this.getSubcategoriesRef(newCategoryId),
|
|
255
|
-
subcategoryId
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
await setDoc(newDocRef, newData);
|
|
259
|
-
await deleteDoc(oldDocRef);
|
|
260
|
-
|
|
261
|
-
return { id: subcategoryId, ...newData };
|
|
262
|
-
} else {
|
|
263
|
-
// Category has not changed, just update the document
|
|
264
|
-
const updateData = {
|
|
265
|
-
...subcategory,
|
|
266
|
-
updatedAt: new Date(),
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
|
|
270
|
-
await updateDoc(docRef, updateData);
|
|
271
|
-
return this.getById(categoryId, subcategoryId);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Soft delete podkategorije (postavlja isActive na false)
|
|
277
|
-
* @param categoryId - ID kategorije kojoj pripada podkategorija
|
|
278
|
-
* @param subcategoryId - ID podkategorije koja se briše
|
|
279
|
-
*/
|
|
280
|
-
async delete(categoryId: string, subcategoryId: string) {
|
|
281
|
-
await this.update(categoryId, subcategoryId, { isActive: false });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Reactivates a subcategory by setting its isActive flag to true.
|
|
286
|
-
* @param categoryId - The ID of the category to which the subcategory belongs.
|
|
287
|
-
* @param subcategoryId - The ID of the subcategory to reactivate.
|
|
288
|
-
*/
|
|
289
|
-
async reactivate(categoryId: string, subcategoryId: string) {
|
|
290
|
-
await this.update(categoryId, subcategoryId, { isActive: true });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Vraća podkategoriju po ID-u
|
|
295
|
-
* @param categoryId - ID kategorije kojoj pripada podkategorija
|
|
296
|
-
* @param subcategoryId - ID tražene podkategorije
|
|
297
|
-
* @returns Podkategorija ili null ako ne postoji
|
|
298
|
-
*/
|
|
299
|
-
async getById(categoryId: string, subcategoryId: string) {
|
|
300
|
-
const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
|
|
301
|
-
const docSnap = await getDoc(docRef);
|
|
302
|
-
if (!docSnap.exists()) return null;
|
|
303
|
-
return {
|
|
304
|
-
id: docSnap.id,
|
|
305
|
-
...docSnap.data(),
|
|
306
|
-
} as Subcategory;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Exports subcategories to CSV string, suitable for Excel/Sheets.
|
|
311
|
-
* Includes headers and optional UTF-8 BOM.
|
|
312
|
-
* By default exports only active subcategories (set includeInactive to true to export all).
|
|
313
|
-
*/
|
|
314
|
-
async exportToCsv(options?: {
|
|
315
|
-
includeInactive?: boolean;
|
|
316
|
-
includeBom?: boolean;
|
|
317
|
-
}): Promise<string> {
|
|
318
|
-
const includeInactive = options?.includeInactive ?? false;
|
|
319
|
-
const includeBom = options?.includeBom ?? true;
|
|
320
|
-
|
|
321
|
-
const headers = [
|
|
322
|
-
"id",
|
|
323
|
-
"name",
|
|
324
|
-
"categoryId",
|
|
325
|
-
"description",
|
|
326
|
-
"isActive",
|
|
327
|
-
];
|
|
328
|
-
|
|
329
|
-
const rows: string[] = [];
|
|
330
|
-
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
331
|
-
|
|
332
|
-
const PAGE_SIZE = 1000;
|
|
333
|
-
let cursor: any | undefined;
|
|
334
|
-
|
|
335
|
-
// Build base constraints
|
|
336
|
-
const constraints: any[] = [];
|
|
337
|
-
if (!includeInactive) {
|
|
338
|
-
constraints.push(where("isActive", "==", true));
|
|
339
|
-
}
|
|
340
|
-
constraints.push(orderBy("name"));
|
|
341
|
-
|
|
342
|
-
// Page through all results using collectionGroup
|
|
343
|
-
// eslint-disable-next-line no-constant-condition
|
|
344
|
-
while (true) {
|
|
345
|
-
const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
|
|
346
|
-
if (cursor) queryConstraints.push(startAfter(cursor));
|
|
347
|
-
|
|
348
|
-
const q = query(
|
|
349
|
-
collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
|
|
350
|
-
...queryConstraints
|
|
351
|
-
);
|
|
352
|
-
const snapshot = await getDocs(q);
|
|
353
|
-
if (snapshot.empty) break;
|
|
354
|
-
|
|
355
|
-
for (const d of snapshot.docs) {
|
|
356
|
-
const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
|
|
357
|
-
rows.push(this.subcategoryToCsvRow(subcategory));
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
361
|
-
if (snapshot.size < PAGE_SIZE) break;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const csvBody = rows.join("\r\n");
|
|
365
|
-
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private subcategoryToCsvRow(subcategory: Subcategory): string {
|
|
369
|
-
const values = [
|
|
370
|
-
subcategory.id ?? "",
|
|
371
|
-
subcategory.name ?? "",
|
|
372
|
-
subcategory.categoryId ?? "",
|
|
373
|
-
subcategory.description ?? "",
|
|
374
|
-
String(subcategory.isActive ?? ""),
|
|
375
|
-
];
|
|
376
|
-
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
private formatDateIso(value: any): string {
|
|
380
|
-
// Firestore timestamps may come back as Date or Timestamp; handle both
|
|
381
|
-
if (value instanceof Date) return value.toISOString();
|
|
382
|
-
if (value && typeof value.toDate === "function") {
|
|
383
|
-
const d = value.toDate();
|
|
384
|
-
return d instanceof Date ? d.toISOString() : String(value);
|
|
385
|
-
}
|
|
386
|
-
return String(value ?? "");
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
private formatCsvValue(value: any): string {
|
|
390
|
-
const str = value === null || value === undefined ? "" : String(value);
|
|
391
|
-
// Escape double quotes by doubling them and wrap in quotes
|
|
392
|
-
const escaped = str.replace(/"/g, '""');
|
|
393
|
-
return `"${escaped}"`;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
addDoc,
|
|
3
|
+
collection,
|
|
4
|
+
collectionGroup,
|
|
5
|
+
deleteDoc,
|
|
6
|
+
doc,
|
|
7
|
+
DocumentData,
|
|
8
|
+
getCountFromServer,
|
|
9
|
+
getDoc,
|
|
10
|
+
getDocs,
|
|
11
|
+
limit,
|
|
12
|
+
orderBy,
|
|
13
|
+
query,
|
|
14
|
+
setDoc,
|
|
15
|
+
startAfter,
|
|
16
|
+
updateDoc,
|
|
17
|
+
where,
|
|
18
|
+
} from "firebase/firestore";
|
|
19
|
+
import {
|
|
20
|
+
Subcategory,
|
|
21
|
+
SUBCATEGORIES_COLLECTION,
|
|
22
|
+
} from "../types/subcategory.types";
|
|
23
|
+
import { BaseService } from "../../services/base.service";
|
|
24
|
+
import { CATEGORIES_COLLECTION } from "../types/category.types";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Servis za upravljanje podkategorijama procedura.
|
|
28
|
+
* Podkategorije su drugi nivo organizacije i pripadaju određenoj kategoriji.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const subcategoryService = new SubcategoryService();
|
|
32
|
+
*
|
|
33
|
+
* // Kreiranje nove podkategorije
|
|
34
|
+
* const subcategory = await subcategoryService.create(categoryId, {
|
|
35
|
+
* name: "Anti-Wrinkle",
|
|
36
|
+
* description: "Treatments targeting facial wrinkles"
|
|
37
|
+
* });
|
|
38
|
+
*/
|
|
39
|
+
export class SubcategoryService extends BaseService {
|
|
40
|
+
/**
|
|
41
|
+
* Vraća referencu na Firestore kolekciju podkategorija za određenu kategoriju
|
|
42
|
+
* @param categoryId - ID roditeljske kategorije
|
|
43
|
+
*/
|
|
44
|
+
private getSubcategoriesRef(categoryId: string) {
|
|
45
|
+
return collection(
|
|
46
|
+
this.db,
|
|
47
|
+
CATEGORIES_COLLECTION,
|
|
48
|
+
categoryId,
|
|
49
|
+
SUBCATEGORIES_COLLECTION
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Kreira novu podkategoriju u okviru kategorije
|
|
55
|
+
* @param categoryId - ID kategorije kojoj će pripadati nova podkategorija
|
|
56
|
+
* @param subcategory - Podaci za novu podkategoriju
|
|
57
|
+
* @returns Kreirana podkategorija sa generisanim ID-em
|
|
58
|
+
*/
|
|
59
|
+
async create(
|
|
60
|
+
categoryId: string,
|
|
61
|
+
subcategory: Omit<Subcategory, "id" | "createdAt" | "updatedAt">
|
|
62
|
+
) {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const newSubcategory: Omit<Subcategory, "id"> = {
|
|
65
|
+
...subcategory,
|
|
66
|
+
categoryId,
|
|
67
|
+
createdAt: now,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
isActive: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const docRef = await addDoc(
|
|
73
|
+
this.getSubcategoriesRef(categoryId),
|
|
74
|
+
newSubcategory
|
|
75
|
+
);
|
|
76
|
+
return { id: docRef.id, ...newSubcategory };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns counts of subcategories for all categories.
|
|
81
|
+
* @param active - Whether to count active or inactive subcategories.
|
|
82
|
+
* @returns A record mapping category ID to subcategory count.
|
|
83
|
+
*/
|
|
84
|
+
async getSubcategoryCounts(active = true) {
|
|
85
|
+
const categoriesRef = collection(this.db, CATEGORIES_COLLECTION);
|
|
86
|
+
const categoriesSnapshot = await getDocs(categoriesRef);
|
|
87
|
+
const counts: Record<string, number> = {};
|
|
88
|
+
|
|
89
|
+
for (const categoryDoc of categoriesSnapshot.docs) {
|
|
90
|
+
const categoryId = categoryDoc.id;
|
|
91
|
+
const subcategoriesRef = this.getSubcategoriesRef(categoryId);
|
|
92
|
+
const q = query(subcategoriesRef, where("isActive", "==", active));
|
|
93
|
+
const snapshot = await getCountFromServer(q);
|
|
94
|
+
counts[categoryId] = snapshot.data().count;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return counts;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Vraća sve aktivne podkategorije za određenu kategoriju sa paginacijom
|
|
102
|
+
* @param categoryId - ID kategorije čije podkategorije tražimo
|
|
103
|
+
* @param options - Pagination options
|
|
104
|
+
* @returns Lista aktivnih podkategorija i poslednji vidljiv dokument
|
|
105
|
+
*/
|
|
106
|
+
async getAllByCategoryId(
|
|
107
|
+
categoryId: string,
|
|
108
|
+
options: {
|
|
109
|
+
active?: boolean;
|
|
110
|
+
limit?: number;
|
|
111
|
+
lastVisible?: DocumentData;
|
|
112
|
+
} = {}
|
|
113
|
+
) {
|
|
114
|
+
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
115
|
+
const constraints = [
|
|
116
|
+
where("isActive", "==", active),
|
|
117
|
+
orderBy("name"),
|
|
118
|
+
queryLimit ? limit(queryLimit) : undefined,
|
|
119
|
+
lastVisible ? startAfter(lastVisible) : undefined,
|
|
120
|
+
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
121
|
+
|
|
122
|
+
const q = query(this.getSubcategoriesRef(categoryId), ...constraints);
|
|
123
|
+
|
|
124
|
+
const querySnapshot = await getDocs(q);
|
|
125
|
+
const subcategories = querySnapshot.docs.map(
|
|
126
|
+
(doc) =>
|
|
127
|
+
({
|
|
128
|
+
id: doc.id,
|
|
129
|
+
...doc.data(),
|
|
130
|
+
} as Subcategory)
|
|
131
|
+
);
|
|
132
|
+
const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
|
|
133
|
+
return { subcategories, lastVisible: newLastVisible };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Vraća sve podkategorije sa paginacijom koristeći collection group query.
|
|
138
|
+
* NOTE: This query requires a composite index in Firestore on the 'subcategories' collection group.
|
|
139
|
+
* The index should be on 'isActive' (ascending) and 'name' (ascending).
|
|
140
|
+
* Firestore will provide a link to create this index in the console error if it's missing.
|
|
141
|
+
* @param options - Pagination options
|
|
142
|
+
* @returns Lista podkategorija i poslednji vidljiv dokument
|
|
143
|
+
*/
|
|
144
|
+
async getAll(
|
|
145
|
+
options: {
|
|
146
|
+
active?: boolean;
|
|
147
|
+
limit?: number;
|
|
148
|
+
lastVisible?: DocumentData;
|
|
149
|
+
} = {}
|
|
150
|
+
) {
|
|
151
|
+
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
152
|
+
const constraints = [
|
|
153
|
+
where("isActive", "==", active),
|
|
154
|
+
orderBy("name"),
|
|
155
|
+
queryLimit ? limit(queryLimit) : undefined,
|
|
156
|
+
lastVisible ? startAfter(lastVisible) : undefined,
|
|
157
|
+
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
158
|
+
|
|
159
|
+
const q = query(
|
|
160
|
+
collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
|
|
161
|
+
...constraints
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const querySnapshot = await getDocs(q);
|
|
165
|
+
const subcategories = querySnapshot.docs.map(
|
|
166
|
+
(doc) =>
|
|
167
|
+
({
|
|
168
|
+
id: doc.id,
|
|
169
|
+
...doc.data(),
|
|
170
|
+
} as Subcategory)
|
|
171
|
+
);
|
|
172
|
+
const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
|
|
173
|
+
return { subcategories, lastVisible: newLastVisible };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Vraća sve subkategorije za određenu kategoriju za potrebe filtera (bez paginacije)
|
|
178
|
+
* @param categoryId - ID kategorije čije subkategorije tražimo
|
|
179
|
+
* @returns Lista svih aktivnih subkategorija
|
|
180
|
+
*/
|
|
181
|
+
async getAllForFilterByCategoryId(categoryId: string) {
|
|
182
|
+
const q = query(
|
|
183
|
+
this.getSubcategoriesRef(categoryId),
|
|
184
|
+
where("isActive", "==", true)
|
|
185
|
+
);
|
|
186
|
+
const querySnapshot = await getDocs(q);
|
|
187
|
+
return querySnapshot.docs.map(
|
|
188
|
+
(doc) =>
|
|
189
|
+
({
|
|
190
|
+
id: doc.id,
|
|
191
|
+
...doc.data(),
|
|
192
|
+
} as Subcategory)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Vraća sve subkategorije za potrebe filtera (bez paginacije)
|
|
198
|
+
* @returns Lista svih aktivnih subkategorija
|
|
199
|
+
*/
|
|
200
|
+
async getAllForFilter() {
|
|
201
|
+
const q = query(
|
|
202
|
+
collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
|
|
203
|
+
where("isActive", "==", true)
|
|
204
|
+
);
|
|
205
|
+
const querySnapshot = await getDocs(q);
|
|
206
|
+
return querySnapshot.docs.map(
|
|
207
|
+
(doc) =>
|
|
208
|
+
({
|
|
209
|
+
id: doc.id,
|
|
210
|
+
...doc.data(),
|
|
211
|
+
} as Subcategory)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Ažurira postojeću podkategoriju
|
|
217
|
+
* @param categoryId - ID kategorije kojoj pripada podkategorija
|
|
218
|
+
* @param subcategoryId - ID podkategorije koja se ažurira
|
|
219
|
+
* @param subcategory - Novi podaci za podkategoriju
|
|
220
|
+
* @returns Ažurirana podkategorija
|
|
221
|
+
*/
|
|
222
|
+
async update(
|
|
223
|
+
categoryId: string,
|
|
224
|
+
subcategoryId: string,
|
|
225
|
+
subcategory: Partial<Omit<Subcategory, "id" | "createdAt">>
|
|
226
|
+
) {
|
|
227
|
+
const newCategoryId = subcategory.categoryId;
|
|
228
|
+
|
|
229
|
+
if (newCategoryId && newCategoryId !== categoryId) {
|
|
230
|
+
// Category has changed, move the document
|
|
231
|
+
const oldDocRef = doc(
|
|
232
|
+
this.getSubcategoriesRef(categoryId),
|
|
233
|
+
subcategoryId
|
|
234
|
+
);
|
|
235
|
+
const docSnap = await getDoc(oldDocRef);
|
|
236
|
+
|
|
237
|
+
if (!docSnap.exists()) {
|
|
238
|
+
throw new Error("Subcategory to update does not exist.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const existingData = docSnap.data();
|
|
242
|
+
const newData: Omit<Subcategory, "id"> = {
|
|
243
|
+
...(existingData as Omit<
|
|
244
|
+
Subcategory,
|
|
245
|
+
"id" | "createdAt" | "updatedAt"
|
|
246
|
+
>),
|
|
247
|
+
...subcategory,
|
|
248
|
+
categoryId: newCategoryId, // Ensure categoryId is updated
|
|
249
|
+
createdAt: existingData.createdAt, // Preserve original creation date
|
|
250
|
+
updatedAt: new Date(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const newDocRef = doc(
|
|
254
|
+
this.getSubcategoriesRef(newCategoryId),
|
|
255
|
+
subcategoryId
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await setDoc(newDocRef, newData);
|
|
259
|
+
await deleteDoc(oldDocRef);
|
|
260
|
+
|
|
261
|
+
return { id: subcategoryId, ...newData };
|
|
262
|
+
} else {
|
|
263
|
+
// Category has not changed, just update the document
|
|
264
|
+
const updateData = {
|
|
265
|
+
...subcategory,
|
|
266
|
+
updatedAt: new Date(),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
|
|
270
|
+
await updateDoc(docRef, updateData);
|
|
271
|
+
return this.getById(categoryId, subcategoryId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Soft delete podkategorije (postavlja isActive na false)
|
|
277
|
+
* @param categoryId - ID kategorije kojoj pripada podkategorija
|
|
278
|
+
* @param subcategoryId - ID podkategorije koja se briše
|
|
279
|
+
*/
|
|
280
|
+
async delete(categoryId: string, subcategoryId: string) {
|
|
281
|
+
await this.update(categoryId, subcategoryId, { isActive: false });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Reactivates a subcategory by setting its isActive flag to true.
|
|
286
|
+
* @param categoryId - The ID of the category to which the subcategory belongs.
|
|
287
|
+
* @param subcategoryId - The ID of the subcategory to reactivate.
|
|
288
|
+
*/
|
|
289
|
+
async reactivate(categoryId: string, subcategoryId: string) {
|
|
290
|
+
await this.update(categoryId, subcategoryId, { isActive: true });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Vraća podkategoriju po ID-u
|
|
295
|
+
* @param categoryId - ID kategorije kojoj pripada podkategorija
|
|
296
|
+
* @param subcategoryId - ID tražene podkategorije
|
|
297
|
+
* @returns Podkategorija ili null ako ne postoji
|
|
298
|
+
*/
|
|
299
|
+
async getById(categoryId: string, subcategoryId: string) {
|
|
300
|
+
const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
|
|
301
|
+
const docSnap = await getDoc(docRef);
|
|
302
|
+
if (!docSnap.exists()) return null;
|
|
303
|
+
return {
|
|
304
|
+
id: docSnap.id,
|
|
305
|
+
...docSnap.data(),
|
|
306
|
+
} as Subcategory;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Exports subcategories to CSV string, suitable for Excel/Sheets.
|
|
311
|
+
* Includes headers and optional UTF-8 BOM.
|
|
312
|
+
* By default exports only active subcategories (set includeInactive to true to export all).
|
|
313
|
+
*/
|
|
314
|
+
async exportToCsv(options?: {
|
|
315
|
+
includeInactive?: boolean;
|
|
316
|
+
includeBom?: boolean;
|
|
317
|
+
}): Promise<string> {
|
|
318
|
+
const includeInactive = options?.includeInactive ?? false;
|
|
319
|
+
const includeBom = options?.includeBom ?? true;
|
|
320
|
+
|
|
321
|
+
const headers = [
|
|
322
|
+
"id",
|
|
323
|
+
"name",
|
|
324
|
+
"categoryId",
|
|
325
|
+
"description",
|
|
326
|
+
"isActive",
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const rows: string[] = [];
|
|
330
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
331
|
+
|
|
332
|
+
const PAGE_SIZE = 1000;
|
|
333
|
+
let cursor: any | undefined;
|
|
334
|
+
|
|
335
|
+
// Build base constraints
|
|
336
|
+
const constraints: any[] = [];
|
|
337
|
+
if (!includeInactive) {
|
|
338
|
+
constraints.push(where("isActive", "==", true));
|
|
339
|
+
}
|
|
340
|
+
constraints.push(orderBy("name"));
|
|
341
|
+
|
|
342
|
+
// Page through all results using collectionGroup
|
|
343
|
+
// eslint-disable-next-line no-constant-condition
|
|
344
|
+
while (true) {
|
|
345
|
+
const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
|
|
346
|
+
if (cursor) queryConstraints.push(startAfter(cursor));
|
|
347
|
+
|
|
348
|
+
const q = query(
|
|
349
|
+
collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
|
|
350
|
+
...queryConstraints
|
|
351
|
+
);
|
|
352
|
+
const snapshot = await getDocs(q);
|
|
353
|
+
if (snapshot.empty) break;
|
|
354
|
+
|
|
355
|
+
for (const d of snapshot.docs) {
|
|
356
|
+
const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
|
|
357
|
+
rows.push(this.subcategoryToCsvRow(subcategory));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
361
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const csvBody = rows.join("\r\n");
|
|
365
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private subcategoryToCsvRow(subcategory: Subcategory): string {
|
|
369
|
+
const values = [
|
|
370
|
+
subcategory.id ?? "",
|
|
371
|
+
subcategory.name ?? "",
|
|
372
|
+
subcategory.categoryId ?? "",
|
|
373
|
+
subcategory.description ?? "",
|
|
374
|
+
String(subcategory.isActive ?? ""),
|
|
375
|
+
];
|
|
376
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private formatDateIso(value: any): string {
|
|
380
|
+
// Firestore timestamps may come back as Date or Timestamp; handle both
|
|
381
|
+
if (value instanceof Date) return value.toISOString();
|
|
382
|
+
if (value && typeof value.toDate === "function") {
|
|
383
|
+
const d = value.toDate();
|
|
384
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
385
|
+
}
|
|
386
|
+
return String(value ?? "");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private formatCsvValue(value: any): string {
|
|
390
|
+
const str = value === null || value === undefined ? "" : String(value);
|
|
391
|
+
// Escape double quotes by doubling them and wrap in quotes
|
|
392
|
+
const escaped = str.replace(/"/g, '""');
|
|
393
|
+
return `"${escaped}"`;
|
|
394
|
+
}
|
|
395
|
+
}
|