@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8
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 +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/dist/index.d.mts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +168 -6
- package/dist/index.mjs +168 -6
- package/package.json +121 -121
- 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 +1984 -1984
- 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 +966 -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 +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- 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 +81 -81
- 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 +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- 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 +10 -10
- 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 +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- 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 +67 -67
- 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 +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- 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 +211 -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/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- 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 +1043 -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 +14 -14
- 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 +1799 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2307 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- 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/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -498
- 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 +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- 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 -275
- 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 +132 -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 +494 -494
- 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 -217
- 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 +195 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,1151 +1,1151 @@
|
|
|
1
|
-
import {
|
|
2
|
-
addDoc,
|
|
3
|
-
collection,
|
|
4
|
-
doc,
|
|
5
|
-
DocumentData,
|
|
6
|
-
getCountFromServer,
|
|
7
|
-
getDoc,
|
|
8
|
-
getDocs,
|
|
9
|
-
limit,
|
|
10
|
-
orderBy,
|
|
11
|
-
query,
|
|
12
|
-
startAfter,
|
|
13
|
-
updateDoc,
|
|
14
|
-
where,
|
|
15
|
-
arrayUnion,
|
|
16
|
-
arrayRemove,
|
|
17
|
-
Firestore,
|
|
18
|
-
writeBatch,
|
|
19
|
-
QueryConstraint,
|
|
20
|
-
} from 'firebase/firestore';
|
|
21
|
-
import { Technology, TECHNOLOGIES_COLLECTION, ITechnologyService } from '../types/technology.types';
|
|
22
|
-
import { Requirement, RequirementType } from '../types/requirement.types';
|
|
23
|
-
import { BlockingCondition } from '../types/static/blocking-condition.types';
|
|
24
|
-
import { ContraindicationDynamic } from '../types/admin-constants.types';
|
|
25
|
-
import { TreatmentBenefitDynamic } from '../types/admin-constants.types';
|
|
26
|
-
import {
|
|
27
|
-
CertificationLevel,
|
|
28
|
-
CertificationSpecialty,
|
|
29
|
-
CertificationRequirement,
|
|
30
|
-
} from '../types/static/certification.types';
|
|
31
|
-
import { BaseService } from '../../services/base.service';
|
|
32
|
-
import { ProcedureFamily } from '../types/static/procedure-family.types';
|
|
33
|
-
import { Practitioner, PractitionerCertification } from '../../types/practitioner';
|
|
34
|
-
import { Product, PRODUCTS_COLLECTION } from '../types/product.types';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* ID of the free-consultation-tech technology that should be hidden from admin backoffice.
|
|
38
|
-
* This technology is used internally for free consultation procedures.
|
|
39
|
-
*/
|
|
40
|
-
const EXCLUDED_TECHNOLOGY_ID = 'free-consultation-tech';
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Default vrednosti za sertifikaciju
|
|
44
|
-
*/
|
|
45
|
-
const DEFAULT_CERTIFICATION_REQUIREMENT: CertificationRequirement = {
|
|
46
|
-
minimumLevel: CertificationLevel.AESTHETICIAN,
|
|
47
|
-
requiredSpecialties: [],
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Service for managing technologies.
|
|
52
|
-
*/
|
|
53
|
-
export class TechnologyService extends BaseService implements ITechnologyService {
|
|
54
|
-
/**
|
|
55
|
-
* Filters out excluded technologies from a list.
|
|
56
|
-
* @param technologies - List of technologies to filter
|
|
57
|
-
* @returns Filtered list without excluded technologies
|
|
58
|
-
*/
|
|
59
|
-
private filterExcludedTechnologies(technologies: Technology[]): Technology[] {
|
|
60
|
-
return technologies.filter(tech => tech.id !== EXCLUDED_TECHNOLOGY_ID);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Reference to the Firestore collection of technologies.
|
|
64
|
-
*/
|
|
65
|
-
private get technologiesRef() {
|
|
66
|
-
return collection(this.db, TECHNOLOGIES_COLLECTION);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Creates a new technology.
|
|
71
|
-
* @param technology - Data for the new technology.
|
|
72
|
-
* @returns The created technology with its generated ID.
|
|
73
|
-
*/
|
|
74
|
-
async create(technology: Omit<Technology, 'id' | 'createdAt' | 'updatedAt'>) {
|
|
75
|
-
const now = new Date();
|
|
76
|
-
// Explicitly construct the object to ensure no undefined values are passed.
|
|
77
|
-
const newTechnology: Omit<Technology, 'id'> = {
|
|
78
|
-
name: technology.name,
|
|
79
|
-
description: technology.description,
|
|
80
|
-
family: technology.family,
|
|
81
|
-
categoryId: technology.categoryId,
|
|
82
|
-
subcategoryId: technology.subcategoryId,
|
|
83
|
-
requirements: technology.requirements || { pre: [], post: [] },
|
|
84
|
-
blockingConditions: technology.blockingConditions || [],
|
|
85
|
-
contraindications: technology.contraindications || [],
|
|
86
|
-
benefits: technology.benefits || [],
|
|
87
|
-
certificationRequirement:
|
|
88
|
-
technology.certificationRequirement || DEFAULT_CERTIFICATION_REQUIREMENT,
|
|
89
|
-
documentationTemplates: technology.documentationTemplates || [],
|
|
90
|
-
isActive: true,
|
|
91
|
-
createdAt: now,
|
|
92
|
-
updatedAt: now,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Add optional fields only if they are not undefined
|
|
96
|
-
if (technology.technicalDetails) {
|
|
97
|
-
newTechnology.technicalDetails = technology.technicalDetails;
|
|
98
|
-
}
|
|
99
|
-
if (technology.photoTemplate) {
|
|
100
|
-
newTechnology.photoTemplate = technology.photoTemplate;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const docRef = await addDoc(this.technologiesRef, newTechnology as any);
|
|
104
|
-
return { id: docRef.id, ...newTechnology };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Returns counts of technologies for each subcategory.
|
|
109
|
-
* @param active - Whether to count active or inactive technologies.
|
|
110
|
-
* @returns A record mapping subcategory ID to technology count.
|
|
111
|
-
*/
|
|
112
|
-
async getTechnologyCounts(active = true) {
|
|
113
|
-
const q = query(this.technologiesRef, where('isActive', '==', active));
|
|
114
|
-
const snapshot = await getDocs(q);
|
|
115
|
-
const counts: Record<string, number> = {};
|
|
116
|
-
snapshot.docs.forEach(doc => {
|
|
117
|
-
// Exclude free-consultation-tech from counts
|
|
118
|
-
if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
|
|
119
|
-
const tech = doc.data() as Technology;
|
|
120
|
-
counts[tech.subcategoryId] = (counts[tech.subcategoryId] || 0) + 1;
|
|
121
|
-
});
|
|
122
|
-
return counts;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Returns counts of technologies for each category.
|
|
127
|
-
* @param active - Whether to count active or inactive technologies.
|
|
128
|
-
* @returns A record mapping category ID to technology count.
|
|
129
|
-
*/
|
|
130
|
-
async getTechnologyCountsByCategory(active = true) {
|
|
131
|
-
const q = query(this.technologiesRef, where('isActive', '==', active));
|
|
132
|
-
const snapshot = await getDocs(q);
|
|
133
|
-
const counts: Record<string, number> = {};
|
|
134
|
-
snapshot.docs.forEach(doc => {
|
|
135
|
-
// Exclude free-consultation-tech from counts
|
|
136
|
-
if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
|
|
137
|
-
const tech = doc.data() as Technology;
|
|
138
|
-
counts[tech.categoryId] = (counts[tech.categoryId] || 0) + 1;
|
|
139
|
-
});
|
|
140
|
-
return counts;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Returns all technologies with pagination.
|
|
145
|
-
* @param options - Pagination and filter options.
|
|
146
|
-
* @returns A list of technologies and the last visible document.
|
|
147
|
-
*/
|
|
148
|
-
async getAll(
|
|
149
|
-
options: {
|
|
150
|
-
active?: boolean;
|
|
151
|
-
limit?: number;
|
|
152
|
-
lastVisible?: DocumentData;
|
|
153
|
-
} = {},
|
|
154
|
-
) {
|
|
155
|
-
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
156
|
-
const constraints = [
|
|
157
|
-
where('isActive', '==', active),
|
|
158
|
-
orderBy('name'),
|
|
159
|
-
queryLimit ? limit(queryLimit) : undefined,
|
|
160
|
-
lastVisible ? startAfter(lastVisible) : undefined,
|
|
161
|
-
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
162
|
-
|
|
163
|
-
const q = query(this.technologiesRef, ...constraints);
|
|
164
|
-
const snapshot = await getDocs(q);
|
|
165
|
-
const technologies = snapshot.docs.map(
|
|
166
|
-
doc =>
|
|
167
|
-
({
|
|
168
|
-
id: doc.id,
|
|
169
|
-
...doc.data(),
|
|
170
|
-
} as Technology),
|
|
171
|
-
);
|
|
172
|
-
const filteredTechnologies = this.filterExcludedTechnologies(technologies);
|
|
173
|
-
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
174
|
-
return { technologies: filteredTechnologies, lastVisible: newLastVisible };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Returns all technologies for a specific category with pagination.
|
|
179
|
-
* @param categoryId - The ID of the category.
|
|
180
|
-
* @param options - Pagination options.
|
|
181
|
-
* @returns A list of technologies for the specified category.
|
|
182
|
-
*/
|
|
183
|
-
async getAllByCategoryId(
|
|
184
|
-
categoryId: string,
|
|
185
|
-
options: {
|
|
186
|
-
active?: boolean;
|
|
187
|
-
limit?: number;
|
|
188
|
-
lastVisible?: DocumentData;
|
|
189
|
-
} = {},
|
|
190
|
-
) {
|
|
191
|
-
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
192
|
-
const constraints = [
|
|
193
|
-
where('categoryId', '==', categoryId),
|
|
194
|
-
where('isActive', '==', active),
|
|
195
|
-
orderBy('name'),
|
|
196
|
-
queryLimit ? limit(queryLimit) : undefined,
|
|
197
|
-
lastVisible ? startAfter(lastVisible) : undefined,
|
|
198
|
-
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
199
|
-
|
|
200
|
-
const q = query(this.technologiesRef, ...constraints);
|
|
201
|
-
const snapshot = await getDocs(q);
|
|
202
|
-
const technologies = snapshot.docs.map(
|
|
203
|
-
doc =>
|
|
204
|
-
({
|
|
205
|
-
id: doc.id,
|
|
206
|
-
...doc.data(),
|
|
207
|
-
} as Technology),
|
|
208
|
-
);
|
|
209
|
-
const filteredTechnologies = this.filterExcludedTechnologies(technologies);
|
|
210
|
-
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
211
|
-
return { technologies: filteredTechnologies, lastVisible: newLastVisible };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Returns all technologies for a specific subcategory with pagination.
|
|
216
|
-
* @param subcategoryId - The ID of the subcategory.
|
|
217
|
-
* @param options - Pagination options.
|
|
218
|
-
* @returns A list of technologies for the specified subcategory.
|
|
219
|
-
*/
|
|
220
|
-
async getAllBySubcategoryId(
|
|
221
|
-
subcategoryId: string,
|
|
222
|
-
options: {
|
|
223
|
-
active?: boolean;
|
|
224
|
-
limit?: number;
|
|
225
|
-
lastVisible?: DocumentData;
|
|
226
|
-
} = {},
|
|
227
|
-
) {
|
|
228
|
-
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
229
|
-
const constraints = [
|
|
230
|
-
where('subcategoryId', '==', subcategoryId),
|
|
231
|
-
where('isActive', '==', active),
|
|
232
|
-
orderBy('name'),
|
|
233
|
-
queryLimit ? limit(queryLimit) : undefined,
|
|
234
|
-
lastVisible ? startAfter(lastVisible) : undefined,
|
|
235
|
-
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
236
|
-
|
|
237
|
-
const q = query(this.technologiesRef, ...constraints);
|
|
238
|
-
const snapshot = await getDocs(q);
|
|
239
|
-
const technologies = snapshot.docs.map(
|
|
240
|
-
doc =>
|
|
241
|
-
({
|
|
242
|
-
id: doc.id,
|
|
243
|
-
...doc.data(),
|
|
244
|
-
} as Technology),
|
|
245
|
-
);
|
|
246
|
-
const filteredTechnologies = this.filterExcludedTechnologies(technologies);
|
|
247
|
-
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
248
|
-
return { technologies: filteredTechnologies, lastVisible: newLastVisible };
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Updates an existing technology.
|
|
253
|
-
* @param id - The ID of the technology to update.
|
|
254
|
-
* @param technology - New data for the technology.
|
|
255
|
-
* @returns The updated technology.
|
|
256
|
-
*/
|
|
257
|
-
async update(id: string, technology: Partial<Omit<Technology, 'id' | 'createdAt'>>) {
|
|
258
|
-
const updateData: { [key: string]: any } = { ...technology };
|
|
259
|
-
|
|
260
|
-
// Remove undefined fields to prevent Firestore errors
|
|
261
|
-
Object.keys(updateData).forEach(key => {
|
|
262
|
-
if (updateData[key] === undefined) {
|
|
263
|
-
delete updateData[key];
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Handle photoTemplate: if explicitly set to null or empty string, allow it to be cleared
|
|
268
|
-
// If undefined, don't include it in the update (field won't change)
|
|
269
|
-
if ('photoTemplate' in technology) {
|
|
270
|
-
if (technology.photoTemplate === null || technology.photoTemplate === '') {
|
|
271
|
-
updateData.photoTemplate = null;
|
|
272
|
-
} else if (technology.photoTemplate !== undefined) {
|
|
273
|
-
updateData.photoTemplate = technology.photoTemplate;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
updateData.updatedAt = new Date();
|
|
278
|
-
|
|
279
|
-
const docRef = doc(this.technologiesRef, id);
|
|
280
|
-
|
|
281
|
-
// Get the technology before update to check what changed
|
|
282
|
-
const beforeTech = await this.getById(id);
|
|
283
|
-
|
|
284
|
-
await updateDoc(docRef, updateData);
|
|
285
|
-
|
|
286
|
-
// If categoryId, subcategoryId, or name changed, update all products in subcollection
|
|
287
|
-
const categoryChanged = beforeTech && updateData.categoryId && beforeTech.categoryId !== updateData.categoryId;
|
|
288
|
-
const subcategoryChanged = beforeTech && updateData.subcategoryId && beforeTech.subcategoryId !== updateData.subcategoryId;
|
|
289
|
-
const nameChanged = beforeTech && updateData.name && beforeTech.name !== updateData.name;
|
|
290
|
-
|
|
291
|
-
if (categoryChanged || subcategoryChanged || nameChanged) {
|
|
292
|
-
await this.updateProductsInSubcollection(id, {
|
|
293
|
-
categoryId: updateData.categoryId,
|
|
294
|
-
subcategoryId: updateData.subcategoryId,
|
|
295
|
-
technologyName: updateData.name,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return this.getById(id);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Soft deletes a technology.
|
|
304
|
-
* @param id - The ID of the technology to delete.
|
|
305
|
-
*/
|
|
306
|
-
async delete(id: string) {
|
|
307
|
-
await this.update(id, { isActive: false });
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Reactivates a technology.
|
|
312
|
-
* @param id - The ID of the technology to reactivate.
|
|
313
|
-
*/
|
|
314
|
-
async reactivate(id: string) {
|
|
315
|
-
await this.update(id, { isActive: true });
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Returns a technology by its ID.
|
|
320
|
-
* @param id - The ID of the requested technology.
|
|
321
|
-
* @returns The technology or null if it doesn't exist.
|
|
322
|
-
*/
|
|
323
|
-
async getById(id: string): Promise<Technology | null> {
|
|
324
|
-
// Prevent access to excluded technology
|
|
325
|
-
if (id === EXCLUDED_TECHNOLOGY_ID) return null;
|
|
326
|
-
|
|
327
|
-
const docRef = doc(this.technologiesRef, id);
|
|
328
|
-
const docSnap = await getDoc(docRef);
|
|
329
|
-
if (!docSnap.exists()) return null;
|
|
330
|
-
return {
|
|
331
|
-
id: docSnap.id,
|
|
332
|
-
...docSnap.data(),
|
|
333
|
-
} as Technology;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Internal method to get technology by ID without filtering.
|
|
338
|
-
* Used internally for consultation procedures.
|
|
339
|
-
* @param id - The ID of the requested technology
|
|
340
|
-
* @returns The technology or null if it doesn't exist
|
|
341
|
-
*/
|
|
342
|
-
async getByIdInternal(id: string): Promise<Technology | null> {
|
|
343
|
-
const docRef = doc(this.technologiesRef, id);
|
|
344
|
-
const docSnap = await getDoc(docRef);
|
|
345
|
-
if (!docSnap.exists()) return null;
|
|
346
|
-
return {
|
|
347
|
-
id: docSnap.id,
|
|
348
|
-
...docSnap.data(),
|
|
349
|
-
} as Technology;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Finds a technology by exact name match.
|
|
354
|
-
* Used for CSV import duplicate detection.
|
|
355
|
-
* @param name - Exact name of the technology to find
|
|
356
|
-
* @returns Technology if found, null otherwise
|
|
357
|
-
*/
|
|
358
|
-
async findByName(name: string): Promise<Technology | null> {
|
|
359
|
-
const q = query(
|
|
360
|
-
this.technologiesRef,
|
|
361
|
-
where('name', '==', name),
|
|
362
|
-
where('isActive', '==', true),
|
|
363
|
-
);
|
|
364
|
-
const snapshot = await getDocs(q);
|
|
365
|
-
if (snapshot.empty) return null;
|
|
366
|
-
const doc = snapshot.docs[0];
|
|
367
|
-
// Exclude free-consultation-tech
|
|
368
|
-
if (doc.id === EXCLUDED_TECHNOLOGY_ID) return null;
|
|
369
|
-
return {
|
|
370
|
-
id: doc.id,
|
|
371
|
-
...doc.data(),
|
|
372
|
-
} as Technology;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Dodaje novi zahtev tehnologiji
|
|
377
|
-
* @param technologyId - ID tehnologije
|
|
378
|
-
* @param requirement - Zahtev koji se dodaje
|
|
379
|
-
* @returns Ažurirana tehnologija sa novim zahtevom
|
|
380
|
-
*/
|
|
381
|
-
async addRequirement(technologyId: string, requirement: Requirement) {
|
|
382
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
383
|
-
|
|
384
|
-
const requirementType = requirement.type === 'pre' ? 'requirements.pre' : 'requirements.post';
|
|
385
|
-
|
|
386
|
-
await updateDoc(docRef, {
|
|
387
|
-
[requirementType]: arrayUnion(requirement),
|
|
388
|
-
updatedAt: new Date(),
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
return this.getById(technologyId);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Uklanja zahtev iz tehnologije
|
|
396
|
-
* @param technologyId - ID tehnologije
|
|
397
|
-
* @param requirement - Zahtev koji se uklanja
|
|
398
|
-
* @returns Ažurirana tehnologija bez uklonjenog zahteva
|
|
399
|
-
*/
|
|
400
|
-
async removeRequirement(technologyId: string, requirement: Requirement) {
|
|
401
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
402
|
-
|
|
403
|
-
const requirementType = requirement.type === 'pre' ? 'requirements.pre' : 'requirements.post';
|
|
404
|
-
|
|
405
|
-
await updateDoc(docRef, {
|
|
406
|
-
[requirementType]: arrayRemove(requirement),
|
|
407
|
-
updatedAt: new Date(),
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
return this.getById(technologyId);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Vraća sve zahteve za tehnologiju
|
|
415
|
-
* @param technologyId - ID tehnologije
|
|
416
|
-
* @param type - Opcioni filter za tip zahteva (pre/post)
|
|
417
|
-
* @returns Lista zahteva
|
|
418
|
-
*/
|
|
419
|
-
async getRequirements(technologyId: string, type?: RequirementType) {
|
|
420
|
-
const technology = await this.getById(technologyId);
|
|
421
|
-
if (!technology || !technology.requirements) return [];
|
|
422
|
-
|
|
423
|
-
if (type) {
|
|
424
|
-
return technology.requirements[type];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return [...technology.requirements.pre, ...technology.requirements.post];
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Ažurira postojeći zahtev
|
|
432
|
-
* @param technologyId - ID tehnologije
|
|
433
|
-
* @param oldRequirement - Stari zahtev koji se menja
|
|
434
|
-
* @param newRequirement - Novi zahtev koji zamenjuje stari
|
|
435
|
-
* @returns Ažurirana tehnologija
|
|
436
|
-
*/
|
|
437
|
-
async updateRequirement(
|
|
438
|
-
technologyId: string,
|
|
439
|
-
oldRequirement: Requirement,
|
|
440
|
-
newRequirement: Requirement,
|
|
441
|
-
) {
|
|
442
|
-
await this.removeRequirement(technologyId, oldRequirement);
|
|
443
|
-
return this.addRequirement(technologyId, newRequirement);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
/**
|
|
447
|
-
* Dodaje blokirajući uslov tehnologiji
|
|
448
|
-
* @param technologyId - ID tehnologije
|
|
449
|
-
* @param condition - Blokirajući uslov koji se dodaje
|
|
450
|
-
* @returns Ažurirana tehnologija
|
|
451
|
-
*/
|
|
452
|
-
async addBlockingCondition(technologyId: string, condition: BlockingCondition) {
|
|
453
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
454
|
-
|
|
455
|
-
await updateDoc(docRef, {
|
|
456
|
-
blockingConditions: arrayUnion(condition),
|
|
457
|
-
updatedAt: new Date(),
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
return this.getById(technologyId);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Uklanja blokirajući uslov iz tehnologije
|
|
465
|
-
* @param technologyId - ID tehnologije
|
|
466
|
-
* @param condition - Blokirajući uslov koji se uklanja
|
|
467
|
-
* @returns Ažurirana tehnologija
|
|
468
|
-
*/
|
|
469
|
-
async removeBlockingCondition(technologyId: string, condition: BlockingCondition) {
|
|
470
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
471
|
-
|
|
472
|
-
await updateDoc(docRef, {
|
|
473
|
-
blockingConditions: arrayRemove(condition),
|
|
474
|
-
updatedAt: new Date(),
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
return this.getById(technologyId);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Dodaje kontraindikaciju tehnologiji
|
|
482
|
-
* @param technologyId - ID tehnologije
|
|
483
|
-
* @param contraindication - Kontraindikacija koja se dodaje
|
|
484
|
-
* @returns Ažurirana tehnologija
|
|
485
|
-
*/
|
|
486
|
-
async addContraindication(technologyId: string, contraindication: ContraindicationDynamic) {
|
|
487
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
488
|
-
const technology = await this.getById(technologyId);
|
|
489
|
-
if (!technology) {
|
|
490
|
-
throw new Error(`Technology with id ${technologyId} not found`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const existingContraindications = technology.contraindications || [];
|
|
494
|
-
if (existingContraindications.some(c => c.id === contraindication.id)) {
|
|
495
|
-
return technology; // Already exists, do nothing
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
await updateDoc(docRef, {
|
|
499
|
-
contraindications: [...existingContraindications, contraindication],
|
|
500
|
-
updatedAt: new Date(),
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
return this.getById(technologyId);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Uklanja kontraindikaciju iz tehnologije
|
|
508
|
-
* @param technologyId - ID tehnologije
|
|
509
|
-
* @param contraindication - Kontraindikacija koja se uklanja
|
|
510
|
-
* @returns Ažurirana tehnologija
|
|
511
|
-
*/
|
|
512
|
-
async removeContraindication(technologyId: string, contraindication: ContraindicationDynamic) {
|
|
513
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
514
|
-
const technology = await this.getById(technologyId);
|
|
515
|
-
if (!technology) {
|
|
516
|
-
throw new Error(`Technology with id ${technologyId} not found`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const updatedContraindications = (technology.contraindications || []).filter(
|
|
520
|
-
c => c.id !== contraindication.id,
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
await updateDoc(docRef, {
|
|
524
|
-
contraindications: updatedContraindications,
|
|
525
|
-
updatedAt: new Date(),
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
return this.getById(technologyId);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Updates an existing contraindication in a technology's list.
|
|
533
|
-
* If the contraindication does not exist, it will not be added.
|
|
534
|
-
* @param technologyId - ID of the technology
|
|
535
|
-
* @param contraindication - The updated contraindication object
|
|
536
|
-
* @returns The updated technology
|
|
537
|
-
*/
|
|
538
|
-
async updateContraindication(technologyId: string, contraindication: ContraindicationDynamic) {
|
|
539
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
540
|
-
const technology = await this.getById(technologyId);
|
|
541
|
-
if (!technology) {
|
|
542
|
-
throw new Error(`Technology with id ${technologyId} not found`);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const contraindications = technology.contraindications || [];
|
|
546
|
-
const index = contraindications.findIndex(c => c.id === contraindication.id);
|
|
547
|
-
|
|
548
|
-
if (index === -1) {
|
|
549
|
-
// If contraindication doesn't exist, do not update
|
|
550
|
-
// Consider throwing an error if this is an unexpected state
|
|
551
|
-
console.warn(
|
|
552
|
-
`Contraindication with id ${contraindication.id} not found for technology ${technologyId}. No update performed.`,
|
|
553
|
-
);
|
|
554
|
-
return technology;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const updatedContraindications = [...contraindications];
|
|
558
|
-
updatedContraindications[index] = contraindication;
|
|
559
|
-
|
|
560
|
-
await updateDoc(docRef, {
|
|
561
|
-
contraindications: updatedContraindications,
|
|
562
|
-
updatedAt: new Date(),
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
return this.getById(technologyId);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Dodaje benefit tehnologiji
|
|
570
|
-
* @param technologyId - ID tehnologije
|
|
571
|
-
* @param benefit - Benefit koji se dodaje
|
|
572
|
-
* @returns Ažurirana tehnologija
|
|
573
|
-
*/
|
|
574
|
-
async addBenefit(technologyId: string, benefit: TreatmentBenefitDynamic) {
|
|
575
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
576
|
-
const technology = await this.getById(technologyId);
|
|
577
|
-
if (!technology) {
|
|
578
|
-
throw new Error(`Technology with id ${technologyId} not found`);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const existingBenefits = technology.benefits || [];
|
|
582
|
-
if (existingBenefits.some(b => b.id === benefit.id)) {
|
|
583
|
-
return technology; // Already exists, do nothing
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
await updateDoc(docRef, {
|
|
587
|
-
benefits: [...existingBenefits, benefit],
|
|
588
|
-
updatedAt: new Date(),
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
return this.getById(technologyId);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* Uklanja benefit iz tehnologije
|
|
596
|
-
* @param technologyId - ID tehnologije
|
|
597
|
-
* @param benefit - Benefit koji se uklanja
|
|
598
|
-
* @returns Ažurirana tehnologija
|
|
599
|
-
*/
|
|
600
|
-
async removeBenefit(technologyId: string, benefit: TreatmentBenefitDynamic) {
|
|
601
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
602
|
-
const technology = await this.getById(technologyId);
|
|
603
|
-
if (!technology) {
|
|
604
|
-
throw new Error(`Technology with id ${technologyId} not found`);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
const updatedBenefits = (technology.benefits || []).filter(b => b.id !== benefit.id);
|
|
608
|
-
|
|
609
|
-
await updateDoc(docRef, {
|
|
610
|
-
benefits: updatedBenefits,
|
|
611
|
-
updatedAt: new Date(),
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
return this.getById(technologyId);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Updates an existing benefit in a technology's list.
|
|
619
|
-
* If the benefit does not exist, it will not be added.
|
|
620
|
-
* @param technologyId - ID of the technology
|
|
621
|
-
* @param benefit - The updated benefit object
|
|
622
|
-
* @returns The updated technology
|
|
623
|
-
*/
|
|
624
|
-
async updateBenefit(technologyId: string, benefit: TreatmentBenefitDynamic) {
|
|
625
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
626
|
-
const technology = await this.getById(technologyId);
|
|
627
|
-
if (!technology) {
|
|
628
|
-
throw new Error(`Technology with id ${technologyId} not found`);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const benefits = technology.benefits || [];
|
|
632
|
-
const index = benefits.findIndex(b => b.id === benefit.id);
|
|
633
|
-
|
|
634
|
-
if (index === -1) {
|
|
635
|
-
// If benefit doesn't exist, do not update
|
|
636
|
-
console.warn(
|
|
637
|
-
`Benefit with id ${benefit.id} not found for technology ${technologyId}. No update performed.`,
|
|
638
|
-
);
|
|
639
|
-
return technology;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const updatedBenefits = [...benefits];
|
|
643
|
-
updatedBenefits[index] = benefit;
|
|
644
|
-
|
|
645
|
-
await updateDoc(docRef, {
|
|
646
|
-
benefits: updatedBenefits,
|
|
647
|
-
updatedAt: new Date(),
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
return this.getById(technologyId);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Vraća sve blokirajuće uslove za tehnologiju
|
|
655
|
-
* @param technologyId - ID tehnologije
|
|
656
|
-
* @returns Lista blokirajućih uslova
|
|
657
|
-
*/
|
|
658
|
-
async getBlockingConditions(technologyId: string) {
|
|
659
|
-
const technology = await this.getById(technologyId);
|
|
660
|
-
return technology?.blockingConditions || [];
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Vraća sve kontraindikacije za tehnologiju
|
|
665
|
-
* @param technologyId - ID tehnologije
|
|
666
|
-
* @returns Lista kontraindikacija
|
|
667
|
-
*/
|
|
668
|
-
async getContraindications(technologyId: string) {
|
|
669
|
-
const technology = await this.getById(technologyId);
|
|
670
|
-
return technology?.contraindications || [];
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Vraća sve benefite za tehnologiju
|
|
675
|
-
* @param technologyId - ID tehnologije
|
|
676
|
-
* @returns Lista benefita
|
|
677
|
-
*/
|
|
678
|
-
async getBenefits(technologyId: string) {
|
|
679
|
-
const technology = await this.getById(technologyId);
|
|
680
|
-
return technology?.benefits || [];
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Ažurira zahteve sertifikacije za tehnologiju
|
|
685
|
-
* @param technologyId - ID tehnologije
|
|
686
|
-
* @param certificationRequirement - Novi zahtevi sertifikacije
|
|
687
|
-
* @returns Ažurirana tehnologija
|
|
688
|
-
*/
|
|
689
|
-
async updateCertificationRequirement(
|
|
690
|
-
technologyId: string,
|
|
691
|
-
certificationRequirement: CertificationRequirement,
|
|
692
|
-
) {
|
|
693
|
-
const docRef = doc(this.technologiesRef, technologyId);
|
|
694
|
-
|
|
695
|
-
await updateDoc(docRef, {
|
|
696
|
-
certificationRequirement,
|
|
697
|
-
updatedAt: new Date(),
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
return this.getById(technologyId);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Vraća zahteve sertifikacije za tehnologiju
|
|
705
|
-
* @param technologyId - ID tehnologije
|
|
706
|
-
* @returns Zahtevi sertifikacije ili null ako tehnologija ne postoji
|
|
707
|
-
*/
|
|
708
|
-
async getCertificationRequirement(
|
|
709
|
-
technologyId: string,
|
|
710
|
-
): Promise<CertificationRequirement | null> {
|
|
711
|
-
const technology = await this.getById(technologyId);
|
|
712
|
-
return technology?.certificationRequirement || null;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Proverava da li doktor ima odgovarajuću sertifikaciju za izvođenje tehnologije
|
|
717
|
-
*
|
|
718
|
-
* @param requiredCertification - Zahtevana sertifikacija za tehnologiju
|
|
719
|
-
* @param practitionerCertification - Sertifikacija zdravstvenog radnika
|
|
720
|
-
* @returns true ako zdravstveni radnik ima odgovarajuću sertifikaciju, false ako nema
|
|
721
|
-
*
|
|
722
|
-
* @example
|
|
723
|
-
* const isValid = technologyService.validateCertification(
|
|
724
|
-
* {
|
|
725
|
-
* minimumLevel: CertificationLevel.DOCTOR,
|
|
726
|
-
* requiredSpecialties: [CertificationSpecialty.INJECTABLES]
|
|
727
|
-
* },
|
|
728
|
-
* {
|
|
729
|
-
* level: CertificationLevel.SPECIALIST,
|
|
730
|
-
* specialties: [CertificationSpecialty.INJECTABLES, CertificationSpecialty.LASER]
|
|
731
|
-
* }
|
|
732
|
-
* );
|
|
733
|
-
*/
|
|
734
|
-
validateCertification(
|
|
735
|
-
requiredCertification: CertificationRequirement,
|
|
736
|
-
practitionerCertification: PractitionerCertification,
|
|
737
|
-
): boolean {
|
|
738
|
-
// Provera nivoa sertifikacije
|
|
739
|
-
// Enum je definisan od najnižeg ka najvišem, pa možemo porediti brojeve
|
|
740
|
-
const doctorLevel = Object.values(CertificationLevel).indexOf(practitionerCertification.level);
|
|
741
|
-
const requiredLevel = Object.values(CertificationLevel).indexOf(
|
|
742
|
-
requiredCertification.minimumLevel,
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
// Doktor mora imati nivo koji je jednak ili viši od zahtevanog
|
|
746
|
-
if (doctorLevel < requiredLevel) return false;
|
|
747
|
-
|
|
748
|
-
// Provera specijalizacija
|
|
749
|
-
const requiredSpecialties = requiredCertification.requiredSpecialties || [];
|
|
750
|
-
if (requiredSpecialties.length > 0) {
|
|
751
|
-
// Doktor mora imati sve zahtevane specijalizacije
|
|
752
|
-
const doctorSpecialties = practitionerCertification.specialties;
|
|
753
|
-
const hasAllRequiredSpecialties = requiredSpecialties.every(requiredSpecialty =>
|
|
754
|
-
doctorSpecialties.includes(requiredSpecialty),
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
if (!hasAllRequiredSpecialties) return false;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return true;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Vraća sve tehnologije koje je zdravstveni radnik sertifikovan da izvodi
|
|
765
|
-
* zajedno sa listama dozvoljenih familija, kategorija i podkategorija
|
|
766
|
-
*
|
|
767
|
-
* @param practitioner - Profil zdravstvenog radnika
|
|
768
|
-
* @returns Objekat koji sadrži:
|
|
769
|
-
* - technologies: Lista tehnologija koje zdravstveni radnik može da izvodi
|
|
770
|
-
* - families: Lista familija procedura koje zdravstveni radnik može da izvodi
|
|
771
|
-
* - categories: Lista ID-eva kategorija koje zdravstveni radnik može da izvodi
|
|
772
|
-
* - subcategories: Lista ID-eva podkategorija koje zdravstveni radnik može da izvodi
|
|
773
|
-
*
|
|
774
|
-
* @example
|
|
775
|
-
* const practitioner = {
|
|
776
|
-
* certification: {
|
|
777
|
-
* level: CertificationLevel.DOCTOR,
|
|
778
|
-
* specialties: [CertificationSpecialty.INJECTABLES]
|
|
779
|
-
* }
|
|
780
|
-
* };
|
|
781
|
-
* const allowedTechnologies = await technologyService.getAllowedTechnologies(practitioner);
|
|
782
|
-
* console.log(allowedTechnologies.families); // [ProcedureFamily.AESTHETICS]
|
|
783
|
-
* console.log(allowedTechnologies.categories); // ["category1", "category2"]
|
|
784
|
-
* console.log(allowedTechnologies.subcategories); // ["subcategory1", "subcategory2"]
|
|
785
|
-
*/
|
|
786
|
-
async getAllowedTechnologies(practitioner: Practitioner): Promise<{
|
|
787
|
-
technologies: Technology[];
|
|
788
|
-
families: ProcedureFamily[];
|
|
789
|
-
categories: string[];
|
|
790
|
-
subcategories: string[];
|
|
791
|
-
}> {
|
|
792
|
-
// Get all active technologies
|
|
793
|
-
const allTechnologies = await this.getAllForFilter();
|
|
794
|
-
|
|
795
|
-
// Filter technologies based on certification requirements
|
|
796
|
-
const allowedTechnologies = allTechnologies.filter(technology =>
|
|
797
|
-
this.validateCertification(technology.certificationRequirement, practitioner.certification),
|
|
798
|
-
);
|
|
799
|
-
|
|
800
|
-
// Extract unique families, categories, and subcategories
|
|
801
|
-
const families = [...new Set(allowedTechnologies.map(t => t.family))];
|
|
802
|
-
const categories = [...new Set(allowedTechnologies.map(t => t.categoryId))];
|
|
803
|
-
const subcategories = [...new Set(allowedTechnologies.map(t => t.subcategoryId))];
|
|
804
|
-
|
|
805
|
-
return {
|
|
806
|
-
technologies: allowedTechnologies,
|
|
807
|
-
families,
|
|
808
|
-
categories,
|
|
809
|
-
subcategories,
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* Gets all active technologies for a subcategory for filter dropdowns (by subcategory only).
|
|
815
|
-
* @param subcategoryId - The ID of the subcategory.
|
|
816
|
-
*/
|
|
817
|
-
async getAllForFilterBySubcategory(subcategoryId: string): Promise<Technology[]> {
|
|
818
|
-
const q = query(
|
|
819
|
-
collection(this.db, TECHNOLOGIES_COLLECTION),
|
|
820
|
-
where('isActive', '==', true),
|
|
821
|
-
where('subcategoryId', '==', subcategoryId),
|
|
822
|
-
orderBy('name'),
|
|
823
|
-
);
|
|
824
|
-
const snapshot = await getDocs(q);
|
|
825
|
-
const technologies = snapshot.docs.map(
|
|
826
|
-
doc =>
|
|
827
|
-
({
|
|
828
|
-
id: doc.id,
|
|
829
|
-
...doc.data(),
|
|
830
|
-
} as Technology),
|
|
831
|
-
);
|
|
832
|
-
return this.filterExcludedTechnologies(technologies);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Gets all active technologies for a subcategory for filter dropdowns.
|
|
837
|
-
* @param categoryId - The ID of the parent category.
|
|
838
|
-
* @param subcategoryId - The ID of the subcategory.
|
|
839
|
-
*/
|
|
840
|
-
async getAllForFilterBySubcategoryId(
|
|
841
|
-
categoryId: string,
|
|
842
|
-
subcategoryId: string,
|
|
843
|
-
): Promise<Technology[]> {
|
|
844
|
-
const q = query(
|
|
845
|
-
collection(this.db, TECHNOLOGIES_COLLECTION),
|
|
846
|
-
where('isActive', '==', true),
|
|
847
|
-
where('categoryId', '==', categoryId),
|
|
848
|
-
where('subcategoryId', '==', subcategoryId),
|
|
849
|
-
orderBy('name'),
|
|
850
|
-
);
|
|
851
|
-
const snapshot = await getDocs(q);
|
|
852
|
-
const technologies = snapshot.docs.map(
|
|
853
|
-
doc =>
|
|
854
|
-
({
|
|
855
|
-
id: doc.id,
|
|
856
|
-
...doc.data(),
|
|
857
|
-
} as Technology),
|
|
858
|
-
);
|
|
859
|
-
return this.filterExcludedTechnologies(technologies);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Gets all active technologies for filter dropdowns.
|
|
864
|
-
*/
|
|
865
|
-
async getAllForFilter(): Promise<Technology[]> {
|
|
866
|
-
const q = query(
|
|
867
|
-
collection(this.db, TECHNOLOGIES_COLLECTION),
|
|
868
|
-
where('isActive', '==', true),
|
|
869
|
-
orderBy('name'),
|
|
870
|
-
);
|
|
871
|
-
const snapshot = await getDocs(q);
|
|
872
|
-
const technologies = snapshot.docs.map(
|
|
873
|
-
doc =>
|
|
874
|
-
({
|
|
875
|
-
id: doc.id,
|
|
876
|
-
...doc.data(),
|
|
877
|
-
} as Technology),
|
|
878
|
-
);
|
|
879
|
-
return this.filterExcludedTechnologies(technologies);
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// ==========================================
|
|
883
|
-
// NEW METHODS: Product assignment management
|
|
884
|
-
// ==========================================
|
|
885
|
-
|
|
886
|
-
/**
|
|
887
|
-
* Assigns multiple products to a technology
|
|
888
|
-
* Updates each product's assignedTechnologyIds array
|
|
889
|
-
*/
|
|
890
|
-
async assignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
891
|
-
const batch = writeBatch(this.db);
|
|
892
|
-
|
|
893
|
-
for (const productId of productIds) {
|
|
894
|
-
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
895
|
-
batch.update(productRef, {
|
|
896
|
-
assignedTechnologyIds: arrayUnion(technologyId),
|
|
897
|
-
updatedAt: new Date(),
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
await batch.commit();
|
|
902
|
-
// Cloud Function will handle syncing to subcollections
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Unassigns multiple products from a technology
|
|
907
|
-
* Updates each product's assignedTechnologyIds array
|
|
908
|
-
*/
|
|
909
|
-
async unassignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
910
|
-
const batch = writeBatch(this.db);
|
|
911
|
-
|
|
912
|
-
for (const productId of productIds) {
|
|
913
|
-
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
914
|
-
batch.update(productRef, {
|
|
915
|
-
assignedTechnologyIds: arrayRemove(technologyId),
|
|
916
|
-
updatedAt: new Date(),
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
await batch.commit();
|
|
921
|
-
// Cloud Function will handle removing from subcollections
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Gets products assigned to a specific technology
|
|
926
|
-
* Reads from top-level collection for immediate consistency (Cloud Functions may lag)
|
|
927
|
-
*/
|
|
928
|
-
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
929
|
-
const q = query(
|
|
930
|
-
collection(this.db, PRODUCTS_COLLECTION),
|
|
931
|
-
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
932
|
-
where('isActive', '==', true),
|
|
933
|
-
orderBy('name'),
|
|
934
|
-
);
|
|
935
|
-
const snapshot = await getDocs(q);
|
|
936
|
-
|
|
937
|
-
return snapshot.docs.map(
|
|
938
|
-
doc =>
|
|
939
|
-
({
|
|
940
|
-
id: doc.id,
|
|
941
|
-
...doc.data(),
|
|
942
|
-
} as Product),
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Gets products NOT assigned to a specific technology
|
|
948
|
-
*/
|
|
949
|
-
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
950
|
-
const q = query(
|
|
951
|
-
collection(this.db, PRODUCTS_COLLECTION),
|
|
952
|
-
where('isActive', '==', true),
|
|
953
|
-
orderBy('name'),
|
|
954
|
-
);
|
|
955
|
-
const snapshot = await getDocs(q);
|
|
956
|
-
|
|
957
|
-
const allProducts = snapshot.docs.map(
|
|
958
|
-
doc =>
|
|
959
|
-
({
|
|
960
|
-
id: doc.id,
|
|
961
|
-
...doc.data(),
|
|
962
|
-
} as Product),
|
|
963
|
-
);
|
|
964
|
-
|
|
965
|
-
// Filter out products already assigned to this technology
|
|
966
|
-
return allProducts.filter(product =>
|
|
967
|
-
!product.assignedTechnologyIds?.includes(technologyId)
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Gets product assignment statistics for a technology
|
|
973
|
-
*/
|
|
974
|
-
async getProductStats(technologyId: string): Promise<{
|
|
975
|
-
totalAssigned: number;
|
|
976
|
-
byBrand: Record<string, number>;
|
|
977
|
-
}> {
|
|
978
|
-
const products = await this.getAssignedProducts(technologyId);
|
|
979
|
-
|
|
980
|
-
const byBrand: Record<string, number> = {};
|
|
981
|
-
products.forEach(product => {
|
|
982
|
-
byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
return {
|
|
986
|
-
totalAssigned: products.length,
|
|
987
|
-
byBrand,
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Updates products in technology subcollection when technology metadata changes
|
|
993
|
-
* @param technologyId - ID of the technology
|
|
994
|
-
* @param updates - Fields to update (categoryId, subcategoryId, technologyName)
|
|
995
|
-
*/
|
|
996
|
-
private async updateProductsInSubcollection(
|
|
997
|
-
technologyId: string,
|
|
998
|
-
updates: { categoryId?: string; subcategoryId?: string; technologyName?: string }
|
|
999
|
-
): Promise<void> {
|
|
1000
|
-
const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
1001
|
-
const productsSnapshot = await getDocs(productsRef);
|
|
1002
|
-
|
|
1003
|
-
if (productsSnapshot.empty) {
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const batch = writeBatch(this.db);
|
|
1008
|
-
|
|
1009
|
-
for (const productDoc of productsSnapshot.docs) {
|
|
1010
|
-
const productRef = productDoc.ref;
|
|
1011
|
-
const updateFields: any = {};
|
|
1012
|
-
|
|
1013
|
-
if (updates.categoryId !== undefined) {
|
|
1014
|
-
updateFields.categoryId = updates.categoryId;
|
|
1015
|
-
}
|
|
1016
|
-
if (updates.subcategoryId !== undefined) {
|
|
1017
|
-
updateFields.subcategoryId = updates.subcategoryId;
|
|
1018
|
-
}
|
|
1019
|
-
if (updates.technologyName !== undefined) {
|
|
1020
|
-
updateFields.technologyName = updates.technologyName;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
if (Object.keys(updateFields).length > 0) {
|
|
1024
|
-
batch.update(productRef, updateFields);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
await batch.commit();
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* Exports technologies to CSV string, suitable for Excel/Sheets.
|
|
1033
|
-
* Includes headers and optional UTF-8 BOM.
|
|
1034
|
-
* By default exports only active technologies (set includeInactive to true to export all).
|
|
1035
|
-
* Includes product names from subcollections.
|
|
1036
|
-
*/
|
|
1037
|
-
async exportToCsv(options?: {
|
|
1038
|
-
includeInactive?: boolean;
|
|
1039
|
-
includeBom?: boolean;
|
|
1040
|
-
}): Promise<string> {
|
|
1041
|
-
const includeInactive = options?.includeInactive ?? false;
|
|
1042
|
-
const includeBom = options?.includeBom ?? true;
|
|
1043
|
-
|
|
1044
|
-
const headers = [
|
|
1045
|
-
"id",
|
|
1046
|
-
"name",
|
|
1047
|
-
"description",
|
|
1048
|
-
"family",
|
|
1049
|
-
"categoryId",
|
|
1050
|
-
"subcategoryId",
|
|
1051
|
-
"technicalDetails",
|
|
1052
|
-
"requirements_pre",
|
|
1053
|
-
"requirements_post",
|
|
1054
|
-
"blockingConditions",
|
|
1055
|
-
"contraindications",
|
|
1056
|
-
"benefits",
|
|
1057
|
-
"certificationMinimumLevel",
|
|
1058
|
-
"certificationRequiredSpecialties",
|
|
1059
|
-
"documentationTemplateIds",
|
|
1060
|
-
"productNames",
|
|
1061
|
-
"isActive",
|
|
1062
|
-
];
|
|
1063
|
-
|
|
1064
|
-
const rows: string[] = [];
|
|
1065
|
-
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
1066
|
-
|
|
1067
|
-
const PAGE_SIZE = 1000;
|
|
1068
|
-
let cursor: any | undefined;
|
|
1069
|
-
|
|
1070
|
-
// Build base constraints
|
|
1071
|
-
const constraints: QueryConstraint[] = [];
|
|
1072
|
-
if (!includeInactive) {
|
|
1073
|
-
constraints.push(where("isActive", "==", true));
|
|
1074
|
-
}
|
|
1075
|
-
constraints.push(orderBy("name"));
|
|
1076
|
-
|
|
1077
|
-
// Page through all results
|
|
1078
|
-
// eslint-disable-next-line no-constant-condition
|
|
1079
|
-
while (true) {
|
|
1080
|
-
const queryConstraints: QueryConstraint[] = [...constraints, limit(PAGE_SIZE)];
|
|
1081
|
-
if (cursor) queryConstraints.push(startAfter(cursor));
|
|
1082
|
-
|
|
1083
|
-
const q = query(this.technologiesRef, ...queryConstraints);
|
|
1084
|
-
const snapshot = await getDocs(q);
|
|
1085
|
-
if (snapshot.empty) break;
|
|
1086
|
-
|
|
1087
|
-
for (const d of snapshot.docs) {
|
|
1088
|
-
// Exclude free-consultation-tech from CSV export
|
|
1089
|
-
if (d.id === EXCLUDED_TECHNOLOGY_ID) continue;
|
|
1090
|
-
const technology = ({ id: d.id, ...d.data() } as unknown) as Technology;
|
|
1091
|
-
// Fetch products for this technology
|
|
1092
|
-
const productNames = await this.getProductNamesForTechnology(technology.id!);
|
|
1093
|
-
rows.push(this.technologyToCsvRow(technology, productNames));
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
1097
|
-
if (snapshot.size < PAGE_SIZE) break;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
const csvBody = rows.join("\r\n");
|
|
1101
|
-
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
/**
|
|
1105
|
-
* Gets product names from the technology's product subcollection
|
|
1106
|
-
*/
|
|
1107
|
-
private async getProductNamesForTechnology(technologyId: string): Promise<string[]> {
|
|
1108
|
-
try {
|
|
1109
|
-
const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
1110
|
-
const q = query(productsRef, where("isActive", "==", true));
|
|
1111
|
-
const snapshot = await getDocs(q);
|
|
1112
|
-
return snapshot.docs.map(doc => {
|
|
1113
|
-
const product = doc.data() as Product;
|
|
1114
|
-
return product.name || "";
|
|
1115
|
-
}).filter(name => name); // Filter out empty names
|
|
1116
|
-
} catch (error) {
|
|
1117
|
-
console.error(`Error fetching products for technology ${technologyId}:`, error);
|
|
1118
|
-
return [];
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
private technologyToCsvRow(technology: Technology, productNames: string[] = []): string {
|
|
1123
|
-
const values = [
|
|
1124
|
-
technology.id ?? "",
|
|
1125
|
-
technology.name ?? "",
|
|
1126
|
-
technology.description ?? "",
|
|
1127
|
-
technology.family ?? "",
|
|
1128
|
-
technology.categoryId ?? "",
|
|
1129
|
-
technology.subcategoryId ?? "",
|
|
1130
|
-
technology.technicalDetails ?? "",
|
|
1131
|
-
technology.requirements?.pre?.map(r => r.name).join(";") ?? "",
|
|
1132
|
-
technology.requirements?.post?.map(r => r.name).join(";") ?? "",
|
|
1133
|
-
technology.blockingConditions?.join(";") ?? "",
|
|
1134
|
-
technology.contraindications?.map(c => c.name).join(";") ?? "",
|
|
1135
|
-
technology.benefits?.map(b => b.name).join(";") ?? "",
|
|
1136
|
-
technology.certificationRequirement?.minimumLevel ?? "",
|
|
1137
|
-
technology.certificationRequirement?.requiredSpecialties?.join(";") ?? "",
|
|
1138
|
-
technology.documentationTemplates?.map(t => t.templateId).join(";") ?? "",
|
|
1139
|
-
productNames.join(";"),
|
|
1140
|
-
String(technology.isActive ?? ""),
|
|
1141
|
-
];
|
|
1142
|
-
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
private formatCsvValue(value: any): string {
|
|
1146
|
-
const str = value === null || value === undefined ? "" : String(value);
|
|
1147
|
-
// Escape double quotes by doubling them and wrap in quotes
|
|
1148
|
-
const escaped = str.replace(/"/g, '""');
|
|
1149
|
-
return `"${escaped}"`;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
addDoc,
|
|
3
|
+
collection,
|
|
4
|
+
doc,
|
|
5
|
+
DocumentData,
|
|
6
|
+
getCountFromServer,
|
|
7
|
+
getDoc,
|
|
8
|
+
getDocs,
|
|
9
|
+
limit,
|
|
10
|
+
orderBy,
|
|
11
|
+
query,
|
|
12
|
+
startAfter,
|
|
13
|
+
updateDoc,
|
|
14
|
+
where,
|
|
15
|
+
arrayUnion,
|
|
16
|
+
arrayRemove,
|
|
17
|
+
Firestore,
|
|
18
|
+
writeBatch,
|
|
19
|
+
QueryConstraint,
|
|
20
|
+
} from 'firebase/firestore';
|
|
21
|
+
import { Technology, TECHNOLOGIES_COLLECTION, ITechnologyService } from '../types/technology.types';
|
|
22
|
+
import { Requirement, RequirementType } from '../types/requirement.types';
|
|
23
|
+
import { BlockingCondition } from '../types/static/blocking-condition.types';
|
|
24
|
+
import { ContraindicationDynamic } from '../types/admin-constants.types';
|
|
25
|
+
import { TreatmentBenefitDynamic } from '../types/admin-constants.types';
|
|
26
|
+
import {
|
|
27
|
+
CertificationLevel,
|
|
28
|
+
CertificationSpecialty,
|
|
29
|
+
CertificationRequirement,
|
|
30
|
+
} from '../types/static/certification.types';
|
|
31
|
+
import { BaseService } from '../../services/base.service';
|
|
32
|
+
import { ProcedureFamily } from '../types/static/procedure-family.types';
|
|
33
|
+
import { Practitioner, PractitionerCertification } from '../../types/practitioner';
|
|
34
|
+
import { Product, PRODUCTS_COLLECTION } from '../types/product.types';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ID of the free-consultation-tech technology that should be hidden from admin backoffice.
|
|
38
|
+
* This technology is used internally for free consultation procedures.
|
|
39
|
+
*/
|
|
40
|
+
const EXCLUDED_TECHNOLOGY_ID = 'free-consultation-tech';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default vrednosti za sertifikaciju
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_CERTIFICATION_REQUIREMENT: CertificationRequirement = {
|
|
46
|
+
minimumLevel: CertificationLevel.AESTHETICIAN,
|
|
47
|
+
requiredSpecialties: [],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Service for managing technologies.
|
|
52
|
+
*/
|
|
53
|
+
export class TechnologyService extends BaseService implements ITechnologyService {
|
|
54
|
+
/**
|
|
55
|
+
* Filters out excluded technologies from a list.
|
|
56
|
+
* @param technologies - List of technologies to filter
|
|
57
|
+
* @returns Filtered list without excluded technologies
|
|
58
|
+
*/
|
|
59
|
+
private filterExcludedTechnologies(technologies: Technology[]): Technology[] {
|
|
60
|
+
return technologies.filter(tech => tech.id !== EXCLUDED_TECHNOLOGY_ID);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reference to the Firestore collection of technologies.
|
|
64
|
+
*/
|
|
65
|
+
private get technologiesRef() {
|
|
66
|
+
return collection(this.db, TECHNOLOGIES_COLLECTION);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Creates a new technology.
|
|
71
|
+
* @param technology - Data for the new technology.
|
|
72
|
+
* @returns The created technology with its generated ID.
|
|
73
|
+
*/
|
|
74
|
+
async create(technology: Omit<Technology, 'id' | 'createdAt' | 'updatedAt'>) {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
// Explicitly construct the object to ensure no undefined values are passed.
|
|
77
|
+
const newTechnology: Omit<Technology, 'id'> = {
|
|
78
|
+
name: technology.name,
|
|
79
|
+
description: technology.description,
|
|
80
|
+
family: technology.family,
|
|
81
|
+
categoryId: technology.categoryId,
|
|
82
|
+
subcategoryId: technology.subcategoryId,
|
|
83
|
+
requirements: technology.requirements || { pre: [], post: [] },
|
|
84
|
+
blockingConditions: technology.blockingConditions || [],
|
|
85
|
+
contraindications: technology.contraindications || [],
|
|
86
|
+
benefits: technology.benefits || [],
|
|
87
|
+
certificationRequirement:
|
|
88
|
+
technology.certificationRequirement || DEFAULT_CERTIFICATION_REQUIREMENT,
|
|
89
|
+
documentationTemplates: technology.documentationTemplates || [],
|
|
90
|
+
isActive: true,
|
|
91
|
+
createdAt: now,
|
|
92
|
+
updatedAt: now,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Add optional fields only if they are not undefined
|
|
96
|
+
if (technology.technicalDetails) {
|
|
97
|
+
newTechnology.technicalDetails = technology.technicalDetails;
|
|
98
|
+
}
|
|
99
|
+
if (technology.photoTemplate) {
|
|
100
|
+
newTechnology.photoTemplate = technology.photoTemplate;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const docRef = await addDoc(this.technologiesRef, newTechnology as any);
|
|
104
|
+
return { id: docRef.id, ...newTechnology };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns counts of technologies for each subcategory.
|
|
109
|
+
* @param active - Whether to count active or inactive technologies.
|
|
110
|
+
* @returns A record mapping subcategory ID to technology count.
|
|
111
|
+
*/
|
|
112
|
+
async getTechnologyCounts(active = true) {
|
|
113
|
+
const q = query(this.technologiesRef, where('isActive', '==', active));
|
|
114
|
+
const snapshot = await getDocs(q);
|
|
115
|
+
const counts: Record<string, number> = {};
|
|
116
|
+
snapshot.docs.forEach(doc => {
|
|
117
|
+
// Exclude free-consultation-tech from counts
|
|
118
|
+
if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
|
|
119
|
+
const tech = doc.data() as Technology;
|
|
120
|
+
counts[tech.subcategoryId] = (counts[tech.subcategoryId] || 0) + 1;
|
|
121
|
+
});
|
|
122
|
+
return counts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns counts of technologies for each category.
|
|
127
|
+
* @param active - Whether to count active or inactive technologies.
|
|
128
|
+
* @returns A record mapping category ID to technology count.
|
|
129
|
+
*/
|
|
130
|
+
async getTechnologyCountsByCategory(active = true) {
|
|
131
|
+
const q = query(this.technologiesRef, where('isActive', '==', active));
|
|
132
|
+
const snapshot = await getDocs(q);
|
|
133
|
+
const counts: Record<string, number> = {};
|
|
134
|
+
snapshot.docs.forEach(doc => {
|
|
135
|
+
// Exclude free-consultation-tech from counts
|
|
136
|
+
if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
|
|
137
|
+
const tech = doc.data() as Technology;
|
|
138
|
+
counts[tech.categoryId] = (counts[tech.categoryId] || 0) + 1;
|
|
139
|
+
});
|
|
140
|
+
return counts;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Returns all technologies with pagination.
|
|
145
|
+
* @param options - Pagination and filter options.
|
|
146
|
+
* @returns A list of technologies and the last visible document.
|
|
147
|
+
*/
|
|
148
|
+
async getAll(
|
|
149
|
+
options: {
|
|
150
|
+
active?: boolean;
|
|
151
|
+
limit?: number;
|
|
152
|
+
lastVisible?: DocumentData;
|
|
153
|
+
} = {},
|
|
154
|
+
) {
|
|
155
|
+
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
156
|
+
const constraints = [
|
|
157
|
+
where('isActive', '==', active),
|
|
158
|
+
orderBy('name'),
|
|
159
|
+
queryLimit ? limit(queryLimit) : undefined,
|
|
160
|
+
lastVisible ? startAfter(lastVisible) : undefined,
|
|
161
|
+
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
162
|
+
|
|
163
|
+
const q = query(this.technologiesRef, ...constraints);
|
|
164
|
+
const snapshot = await getDocs(q);
|
|
165
|
+
const technologies = snapshot.docs.map(
|
|
166
|
+
doc =>
|
|
167
|
+
({
|
|
168
|
+
id: doc.id,
|
|
169
|
+
...doc.data(),
|
|
170
|
+
} as Technology),
|
|
171
|
+
);
|
|
172
|
+
const filteredTechnologies = this.filterExcludedTechnologies(technologies);
|
|
173
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
174
|
+
return { technologies: filteredTechnologies, lastVisible: newLastVisible };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Returns all technologies for a specific category with pagination.
|
|
179
|
+
* @param categoryId - The ID of the category.
|
|
180
|
+
* @param options - Pagination options.
|
|
181
|
+
* @returns A list of technologies for the specified category.
|
|
182
|
+
*/
|
|
183
|
+
async getAllByCategoryId(
|
|
184
|
+
categoryId: string,
|
|
185
|
+
options: {
|
|
186
|
+
active?: boolean;
|
|
187
|
+
limit?: number;
|
|
188
|
+
lastVisible?: DocumentData;
|
|
189
|
+
} = {},
|
|
190
|
+
) {
|
|
191
|
+
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
192
|
+
const constraints = [
|
|
193
|
+
where('categoryId', '==', categoryId),
|
|
194
|
+
where('isActive', '==', active),
|
|
195
|
+
orderBy('name'),
|
|
196
|
+
queryLimit ? limit(queryLimit) : undefined,
|
|
197
|
+
lastVisible ? startAfter(lastVisible) : undefined,
|
|
198
|
+
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
199
|
+
|
|
200
|
+
const q = query(this.technologiesRef, ...constraints);
|
|
201
|
+
const snapshot = await getDocs(q);
|
|
202
|
+
const technologies = snapshot.docs.map(
|
|
203
|
+
doc =>
|
|
204
|
+
({
|
|
205
|
+
id: doc.id,
|
|
206
|
+
...doc.data(),
|
|
207
|
+
} as Technology),
|
|
208
|
+
);
|
|
209
|
+
const filteredTechnologies = this.filterExcludedTechnologies(technologies);
|
|
210
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
211
|
+
return { technologies: filteredTechnologies, lastVisible: newLastVisible };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns all technologies for a specific subcategory with pagination.
|
|
216
|
+
* @param subcategoryId - The ID of the subcategory.
|
|
217
|
+
* @param options - Pagination options.
|
|
218
|
+
* @returns A list of technologies for the specified subcategory.
|
|
219
|
+
*/
|
|
220
|
+
async getAllBySubcategoryId(
|
|
221
|
+
subcategoryId: string,
|
|
222
|
+
options: {
|
|
223
|
+
active?: boolean;
|
|
224
|
+
limit?: number;
|
|
225
|
+
lastVisible?: DocumentData;
|
|
226
|
+
} = {},
|
|
227
|
+
) {
|
|
228
|
+
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
229
|
+
const constraints = [
|
|
230
|
+
where('subcategoryId', '==', subcategoryId),
|
|
231
|
+
where('isActive', '==', active),
|
|
232
|
+
orderBy('name'),
|
|
233
|
+
queryLimit ? limit(queryLimit) : undefined,
|
|
234
|
+
lastVisible ? startAfter(lastVisible) : undefined,
|
|
235
|
+
].filter((c): c is NonNullable<typeof c> => !!c);
|
|
236
|
+
|
|
237
|
+
const q = query(this.technologiesRef, ...constraints);
|
|
238
|
+
const snapshot = await getDocs(q);
|
|
239
|
+
const technologies = snapshot.docs.map(
|
|
240
|
+
doc =>
|
|
241
|
+
({
|
|
242
|
+
id: doc.id,
|
|
243
|
+
...doc.data(),
|
|
244
|
+
} as Technology),
|
|
245
|
+
);
|
|
246
|
+
const filteredTechnologies = this.filterExcludedTechnologies(technologies);
|
|
247
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
248
|
+
return { technologies: filteredTechnologies, lastVisible: newLastVisible };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Updates an existing technology.
|
|
253
|
+
* @param id - The ID of the technology to update.
|
|
254
|
+
* @param technology - New data for the technology.
|
|
255
|
+
* @returns The updated technology.
|
|
256
|
+
*/
|
|
257
|
+
async update(id: string, technology: Partial<Omit<Technology, 'id' | 'createdAt'>>) {
|
|
258
|
+
const updateData: { [key: string]: any } = { ...technology };
|
|
259
|
+
|
|
260
|
+
// Remove undefined fields to prevent Firestore errors
|
|
261
|
+
Object.keys(updateData).forEach(key => {
|
|
262
|
+
if (updateData[key] === undefined) {
|
|
263
|
+
delete updateData[key];
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Handle photoTemplate: if explicitly set to null or empty string, allow it to be cleared
|
|
268
|
+
// If undefined, don't include it in the update (field won't change)
|
|
269
|
+
if ('photoTemplate' in technology) {
|
|
270
|
+
if (technology.photoTemplate === null || technology.photoTemplate === '') {
|
|
271
|
+
updateData.photoTemplate = null;
|
|
272
|
+
} else if (technology.photoTemplate !== undefined) {
|
|
273
|
+
updateData.photoTemplate = technology.photoTemplate;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
updateData.updatedAt = new Date();
|
|
278
|
+
|
|
279
|
+
const docRef = doc(this.technologiesRef, id);
|
|
280
|
+
|
|
281
|
+
// Get the technology before update to check what changed
|
|
282
|
+
const beforeTech = await this.getById(id);
|
|
283
|
+
|
|
284
|
+
await updateDoc(docRef, updateData);
|
|
285
|
+
|
|
286
|
+
// If categoryId, subcategoryId, or name changed, update all products in subcollection
|
|
287
|
+
const categoryChanged = beforeTech && updateData.categoryId && beforeTech.categoryId !== updateData.categoryId;
|
|
288
|
+
const subcategoryChanged = beforeTech && updateData.subcategoryId && beforeTech.subcategoryId !== updateData.subcategoryId;
|
|
289
|
+
const nameChanged = beforeTech && updateData.name && beforeTech.name !== updateData.name;
|
|
290
|
+
|
|
291
|
+
if (categoryChanged || subcategoryChanged || nameChanged) {
|
|
292
|
+
await this.updateProductsInSubcollection(id, {
|
|
293
|
+
categoryId: updateData.categoryId,
|
|
294
|
+
subcategoryId: updateData.subcategoryId,
|
|
295
|
+
technologyName: updateData.name,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return this.getById(id);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Soft deletes a technology.
|
|
304
|
+
* @param id - The ID of the technology to delete.
|
|
305
|
+
*/
|
|
306
|
+
async delete(id: string) {
|
|
307
|
+
await this.update(id, { isActive: false });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Reactivates a technology.
|
|
312
|
+
* @param id - The ID of the technology to reactivate.
|
|
313
|
+
*/
|
|
314
|
+
async reactivate(id: string) {
|
|
315
|
+
await this.update(id, { isActive: true });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns a technology by its ID.
|
|
320
|
+
* @param id - The ID of the requested technology.
|
|
321
|
+
* @returns The technology or null if it doesn't exist.
|
|
322
|
+
*/
|
|
323
|
+
async getById(id: string): Promise<Technology | null> {
|
|
324
|
+
// Prevent access to excluded technology
|
|
325
|
+
if (id === EXCLUDED_TECHNOLOGY_ID) return null;
|
|
326
|
+
|
|
327
|
+
const docRef = doc(this.technologiesRef, id);
|
|
328
|
+
const docSnap = await getDoc(docRef);
|
|
329
|
+
if (!docSnap.exists()) return null;
|
|
330
|
+
return {
|
|
331
|
+
id: docSnap.id,
|
|
332
|
+
...docSnap.data(),
|
|
333
|
+
} as Technology;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Internal method to get technology by ID without filtering.
|
|
338
|
+
* Used internally for consultation procedures.
|
|
339
|
+
* @param id - The ID of the requested technology
|
|
340
|
+
* @returns The technology or null if it doesn't exist
|
|
341
|
+
*/
|
|
342
|
+
async getByIdInternal(id: string): Promise<Technology | null> {
|
|
343
|
+
const docRef = doc(this.technologiesRef, id);
|
|
344
|
+
const docSnap = await getDoc(docRef);
|
|
345
|
+
if (!docSnap.exists()) return null;
|
|
346
|
+
return {
|
|
347
|
+
id: docSnap.id,
|
|
348
|
+
...docSnap.data(),
|
|
349
|
+
} as Technology;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Finds a technology by exact name match.
|
|
354
|
+
* Used for CSV import duplicate detection.
|
|
355
|
+
* @param name - Exact name of the technology to find
|
|
356
|
+
* @returns Technology if found, null otherwise
|
|
357
|
+
*/
|
|
358
|
+
async findByName(name: string): Promise<Technology | null> {
|
|
359
|
+
const q = query(
|
|
360
|
+
this.technologiesRef,
|
|
361
|
+
where('name', '==', name),
|
|
362
|
+
where('isActive', '==', true),
|
|
363
|
+
);
|
|
364
|
+
const snapshot = await getDocs(q);
|
|
365
|
+
if (snapshot.empty) return null;
|
|
366
|
+
const doc = snapshot.docs[0];
|
|
367
|
+
// Exclude free-consultation-tech
|
|
368
|
+
if (doc.id === EXCLUDED_TECHNOLOGY_ID) return null;
|
|
369
|
+
return {
|
|
370
|
+
id: doc.id,
|
|
371
|
+
...doc.data(),
|
|
372
|
+
} as Technology;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Dodaje novi zahtev tehnologiji
|
|
377
|
+
* @param technologyId - ID tehnologije
|
|
378
|
+
* @param requirement - Zahtev koji se dodaje
|
|
379
|
+
* @returns Ažurirana tehnologija sa novim zahtevom
|
|
380
|
+
*/
|
|
381
|
+
async addRequirement(technologyId: string, requirement: Requirement) {
|
|
382
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
383
|
+
|
|
384
|
+
const requirementType = requirement.type === 'pre' ? 'requirements.pre' : 'requirements.post';
|
|
385
|
+
|
|
386
|
+
await updateDoc(docRef, {
|
|
387
|
+
[requirementType]: arrayUnion(requirement),
|
|
388
|
+
updatedAt: new Date(),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return this.getById(technologyId);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Uklanja zahtev iz tehnologije
|
|
396
|
+
* @param technologyId - ID tehnologije
|
|
397
|
+
* @param requirement - Zahtev koji se uklanja
|
|
398
|
+
* @returns Ažurirana tehnologija bez uklonjenog zahteva
|
|
399
|
+
*/
|
|
400
|
+
async removeRequirement(technologyId: string, requirement: Requirement) {
|
|
401
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
402
|
+
|
|
403
|
+
const requirementType = requirement.type === 'pre' ? 'requirements.pre' : 'requirements.post';
|
|
404
|
+
|
|
405
|
+
await updateDoc(docRef, {
|
|
406
|
+
[requirementType]: arrayRemove(requirement),
|
|
407
|
+
updatedAt: new Date(),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return this.getById(technologyId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Vraća sve zahteve za tehnologiju
|
|
415
|
+
* @param technologyId - ID tehnologije
|
|
416
|
+
* @param type - Opcioni filter za tip zahteva (pre/post)
|
|
417
|
+
* @returns Lista zahteva
|
|
418
|
+
*/
|
|
419
|
+
async getRequirements(technologyId: string, type?: RequirementType) {
|
|
420
|
+
const technology = await this.getById(technologyId);
|
|
421
|
+
if (!technology || !technology.requirements) return [];
|
|
422
|
+
|
|
423
|
+
if (type) {
|
|
424
|
+
return technology.requirements[type];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return [...technology.requirements.pre, ...technology.requirements.post];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Ažurira postojeći zahtev
|
|
432
|
+
* @param technologyId - ID tehnologije
|
|
433
|
+
* @param oldRequirement - Stari zahtev koji se menja
|
|
434
|
+
* @param newRequirement - Novi zahtev koji zamenjuje stari
|
|
435
|
+
* @returns Ažurirana tehnologija
|
|
436
|
+
*/
|
|
437
|
+
async updateRequirement(
|
|
438
|
+
technologyId: string,
|
|
439
|
+
oldRequirement: Requirement,
|
|
440
|
+
newRequirement: Requirement,
|
|
441
|
+
) {
|
|
442
|
+
await this.removeRequirement(technologyId, oldRequirement);
|
|
443
|
+
return this.addRequirement(technologyId, newRequirement);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Dodaje blokirajući uslov tehnologiji
|
|
448
|
+
* @param technologyId - ID tehnologije
|
|
449
|
+
* @param condition - Blokirajući uslov koji se dodaje
|
|
450
|
+
* @returns Ažurirana tehnologija
|
|
451
|
+
*/
|
|
452
|
+
async addBlockingCondition(technologyId: string, condition: BlockingCondition) {
|
|
453
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
454
|
+
|
|
455
|
+
await updateDoc(docRef, {
|
|
456
|
+
blockingConditions: arrayUnion(condition),
|
|
457
|
+
updatedAt: new Date(),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
return this.getById(technologyId);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Uklanja blokirajući uslov iz tehnologije
|
|
465
|
+
* @param technologyId - ID tehnologije
|
|
466
|
+
* @param condition - Blokirajući uslov koji se uklanja
|
|
467
|
+
* @returns Ažurirana tehnologija
|
|
468
|
+
*/
|
|
469
|
+
async removeBlockingCondition(technologyId: string, condition: BlockingCondition) {
|
|
470
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
471
|
+
|
|
472
|
+
await updateDoc(docRef, {
|
|
473
|
+
blockingConditions: arrayRemove(condition),
|
|
474
|
+
updatedAt: new Date(),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return this.getById(technologyId);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Dodaje kontraindikaciju tehnologiji
|
|
482
|
+
* @param technologyId - ID tehnologije
|
|
483
|
+
* @param contraindication - Kontraindikacija koja se dodaje
|
|
484
|
+
* @returns Ažurirana tehnologija
|
|
485
|
+
*/
|
|
486
|
+
async addContraindication(technologyId: string, contraindication: ContraindicationDynamic) {
|
|
487
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
488
|
+
const technology = await this.getById(technologyId);
|
|
489
|
+
if (!technology) {
|
|
490
|
+
throw new Error(`Technology with id ${technologyId} not found`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const existingContraindications = technology.contraindications || [];
|
|
494
|
+
if (existingContraindications.some(c => c.id === contraindication.id)) {
|
|
495
|
+
return technology; // Already exists, do nothing
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await updateDoc(docRef, {
|
|
499
|
+
contraindications: [...existingContraindications, contraindication],
|
|
500
|
+
updatedAt: new Date(),
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
return this.getById(technologyId);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Uklanja kontraindikaciju iz tehnologije
|
|
508
|
+
* @param technologyId - ID tehnologije
|
|
509
|
+
* @param contraindication - Kontraindikacija koja se uklanja
|
|
510
|
+
* @returns Ažurirana tehnologija
|
|
511
|
+
*/
|
|
512
|
+
async removeContraindication(technologyId: string, contraindication: ContraindicationDynamic) {
|
|
513
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
514
|
+
const technology = await this.getById(technologyId);
|
|
515
|
+
if (!technology) {
|
|
516
|
+
throw new Error(`Technology with id ${technologyId} not found`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const updatedContraindications = (technology.contraindications || []).filter(
|
|
520
|
+
c => c.id !== contraindication.id,
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
await updateDoc(docRef, {
|
|
524
|
+
contraindications: updatedContraindications,
|
|
525
|
+
updatedAt: new Date(),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
return this.getById(technologyId);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Updates an existing contraindication in a technology's list.
|
|
533
|
+
* If the contraindication does not exist, it will not be added.
|
|
534
|
+
* @param technologyId - ID of the technology
|
|
535
|
+
* @param contraindication - The updated contraindication object
|
|
536
|
+
* @returns The updated technology
|
|
537
|
+
*/
|
|
538
|
+
async updateContraindication(technologyId: string, contraindication: ContraindicationDynamic) {
|
|
539
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
540
|
+
const technology = await this.getById(technologyId);
|
|
541
|
+
if (!technology) {
|
|
542
|
+
throw new Error(`Technology with id ${technologyId} not found`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const contraindications = technology.contraindications || [];
|
|
546
|
+
const index = contraindications.findIndex(c => c.id === contraindication.id);
|
|
547
|
+
|
|
548
|
+
if (index === -1) {
|
|
549
|
+
// If contraindication doesn't exist, do not update
|
|
550
|
+
// Consider throwing an error if this is an unexpected state
|
|
551
|
+
console.warn(
|
|
552
|
+
`Contraindication with id ${contraindication.id} not found for technology ${technologyId}. No update performed.`,
|
|
553
|
+
);
|
|
554
|
+
return technology;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const updatedContraindications = [...contraindications];
|
|
558
|
+
updatedContraindications[index] = contraindication;
|
|
559
|
+
|
|
560
|
+
await updateDoc(docRef, {
|
|
561
|
+
contraindications: updatedContraindications,
|
|
562
|
+
updatedAt: new Date(),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return this.getById(technologyId);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Dodaje benefit tehnologiji
|
|
570
|
+
* @param technologyId - ID tehnologije
|
|
571
|
+
* @param benefit - Benefit koji se dodaje
|
|
572
|
+
* @returns Ažurirana tehnologija
|
|
573
|
+
*/
|
|
574
|
+
async addBenefit(technologyId: string, benefit: TreatmentBenefitDynamic) {
|
|
575
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
576
|
+
const technology = await this.getById(technologyId);
|
|
577
|
+
if (!technology) {
|
|
578
|
+
throw new Error(`Technology with id ${technologyId} not found`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const existingBenefits = technology.benefits || [];
|
|
582
|
+
if (existingBenefits.some(b => b.id === benefit.id)) {
|
|
583
|
+
return technology; // Already exists, do nothing
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
await updateDoc(docRef, {
|
|
587
|
+
benefits: [...existingBenefits, benefit],
|
|
588
|
+
updatedAt: new Date(),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
return this.getById(technologyId);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Uklanja benefit iz tehnologije
|
|
596
|
+
* @param technologyId - ID tehnologije
|
|
597
|
+
* @param benefit - Benefit koji se uklanja
|
|
598
|
+
* @returns Ažurirana tehnologija
|
|
599
|
+
*/
|
|
600
|
+
async removeBenefit(technologyId: string, benefit: TreatmentBenefitDynamic) {
|
|
601
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
602
|
+
const technology = await this.getById(technologyId);
|
|
603
|
+
if (!technology) {
|
|
604
|
+
throw new Error(`Technology with id ${technologyId} not found`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const updatedBenefits = (technology.benefits || []).filter(b => b.id !== benefit.id);
|
|
608
|
+
|
|
609
|
+
await updateDoc(docRef, {
|
|
610
|
+
benefits: updatedBenefits,
|
|
611
|
+
updatedAt: new Date(),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
return this.getById(technologyId);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Updates an existing benefit in a technology's list.
|
|
619
|
+
* If the benefit does not exist, it will not be added.
|
|
620
|
+
* @param technologyId - ID of the technology
|
|
621
|
+
* @param benefit - The updated benefit object
|
|
622
|
+
* @returns The updated technology
|
|
623
|
+
*/
|
|
624
|
+
async updateBenefit(technologyId: string, benefit: TreatmentBenefitDynamic) {
|
|
625
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
626
|
+
const technology = await this.getById(technologyId);
|
|
627
|
+
if (!technology) {
|
|
628
|
+
throw new Error(`Technology with id ${technologyId} not found`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const benefits = technology.benefits || [];
|
|
632
|
+
const index = benefits.findIndex(b => b.id === benefit.id);
|
|
633
|
+
|
|
634
|
+
if (index === -1) {
|
|
635
|
+
// If benefit doesn't exist, do not update
|
|
636
|
+
console.warn(
|
|
637
|
+
`Benefit with id ${benefit.id} not found for technology ${technologyId}. No update performed.`,
|
|
638
|
+
);
|
|
639
|
+
return technology;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const updatedBenefits = [...benefits];
|
|
643
|
+
updatedBenefits[index] = benefit;
|
|
644
|
+
|
|
645
|
+
await updateDoc(docRef, {
|
|
646
|
+
benefits: updatedBenefits,
|
|
647
|
+
updatedAt: new Date(),
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
return this.getById(technologyId);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Vraća sve blokirajuće uslove za tehnologiju
|
|
655
|
+
* @param technologyId - ID tehnologije
|
|
656
|
+
* @returns Lista blokirajućih uslova
|
|
657
|
+
*/
|
|
658
|
+
async getBlockingConditions(technologyId: string) {
|
|
659
|
+
const technology = await this.getById(technologyId);
|
|
660
|
+
return technology?.blockingConditions || [];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Vraća sve kontraindikacije za tehnologiju
|
|
665
|
+
* @param technologyId - ID tehnologije
|
|
666
|
+
* @returns Lista kontraindikacija
|
|
667
|
+
*/
|
|
668
|
+
async getContraindications(technologyId: string) {
|
|
669
|
+
const technology = await this.getById(technologyId);
|
|
670
|
+
return technology?.contraindications || [];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Vraća sve benefite za tehnologiju
|
|
675
|
+
* @param technologyId - ID tehnologije
|
|
676
|
+
* @returns Lista benefita
|
|
677
|
+
*/
|
|
678
|
+
async getBenefits(technologyId: string) {
|
|
679
|
+
const technology = await this.getById(technologyId);
|
|
680
|
+
return technology?.benefits || [];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Ažurira zahteve sertifikacije za tehnologiju
|
|
685
|
+
* @param technologyId - ID tehnologije
|
|
686
|
+
* @param certificationRequirement - Novi zahtevi sertifikacije
|
|
687
|
+
* @returns Ažurirana tehnologija
|
|
688
|
+
*/
|
|
689
|
+
async updateCertificationRequirement(
|
|
690
|
+
technologyId: string,
|
|
691
|
+
certificationRequirement: CertificationRequirement,
|
|
692
|
+
) {
|
|
693
|
+
const docRef = doc(this.technologiesRef, technologyId);
|
|
694
|
+
|
|
695
|
+
await updateDoc(docRef, {
|
|
696
|
+
certificationRequirement,
|
|
697
|
+
updatedAt: new Date(),
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return this.getById(technologyId);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Vraća zahteve sertifikacije za tehnologiju
|
|
705
|
+
* @param technologyId - ID tehnologije
|
|
706
|
+
* @returns Zahtevi sertifikacije ili null ako tehnologija ne postoji
|
|
707
|
+
*/
|
|
708
|
+
async getCertificationRequirement(
|
|
709
|
+
technologyId: string,
|
|
710
|
+
): Promise<CertificationRequirement | null> {
|
|
711
|
+
const technology = await this.getById(technologyId);
|
|
712
|
+
return technology?.certificationRequirement || null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Proverava da li doktor ima odgovarajuću sertifikaciju za izvođenje tehnologije
|
|
717
|
+
*
|
|
718
|
+
* @param requiredCertification - Zahtevana sertifikacija za tehnologiju
|
|
719
|
+
* @param practitionerCertification - Sertifikacija zdravstvenog radnika
|
|
720
|
+
* @returns true ako zdravstveni radnik ima odgovarajuću sertifikaciju, false ako nema
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* const isValid = technologyService.validateCertification(
|
|
724
|
+
* {
|
|
725
|
+
* minimumLevel: CertificationLevel.DOCTOR,
|
|
726
|
+
* requiredSpecialties: [CertificationSpecialty.INJECTABLES]
|
|
727
|
+
* },
|
|
728
|
+
* {
|
|
729
|
+
* level: CertificationLevel.SPECIALIST,
|
|
730
|
+
* specialties: [CertificationSpecialty.INJECTABLES, CertificationSpecialty.LASER]
|
|
731
|
+
* }
|
|
732
|
+
* );
|
|
733
|
+
*/
|
|
734
|
+
validateCertification(
|
|
735
|
+
requiredCertification: CertificationRequirement,
|
|
736
|
+
practitionerCertification: PractitionerCertification,
|
|
737
|
+
): boolean {
|
|
738
|
+
// Provera nivoa sertifikacije
|
|
739
|
+
// Enum je definisan od najnižeg ka najvišem, pa možemo porediti brojeve
|
|
740
|
+
const doctorLevel = Object.values(CertificationLevel).indexOf(practitionerCertification.level);
|
|
741
|
+
const requiredLevel = Object.values(CertificationLevel).indexOf(
|
|
742
|
+
requiredCertification.minimumLevel,
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// Doktor mora imati nivo koji je jednak ili viši od zahtevanog
|
|
746
|
+
if (doctorLevel < requiredLevel) return false;
|
|
747
|
+
|
|
748
|
+
// Provera specijalizacija
|
|
749
|
+
const requiredSpecialties = requiredCertification.requiredSpecialties || [];
|
|
750
|
+
if (requiredSpecialties.length > 0) {
|
|
751
|
+
// Doktor mora imati sve zahtevane specijalizacije
|
|
752
|
+
const doctorSpecialties = practitionerCertification.specialties;
|
|
753
|
+
const hasAllRequiredSpecialties = requiredSpecialties.every(requiredSpecialty =>
|
|
754
|
+
doctorSpecialties.includes(requiredSpecialty),
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
if (!hasAllRequiredSpecialties) return false;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Vraća sve tehnologije koje je zdravstveni radnik sertifikovan da izvodi
|
|
765
|
+
* zajedno sa listama dozvoljenih familija, kategorija i podkategorija
|
|
766
|
+
*
|
|
767
|
+
* @param practitioner - Profil zdravstvenog radnika
|
|
768
|
+
* @returns Objekat koji sadrži:
|
|
769
|
+
* - technologies: Lista tehnologija koje zdravstveni radnik može da izvodi
|
|
770
|
+
* - families: Lista familija procedura koje zdravstveni radnik može da izvodi
|
|
771
|
+
* - categories: Lista ID-eva kategorija koje zdravstveni radnik može da izvodi
|
|
772
|
+
* - subcategories: Lista ID-eva podkategorija koje zdravstveni radnik može da izvodi
|
|
773
|
+
*
|
|
774
|
+
* @example
|
|
775
|
+
* const practitioner = {
|
|
776
|
+
* certification: {
|
|
777
|
+
* level: CertificationLevel.DOCTOR,
|
|
778
|
+
* specialties: [CertificationSpecialty.INJECTABLES]
|
|
779
|
+
* }
|
|
780
|
+
* };
|
|
781
|
+
* const allowedTechnologies = await technologyService.getAllowedTechnologies(practitioner);
|
|
782
|
+
* console.log(allowedTechnologies.families); // [ProcedureFamily.AESTHETICS]
|
|
783
|
+
* console.log(allowedTechnologies.categories); // ["category1", "category2"]
|
|
784
|
+
* console.log(allowedTechnologies.subcategories); // ["subcategory1", "subcategory2"]
|
|
785
|
+
*/
|
|
786
|
+
async getAllowedTechnologies(practitioner: Practitioner): Promise<{
|
|
787
|
+
technologies: Technology[];
|
|
788
|
+
families: ProcedureFamily[];
|
|
789
|
+
categories: string[];
|
|
790
|
+
subcategories: string[];
|
|
791
|
+
}> {
|
|
792
|
+
// Get all active technologies
|
|
793
|
+
const allTechnologies = await this.getAllForFilter();
|
|
794
|
+
|
|
795
|
+
// Filter technologies based on certification requirements
|
|
796
|
+
const allowedTechnologies = allTechnologies.filter(technology =>
|
|
797
|
+
this.validateCertification(technology.certificationRequirement, practitioner.certification),
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// Extract unique families, categories, and subcategories
|
|
801
|
+
const families = [...new Set(allowedTechnologies.map(t => t.family))];
|
|
802
|
+
const categories = [...new Set(allowedTechnologies.map(t => t.categoryId))];
|
|
803
|
+
const subcategories = [...new Set(allowedTechnologies.map(t => t.subcategoryId))];
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
technologies: allowedTechnologies,
|
|
807
|
+
families,
|
|
808
|
+
categories,
|
|
809
|
+
subcategories,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Gets all active technologies for a subcategory for filter dropdowns (by subcategory only).
|
|
815
|
+
* @param subcategoryId - The ID of the subcategory.
|
|
816
|
+
*/
|
|
817
|
+
async getAllForFilterBySubcategory(subcategoryId: string): Promise<Technology[]> {
|
|
818
|
+
const q = query(
|
|
819
|
+
collection(this.db, TECHNOLOGIES_COLLECTION),
|
|
820
|
+
where('isActive', '==', true),
|
|
821
|
+
where('subcategoryId', '==', subcategoryId),
|
|
822
|
+
orderBy('name'),
|
|
823
|
+
);
|
|
824
|
+
const snapshot = await getDocs(q);
|
|
825
|
+
const technologies = snapshot.docs.map(
|
|
826
|
+
doc =>
|
|
827
|
+
({
|
|
828
|
+
id: doc.id,
|
|
829
|
+
...doc.data(),
|
|
830
|
+
} as Technology),
|
|
831
|
+
);
|
|
832
|
+
return this.filterExcludedTechnologies(technologies);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Gets all active technologies for a subcategory for filter dropdowns.
|
|
837
|
+
* @param categoryId - The ID of the parent category.
|
|
838
|
+
* @param subcategoryId - The ID of the subcategory.
|
|
839
|
+
*/
|
|
840
|
+
async getAllForFilterBySubcategoryId(
|
|
841
|
+
categoryId: string,
|
|
842
|
+
subcategoryId: string,
|
|
843
|
+
): Promise<Technology[]> {
|
|
844
|
+
const q = query(
|
|
845
|
+
collection(this.db, TECHNOLOGIES_COLLECTION),
|
|
846
|
+
where('isActive', '==', true),
|
|
847
|
+
where('categoryId', '==', categoryId),
|
|
848
|
+
where('subcategoryId', '==', subcategoryId),
|
|
849
|
+
orderBy('name'),
|
|
850
|
+
);
|
|
851
|
+
const snapshot = await getDocs(q);
|
|
852
|
+
const technologies = snapshot.docs.map(
|
|
853
|
+
doc =>
|
|
854
|
+
({
|
|
855
|
+
id: doc.id,
|
|
856
|
+
...doc.data(),
|
|
857
|
+
} as Technology),
|
|
858
|
+
);
|
|
859
|
+
return this.filterExcludedTechnologies(technologies);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Gets all active technologies for filter dropdowns.
|
|
864
|
+
*/
|
|
865
|
+
async getAllForFilter(): Promise<Technology[]> {
|
|
866
|
+
const q = query(
|
|
867
|
+
collection(this.db, TECHNOLOGIES_COLLECTION),
|
|
868
|
+
where('isActive', '==', true),
|
|
869
|
+
orderBy('name'),
|
|
870
|
+
);
|
|
871
|
+
const snapshot = await getDocs(q);
|
|
872
|
+
const technologies = snapshot.docs.map(
|
|
873
|
+
doc =>
|
|
874
|
+
({
|
|
875
|
+
id: doc.id,
|
|
876
|
+
...doc.data(),
|
|
877
|
+
} as Technology),
|
|
878
|
+
);
|
|
879
|
+
return this.filterExcludedTechnologies(technologies);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ==========================================
|
|
883
|
+
// NEW METHODS: Product assignment management
|
|
884
|
+
// ==========================================
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Assigns multiple products to a technology
|
|
888
|
+
* Updates each product's assignedTechnologyIds array
|
|
889
|
+
*/
|
|
890
|
+
async assignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
891
|
+
const batch = writeBatch(this.db);
|
|
892
|
+
|
|
893
|
+
for (const productId of productIds) {
|
|
894
|
+
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
895
|
+
batch.update(productRef, {
|
|
896
|
+
assignedTechnologyIds: arrayUnion(technologyId),
|
|
897
|
+
updatedAt: new Date(),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
await batch.commit();
|
|
902
|
+
// Cloud Function will handle syncing to subcollections
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Unassigns multiple products from a technology
|
|
907
|
+
* Updates each product's assignedTechnologyIds array
|
|
908
|
+
*/
|
|
909
|
+
async unassignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
910
|
+
const batch = writeBatch(this.db);
|
|
911
|
+
|
|
912
|
+
for (const productId of productIds) {
|
|
913
|
+
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
914
|
+
batch.update(productRef, {
|
|
915
|
+
assignedTechnologyIds: arrayRemove(technologyId),
|
|
916
|
+
updatedAt: new Date(),
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
await batch.commit();
|
|
921
|
+
// Cloud Function will handle removing from subcollections
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Gets products assigned to a specific technology
|
|
926
|
+
* Reads from top-level collection for immediate consistency (Cloud Functions may lag)
|
|
927
|
+
*/
|
|
928
|
+
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
929
|
+
const q = query(
|
|
930
|
+
collection(this.db, PRODUCTS_COLLECTION),
|
|
931
|
+
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
932
|
+
where('isActive', '==', true),
|
|
933
|
+
orderBy('name'),
|
|
934
|
+
);
|
|
935
|
+
const snapshot = await getDocs(q);
|
|
936
|
+
|
|
937
|
+
return snapshot.docs.map(
|
|
938
|
+
doc =>
|
|
939
|
+
({
|
|
940
|
+
id: doc.id,
|
|
941
|
+
...doc.data(),
|
|
942
|
+
} as Product),
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Gets products NOT assigned to a specific technology
|
|
948
|
+
*/
|
|
949
|
+
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
950
|
+
const q = query(
|
|
951
|
+
collection(this.db, PRODUCTS_COLLECTION),
|
|
952
|
+
where('isActive', '==', true),
|
|
953
|
+
orderBy('name'),
|
|
954
|
+
);
|
|
955
|
+
const snapshot = await getDocs(q);
|
|
956
|
+
|
|
957
|
+
const allProducts = snapshot.docs.map(
|
|
958
|
+
doc =>
|
|
959
|
+
({
|
|
960
|
+
id: doc.id,
|
|
961
|
+
...doc.data(),
|
|
962
|
+
} as Product),
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
// Filter out products already assigned to this technology
|
|
966
|
+
return allProducts.filter(product =>
|
|
967
|
+
!product.assignedTechnologyIds?.includes(technologyId)
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Gets product assignment statistics for a technology
|
|
973
|
+
*/
|
|
974
|
+
async getProductStats(technologyId: string): Promise<{
|
|
975
|
+
totalAssigned: number;
|
|
976
|
+
byBrand: Record<string, number>;
|
|
977
|
+
}> {
|
|
978
|
+
const products = await this.getAssignedProducts(technologyId);
|
|
979
|
+
|
|
980
|
+
const byBrand: Record<string, number> = {};
|
|
981
|
+
products.forEach(product => {
|
|
982
|
+
byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
totalAssigned: products.length,
|
|
987
|
+
byBrand,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Updates products in technology subcollection when technology metadata changes
|
|
993
|
+
* @param technologyId - ID of the technology
|
|
994
|
+
* @param updates - Fields to update (categoryId, subcategoryId, technologyName)
|
|
995
|
+
*/
|
|
996
|
+
private async updateProductsInSubcollection(
|
|
997
|
+
technologyId: string,
|
|
998
|
+
updates: { categoryId?: string; subcategoryId?: string; technologyName?: string }
|
|
999
|
+
): Promise<void> {
|
|
1000
|
+
const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
1001
|
+
const productsSnapshot = await getDocs(productsRef);
|
|
1002
|
+
|
|
1003
|
+
if (productsSnapshot.empty) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const batch = writeBatch(this.db);
|
|
1008
|
+
|
|
1009
|
+
for (const productDoc of productsSnapshot.docs) {
|
|
1010
|
+
const productRef = productDoc.ref;
|
|
1011
|
+
const updateFields: any = {};
|
|
1012
|
+
|
|
1013
|
+
if (updates.categoryId !== undefined) {
|
|
1014
|
+
updateFields.categoryId = updates.categoryId;
|
|
1015
|
+
}
|
|
1016
|
+
if (updates.subcategoryId !== undefined) {
|
|
1017
|
+
updateFields.subcategoryId = updates.subcategoryId;
|
|
1018
|
+
}
|
|
1019
|
+
if (updates.technologyName !== undefined) {
|
|
1020
|
+
updateFields.technologyName = updates.technologyName;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (Object.keys(updateFields).length > 0) {
|
|
1024
|
+
batch.update(productRef, updateFields);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
await batch.commit();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Exports technologies to CSV string, suitable for Excel/Sheets.
|
|
1033
|
+
* Includes headers and optional UTF-8 BOM.
|
|
1034
|
+
* By default exports only active technologies (set includeInactive to true to export all).
|
|
1035
|
+
* Includes product names from subcollections.
|
|
1036
|
+
*/
|
|
1037
|
+
async exportToCsv(options?: {
|
|
1038
|
+
includeInactive?: boolean;
|
|
1039
|
+
includeBom?: boolean;
|
|
1040
|
+
}): Promise<string> {
|
|
1041
|
+
const includeInactive = options?.includeInactive ?? false;
|
|
1042
|
+
const includeBom = options?.includeBom ?? true;
|
|
1043
|
+
|
|
1044
|
+
const headers = [
|
|
1045
|
+
"id",
|
|
1046
|
+
"name",
|
|
1047
|
+
"description",
|
|
1048
|
+
"family",
|
|
1049
|
+
"categoryId",
|
|
1050
|
+
"subcategoryId",
|
|
1051
|
+
"technicalDetails",
|
|
1052
|
+
"requirements_pre",
|
|
1053
|
+
"requirements_post",
|
|
1054
|
+
"blockingConditions",
|
|
1055
|
+
"contraindications",
|
|
1056
|
+
"benefits",
|
|
1057
|
+
"certificationMinimumLevel",
|
|
1058
|
+
"certificationRequiredSpecialties",
|
|
1059
|
+
"documentationTemplateIds",
|
|
1060
|
+
"productNames",
|
|
1061
|
+
"isActive",
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1064
|
+
const rows: string[] = [];
|
|
1065
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
1066
|
+
|
|
1067
|
+
const PAGE_SIZE = 1000;
|
|
1068
|
+
let cursor: any | undefined;
|
|
1069
|
+
|
|
1070
|
+
// Build base constraints
|
|
1071
|
+
const constraints: QueryConstraint[] = [];
|
|
1072
|
+
if (!includeInactive) {
|
|
1073
|
+
constraints.push(where("isActive", "==", true));
|
|
1074
|
+
}
|
|
1075
|
+
constraints.push(orderBy("name"));
|
|
1076
|
+
|
|
1077
|
+
// Page through all results
|
|
1078
|
+
// eslint-disable-next-line no-constant-condition
|
|
1079
|
+
while (true) {
|
|
1080
|
+
const queryConstraints: QueryConstraint[] = [...constraints, limit(PAGE_SIZE)];
|
|
1081
|
+
if (cursor) queryConstraints.push(startAfter(cursor));
|
|
1082
|
+
|
|
1083
|
+
const q = query(this.technologiesRef, ...queryConstraints);
|
|
1084
|
+
const snapshot = await getDocs(q);
|
|
1085
|
+
if (snapshot.empty) break;
|
|
1086
|
+
|
|
1087
|
+
for (const d of snapshot.docs) {
|
|
1088
|
+
// Exclude free-consultation-tech from CSV export
|
|
1089
|
+
if (d.id === EXCLUDED_TECHNOLOGY_ID) continue;
|
|
1090
|
+
const technology = ({ id: d.id, ...d.data() } as unknown) as Technology;
|
|
1091
|
+
// Fetch products for this technology
|
|
1092
|
+
const productNames = await this.getProductNamesForTechnology(technology.id!);
|
|
1093
|
+
rows.push(this.technologyToCsvRow(technology, productNames));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
1097
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const csvBody = rows.join("\r\n");
|
|
1101
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Gets product names from the technology's product subcollection
|
|
1106
|
+
*/
|
|
1107
|
+
private async getProductNamesForTechnology(technologyId: string): Promise<string[]> {
|
|
1108
|
+
try {
|
|
1109
|
+
const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
1110
|
+
const q = query(productsRef, where("isActive", "==", true));
|
|
1111
|
+
const snapshot = await getDocs(q);
|
|
1112
|
+
return snapshot.docs.map(doc => {
|
|
1113
|
+
const product = doc.data() as Product;
|
|
1114
|
+
return product.name || "";
|
|
1115
|
+
}).filter(name => name); // Filter out empty names
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
console.error(`Error fetching products for technology ${technologyId}:`, error);
|
|
1118
|
+
return [];
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
private technologyToCsvRow(technology: Technology, productNames: string[] = []): string {
|
|
1123
|
+
const values = [
|
|
1124
|
+
technology.id ?? "",
|
|
1125
|
+
technology.name ?? "",
|
|
1126
|
+
technology.description ?? "",
|
|
1127
|
+
technology.family ?? "",
|
|
1128
|
+
technology.categoryId ?? "",
|
|
1129
|
+
technology.subcategoryId ?? "",
|
|
1130
|
+
technology.technicalDetails ?? "",
|
|
1131
|
+
technology.requirements?.pre?.map(r => r.name).join(";") ?? "",
|
|
1132
|
+
technology.requirements?.post?.map(r => r.name).join(";") ?? "",
|
|
1133
|
+
technology.blockingConditions?.join(";") ?? "",
|
|
1134
|
+
technology.contraindications?.map(c => c.name).join(";") ?? "",
|
|
1135
|
+
technology.benefits?.map(b => b.name).join(";") ?? "",
|
|
1136
|
+
technology.certificationRequirement?.minimumLevel ?? "",
|
|
1137
|
+
technology.certificationRequirement?.requiredSpecialties?.join(";") ?? "",
|
|
1138
|
+
technology.documentationTemplates?.map(t => t.templateId).join(";") ?? "",
|
|
1139
|
+
productNames.join(";"),
|
|
1140
|
+
String(technology.isActive ?? ""),
|
|
1141
|
+
];
|
|
1142
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
private formatCsvValue(value: any): string {
|
|
1146
|
+
const str = value === null || value === undefined ? "" : String(value);
|
|
1147
|
+
// Escape double quotes by doubling them and wrap in quotes
|
|
1148
|
+
const escaped = str.replace(/"/g, '""');
|
|
1149
|
+
return `"${escaped}"`;
|
|
1150
|
+
}
|
|
1151
|
+
}
|