@churchapps/helpers 1.2.23 → 1.2.25

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.
Files changed (80) hide show
  1. package/dist/ApiHelper.js +4 -4
  2. package/dist/ApiHelper.js.map +1 -1
  3. package/dist/ArrayHelper.d.ts.map +1 -1
  4. package/dist/ArrayHelper.js.map +1 -1
  5. package/dist/CommonEnvironmentHelper.d.ts.map +1 -1
  6. package/dist/CommonEnvironmentHelper.js.map +1 -1
  7. package/dist/CurrencyHelper.js +3 -3
  8. package/dist/DateHelper.d.ts.map +1 -1
  9. package/dist/DateHelper.js +22 -22
  10. package/dist/DateHelper.js.map +1 -1
  11. package/dist/DonationHelper.d.ts.map +1 -1
  12. package/dist/DonationHelper.js +2 -2
  13. package/dist/DonationHelper.js.map +1 -1
  14. package/dist/ErrorHelper.d.ts.map +1 -1
  15. package/dist/ErrorHelper.js.map +1 -1
  16. package/dist/EventHelper.d.ts.map +1 -1
  17. package/dist/EventHelper.js +1 -1
  18. package/dist/EventHelper.js.map +1 -1
  19. package/dist/FileHelper.d.ts.map +1 -1
  20. package/dist/FileHelper.js +11 -9
  21. package/dist/FileHelper.js.map +1 -1
  22. package/dist/PersonHelper.d.ts.map +1 -1
  23. package/dist/PersonHelper.js +3 -3
  24. package/dist/PersonHelper.js.map +1 -1
  25. package/dist/PlanHelper.d.ts.map +1 -1
  26. package/dist/PlanHelper.js +1 -3
  27. package/dist/PlanHelper.js.map +1 -1
  28. package/dist/UniqueIdHelper.d.ts.map +1 -1
  29. package/dist/UniqueIdHelper.js +130 -7
  30. package/dist/UniqueIdHelper.js.map +1 -1
  31. package/dist/UserHelper.d.ts.map +1 -1
  32. package/dist/UserHelper.js.map +1 -1
  33. package/dist/contentProviders/LessonsContentProvider.d.ts.map +1 -1
  34. package/dist/contentProviders/LessonsContentProvider.js.map +1 -1
  35. package/dist/interfaces/Doing.d.ts +3 -0
  36. package/dist/interfaces/Doing.d.ts.map +1 -1
  37. package/dist/interfaces/Messaging.d.ts +21 -0
  38. package/dist/interfaces/Messaging.d.ts.map +1 -1
  39. package/dist/interfaces/Permissions.d.ts +9 -0
  40. package/dist/interfaces/Permissions.d.ts.map +1 -1
  41. package/dist/interfaces/Permissions.js +12 -21
  42. package/dist/interfaces/Permissions.js.map +1 -1
  43. package/package.json +13 -11
  44. package/.eslintrc.json +0 -29
  45. package/.github/FUNDING.yml +0 -1
  46. package/.prettierrc +0 -12
  47. package/.yarnrc.yml +0 -6
  48. package/scripts/build-cjs.js +0 -33
  49. package/src/ApiHelper.ts +0 -169
  50. package/src/AppearanceHelper.ts +0 -69
  51. package/src/ArrayHelper.ts +0 -157
  52. package/src/CommonEnvironmentHelper.ts +0 -104
  53. package/src/CurrencyHelper.ts +0 -66
  54. package/src/DateHelper.ts +0 -183
  55. package/src/DonationHelper.ts +0 -26
  56. package/src/ErrorHelper.ts +0 -39
  57. package/src/EventHelper.ts +0 -84
  58. package/src/FileHelper.ts +0 -55
  59. package/src/PersonHelper.ts +0 -82
  60. package/src/PlanHelper.ts +0 -137
  61. package/src/UniqueIdHelper.ts +0 -36
  62. package/src/UserHelper.ts +0 -62
  63. package/src/contentProviders/ContentProvider.ts +0 -15
  64. package/src/contentProviders/LessonsContentProvider.ts +0 -279
  65. package/src/contentProviders/index.ts +0 -2
  66. package/src/index.ts +0 -16
  67. package/src/interfaces/Access.ts +0 -138
  68. package/src/interfaces/Attendance.ts +0 -45
  69. package/src/interfaces/Content.ts +0 -84
  70. package/src/interfaces/Doing.ts +0 -139
  71. package/src/interfaces/Donation.ts +0 -193
  72. package/src/interfaces/Error.ts +0 -17
  73. package/src/interfaces/Lessons.ts +0 -61
  74. package/src/interfaces/Membership.ts +0 -184
  75. package/src/interfaces/Messaging.ts +0 -96
  76. package/src/interfaces/Permissions.ts +0 -92
  77. package/src/interfaces/Reporting.ts +0 -41
  78. package/src/interfaces/UserContextInterface.ts +0 -13
  79. package/src/interfaces/index.ts +0 -14
  80. package/tsconfig.json +0 -36
package/src/DateHelper.ts DELETED
@@ -1,183 +0,0 @@
1
- import dayjs from "dayjs"
2
- import utc from "dayjs/plugin/utc.js";
3
- import customParseFormat from "dayjs/plugin/customParseFormat.js";
4
-
5
- // Extend dayjs with plugins
6
- dayjs.extend(utc);
7
- dayjs.extend(customParseFormat);
8
-
9
- export class DateHelper {
10
-
11
- //Fixes timezone issues when you just need the date.
12
- static toDate(input: any) {
13
- const str = input.toString();
14
- // Check if it's a YYYY-MM-DD format (HTML5 date input)
15
- const dateOnlyMatch = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
16
- if (dateOnlyMatch) {
17
- // Parse as local date at noon to avoid timezone issues
18
- const [, year, month, day] = dateOnlyMatch;
19
- return new Date(parseInt(year), parseInt(month) - 1, parseInt(day), 12, 0, 0);
20
- }
21
- // For other formats, use existing behavior
22
- return new Date(Date.parse(str.replace("Z", "")));
23
- }
24
-
25
- static toDateTime(input: any) {
26
- return new Date(Date.parse(input.toString()));
27
- }
28
-
29
- //obsolete. Do not use
30
- static convertToDate(input: any) {
31
- return this.toDateTime(input);
32
- }
33
-
34
- static addDays(date: Date, days: number) {
35
- const result = new Date(date.getTime());
36
- result.setDate(result.getDate() + days);
37
- return result;
38
- }
39
-
40
- static prettyDate(date: Date) {
41
- if (date === undefined || date === null) return "";
42
- return this.formatDateTime(date, "MMM d, yyyy");
43
- }
44
-
45
- static prettyDateTime(date: Date) {
46
- if (date === undefined || date === null) return "";
47
- return this.formatDateTime(date, "MMM d, yyyy h:mm a");
48
- }
49
-
50
- static prettyTime(date: Date) {
51
- if (date === undefined || date === null) return "";
52
- return this.formatDateTime(date, "h:mm a");
53
- }
54
-
55
- static getLastSunday() {
56
- let result = new Date();
57
- while (result.getDay() !== 0) result.setDate(result.getDate() - 1);
58
- return result;
59
- }
60
-
61
- static getNextSunday() {
62
- let result = this.getLastSunday();
63
- result.setDate(result.getDate() + 7);
64
- return result;
65
- }
66
-
67
- static getWeekSunday(year: number, week: number) {
68
- let result = new Date(year, 0, 1);
69
- while (result.getDay() !== 0) result.setDate(result.getDate() + 1);
70
- result.setDate(result.getDate() + ((week - 1) * 7));
71
- return result;
72
- }
73
-
74
- static formatHtml5Date(date: Date | string | null | undefined): string {
75
- if (!date) return "";
76
-
77
- // If already a YYYY-MM-DD string, return as-is
78
- if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) return date;
79
-
80
- // If ISO string, extract date portion (ignore timezone)
81
- if (typeof date === 'string') {
82
- const match = date.match(/^(\d{4}-\d{2}-\d{2})/);
83
- if (match) return match[1];
84
- }
85
-
86
- // For Date objects, use LOCAL year/month/day (not UTC)
87
- const d = date instanceof Date ? date : new Date(date);
88
- if (isNaN(d.getTime())) return "";
89
-
90
- const year = d.getFullYear();
91
- const month = String(d.getMonth() + 1).padStart(2, '0');
92
- const day = String(d.getDate()).padStart(2, '0');
93
- return `${year}-${month}-${day}`;
94
- }
95
-
96
- // For DATE-only fields - preserves calendar date without timezone conversion
97
- static toMysqlDateOnly(date: Date | string | null | undefined): string | null {
98
- if (date === null || date === undefined) return null;
99
-
100
- if (typeof date === 'string') {
101
- const match = date.match(/^(\d{4}-\d{2}-\d{2})/);
102
- return match ? match[1] : null;
103
- }
104
-
105
- const year = date.getFullYear();
106
- const month = String(date.getMonth() + 1).padStart(2, '0');
107
- const day = String(date.getDate()).padStart(2, '0');
108
- return `${year}-${month}-${day}`;
109
- }
110
-
111
- static formatHtml5Time(time: Date): string {
112
- if (time === undefined || time === null) return "";
113
- let h = time.getHours();
114
- let m = time.getMinutes();
115
- let s = time.getSeconds();
116
- return `${h < 10 ? ("0" + h) : h}:${m < 10 ? ("0" + m) : m}:${s < 10 ? ("0" + s) : s}`;
117
- }
118
-
119
- static formatHtml5DateTime(date: Date): string {
120
- if (date === undefined || date === null) return "";
121
- else {
122
- return this.formatDateTime(date, "yyyy-MM-dd") + "T" + this.formatDateTime(date, "HH:mm");
123
- }
124
- }
125
-
126
- static getDisplayDuration(d: Date): string {
127
- let seconds = Math.round((new Date().getTime() - d.getTime()) / 1000);
128
- if (seconds > 86400) {
129
- let days = Math.floor(seconds / 86400);
130
- return (days === 1) ? "1d" : days.toString() + "d";
131
- }
132
- else if (seconds > 3600) {
133
- let hours = Math.floor(seconds / 3600);
134
- return (hours === 1) ? "1h" : hours.toString() + "h";
135
- }
136
- else if (seconds > 60) {
137
- let minutes = Math.floor(seconds / 60);
138
- return (minutes === 1) ? "1m" : minutes.toString() + "m";
139
- }
140
- else return (seconds === 1) ? "1s" : Math.floor(seconds).toString() + "s";
141
- }
142
-
143
- static getShortDate(d: Date): string {
144
- return (d.getMonth() + 1).toString() + "/" + (d.getDate()).toString() + "/" + d.getFullYear().toString();
145
- }
146
-
147
- static convertDatePickerFormat(d: Date): Date {
148
- const date = this.formatHtml5Date(d).split("-");
149
- if (date.length === 3) return new Date(`${date[1]}-${date[2]}-${date[0]}`);
150
- return new Date();
151
- }
152
-
153
- private static formatDateTime(date: Date, format: string) {
154
- try {
155
- // Convert date-fns format to dayjs format
156
- const dayjsFormat = format
157
- .replace(/yyyy/g, 'YYYY')
158
- .replace(/yy/g, 'YY')
159
- .replace(/d/g, 'D')
160
- .replace(/DD/g, 'DD')
161
- .replace(/a/g, 'A');
162
-
163
- return dayjs(date).format(dayjsFormat);
164
- } catch { return ""; }
165
- }
166
-
167
- public static toMysqlDate(d: Date) {
168
- if (d === null || d === undefined) {
169
- return undefined;
170
- }
171
- return dayjs(d).format("YYYY-MM-DD HH:mm:ss")
172
- }
173
-
174
- public static subtractHoursFromNow(hour: number) {
175
- const now = new Date();
176
- return new Date(now.setHours(now.getHours() - hour));
177
- }
178
-
179
- public static toUTCDate(d: Date) {
180
- return dayjs(d).utc().format("YYYY-MM-DD HH:mm:ss");
181
- }
182
-
183
- }
@@ -1,26 +0,0 @@
1
- export class DonationHelper {
2
-
3
- static getInterval(intervalName:string) {
4
- let intervalCount = 1;
5
- let intervalType = "month";
6
- let parts = intervalName.split("_");
7
- if (parts.length === 2) {
8
- switch (parts[0])
9
- {
10
- case "two": intervalCount = 2; break;
11
- case "three": intervalCount = 3; break;
12
- }
13
- intervalType = parts[1];
14
- }
15
- let result = { interval_count: intervalCount, interval: intervalType };
16
- return result;
17
- }
18
-
19
- static getIntervalKeyName(intervalCount: number, intervalType: string) {
20
- let firstPart = "one";
21
- if (intervalCount === 2) firstPart = "two";
22
- else if (intervalCount === 3) firstPart = "three";
23
- return firstPart + "_" + intervalType;
24
- }
25
-
26
- }
@@ -1,39 +0,0 @@
1
- import { ErrorLogInterface, ErrorAppDataInterface } from "./interfaces/Error.js";
2
- import { ApiHelper } from "./ApiHelper.js";
3
-
4
-
5
- export class ErrorHelper {
6
-
7
- static getAppData: () => { churchId: string, userId: string, originUrl: string, application: string };
8
- static customErrorHandler: (errorLog: ErrorLogInterface) => void;
9
-
10
- static init = (getAppData: () => ErrorAppDataInterface, customErrorHandler: (errorLog: ErrorLogInterface) => void) => {
11
- ErrorHelper.getAppData = getAppData;
12
- ErrorHelper.customErrorHandler = customErrorHandler;
13
- }
14
-
15
- static logError = (errorType: string, message: string, details: string) => {
16
-
17
-
18
- if (this.getAppData) {
19
- const data = this.getAppData();
20
- const log: ErrorLogInterface = {
21
- application: data.application,
22
- errorTime: new Date(),
23
- userId: data.userId,
24
- churchId: data.churchId,
25
- originUrl: data.originUrl,
26
- errorType: errorType,
27
- message: message,
28
- details: details
29
- }
30
-
31
- if (log.errorType === "401" && log.message.indexOf("/users/login") > -1) return;
32
- //temporarily disabled error logging
33
- //ApiHelper.postAnonymous("/errors", [log], "MembershipApi");
34
- if (ErrorHelper.customErrorHandler) ErrorHelper.customErrorHandler(log);
35
- }
36
- }
37
-
38
-
39
- }
@@ -1,84 +0,0 @@
1
- import { EventInterface } from "./interfaces/index.js";
2
-
3
- // Lazy initialization to handle both bundlers and Node.js ESM
4
- let RRule: any = null;
5
- let initPromise: Promise<void> | null = null;
6
-
7
- const ensureRRule = () => {
8
- if (RRule) return Promise.resolve();
9
- if (!initPromise) {
10
- initPromise = import("rrule").then((mod) => {
11
- RRule = (mod as any).RRule ?? (mod as any).default?.RRule ?? mod;
12
- });
13
- }
14
- return initPromise;
15
- };
16
-
17
- // Synchronous getter for RRule - throws if not initialized
18
- const getRRule = () => {
19
- if (!RRule) {
20
- throw new Error("EventHelper: RRule not initialized. Ensure ensureRRule() is called first.");
21
- }
22
- return RRule;
23
- };
24
-
25
- // Initialize on first use for bundlers (they process the dynamic import statically)
26
- // The promise is created immediately to start loading
27
- initPromise = import("rrule").then((mod) => {
28
- RRule = (mod as any).RRule ?? (mod as any).default?.RRule ?? mod;
29
- }).catch(() => {
30
- // rrule not available - EventHelper methods will fail gracefully
31
- });
32
-
33
- // Define ParsedOptions as any since RRule is dynamically loaded
34
- type ParsedOptions = any;
35
-
36
- export class EventHelper {
37
-
38
- static getRange = (event:EventInterface, startDate:Date, endDate:Date) => {
39
- const start = new Date(event.start);
40
- const rrule = EventHelper.getFullRRule(event);
41
-
42
- const dates = rrule.between(startDate, endDate);
43
-
44
- dates.forEach((d: Date) => {
45
- d.setHours(start.getHours());
46
- d.setMinutes(start.getMinutes());
47
- d.setSeconds(start.getSeconds());
48
- })
49
- return dates;
50
- }
51
-
52
- static getFullRRule = (event:EventInterface) => {
53
- const RR = getRRule();
54
- let rrule = RR.fromString(event.recurrenceRule);
55
- rrule.options.dtstart = new Date(event.start);
56
- return rrule;
57
- }
58
-
59
- static removeExcludeDates = (events:EventInterface[]) => {
60
- for (let i=events.length-1; i>=0; i--) {
61
- if (events[i].exceptionDates?.length>0)
62
- {
63
- const parsedDates = events[i].exceptionDates.map((d: string | Date)=>new Date(d).toISOString());
64
- if (parsedDates.indexOf(events[i].start.toISOString())>-1) events.splice(i,1);
65
- }
66
- }
67
- }
68
-
69
- static getPartialRRuleString = (options:ParsedOptions) => {
70
- const RR = getRRule();
71
- const parts = new RR(options).toString().split("RRULE:");
72
- const result = parts.length===2 ? parts[1] : "";
73
- return result;
74
- }
75
-
76
- static cleanRule = (options:ParsedOptions) => {
77
- options.byhour = undefined;
78
- options.byminute = undefined;
79
- options.bysecond = undefined;
80
- }
81
-
82
- // Export for consumers who need to ensure initialization
83
- static ensureInitialized = ensureRRule;
84
- }
package/src/FileHelper.ts DELETED
@@ -1,55 +0,0 @@
1
- export class FileHelper {
2
-
3
- static postPresignedFile = (presigned: any, uploadedFile: File, progressCallback: (percent: number) => void) => {
4
- const formData = new FormData();
5
- formData.append("key", presigned.key);
6
- formData.append("acl", "public-read");
7
- formData.append("Content-Type", uploadedFile.type);
8
- for (const property in presigned.fields) formData.append(property, presigned.fields[property]);
9
- formData.append("file", uploadedFile);
10
-
11
- // Use XMLHttpRequest for upload progress tracking since fetch doesn't support it natively
12
- return new Promise((resolve, reject) => {
13
- const xhr = new XMLHttpRequest();
14
-
15
- xhr.upload.addEventListener('progress', (event) => {
16
- if (event.lengthComputable) {
17
- const percent = Math.round((event.loaded / event.total) * 100);
18
- progressCallback(percent);
19
- }
20
- });
21
-
22
- xhr.addEventListener('load', () => {
23
- if (xhr.status >= 200 && xhr.status < 300) {
24
- resolve({
25
- status: xhr.status,
26
- statusText: xhr.statusText,
27
- data: xhr.responseText
28
- });
29
- } else {
30
- reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
31
- }
32
- });
33
-
34
- xhr.addEventListener('error', () => {
35
- reject(new Error('Network error occurred'));
36
- });
37
-
38
- xhr.open('POST', presigned.url);
39
- xhr.send(formData);
40
- });
41
- };
42
-
43
- static dataURLtoBlob(dataurl: string) {
44
- let arr = dataurl.split(",");
45
- if (arr.length < 2) throw new Error("Invalid data URL format");
46
- let mimeMatch = arr[0].match(/:(.*?);/);
47
- if (!mimeMatch) throw new Error("Invalid MIME type in data URL");
48
- let mime = mimeMatch[1];
49
- let bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
50
- while (n--) {
51
- u8arr[n] = bstr.charCodeAt(n);
52
- }
53
- return new Blob([u8arr], { type: mime });
54
- }
55
- }
@@ -1,82 +0,0 @@
1
- import { PersonInterface, ContactInfoInterface } from "./interfaces/index.js";
2
- import { CommonEnvironmentHelper } from "./CommonEnvironmentHelper.js";
3
-
4
- export class PersonHelper {
5
-
6
- public static getPhotoPath(churchId: string, person: any) {
7
- if (!person.photoUpdated) {
8
- if (person.id?.startsWith("PER0000")) return "https://app.staging.b1.church/images/demo/avatars/" + person.id + ".svg";
9
- else return "";
10
- }
11
- else return "/" + churchId + "/membership/people/" + person.id + ".png?dt=" + new Date(person.photoUpdated).getTime().toString();
12
- }
13
-
14
- static getPhotoUrl(person: PersonInterface) {
15
- if (!person?.photo) return "/images/sample-profile.png"
16
- else {
17
- return (person?.photo?.startsWith("data:image/png;base64,") || person.photo?.indexOf("://") > -1)
18
- ? person.photo
19
- : CommonEnvironmentHelper.ContentRoot + person.photo;
20
- }
21
- }
22
-
23
- static getAge(birthdate: Date): string {
24
- if (birthdate !== undefined && birthdate !== null) {
25
- let ageDifMs = Date.now() - new Date(birthdate).getTime();
26
- let ageDate = new Date(ageDifMs);
27
- let years = Math.abs(ageDate.getUTCFullYear() - 1970);
28
- return years + " years";
29
- }
30
- else return "";
31
- }
32
-
33
- public static getDisplayNameFromPerson(person: any) {
34
- if (person?.name?.nick !== null && person?.name?.nick !== "" && person?.name?.nick !== undefined) return person.name.first + " \"" + person.name.nick + "\" " + person.name.last;
35
- else return person.name.first + " " + person.name.last;
36
- }
37
-
38
- static getDisplayName(firstName: string, lastName: string, nickName: string): string {
39
- if (nickName !== undefined && nickName !== null && nickName.length > 0) return firstName + ' "' + nickName + '" ' + lastName;
40
- else return firstName + " " + lastName;
41
- }
42
-
43
- public static getBirthMonth(birthdate: Date): number {
44
- if (birthdate) return new Date(birthdate).getMonth() + 1;
45
- else return -1;
46
- }
47
-
48
- public static compareAddress(address1: ContactInfoInterface, address2: ContactInfoInterface): boolean {
49
- const displayAddress1: string = this.addressToString(address1).trim();
50
- const displayAddress2: string = this.addressToString(address2).trim();
51
-
52
- if (displayAddress1 !== displayAddress2) {
53
- return true
54
- }
55
- return false
56
- }
57
-
58
- public static addressToString(address: ContactInfoInterface): string {
59
- return `${address.address1 || ""} ${address.address2 || ""} ${address.city || ""}${(address.city && address.state) ? "," : ""} ${address.state || ""} ${address.zip || ""}`
60
- }
61
-
62
- public static changeOnlyAddress(contactInfo1: ContactInfoInterface, contactInfo2: ContactInfoInterface): ContactInfoInterface {
63
- const updatedAddress: ContactInfoInterface = {
64
- ...contactInfo1,
65
- address1: contactInfo2.address1,
66
- address2: contactInfo2.address2,
67
- city: contactInfo2.city,
68
- state: contactInfo2.state,
69
- zip: contactInfo2.zip
70
- }
71
-
72
- return updatedAddress
73
- }
74
-
75
- public static checkAddressAvailabilty(person: PersonInterface): boolean {
76
- const addressString: string = this.addressToString(person.contactInfo).trim();
77
- if (addressString !== "") {
78
- return true;
79
- }
80
- return false;
81
- }
82
- }
package/src/PlanHelper.ts DELETED
@@ -1,137 +0,0 @@
1
- import type { PlanInterface, PlanItemInterface, PlanItemContentInterface } from "./interfaces/index.js";
2
- import type { ContentProviderInterface } from "./contentProviders/ContentProvider.js";
3
- import { LessonsContentProvider } from "./contentProviders/LessonsContentProvider.js";
4
-
5
- export class PlanHelper {
6
- private static providers: ContentProviderInterface[] = [
7
- new LessonsContentProvider()
8
- ];
9
-
10
- // Register additional content providers
11
- static registerProvider(provider: ContentProviderInterface): void {
12
- // Avoid duplicates
13
- if (!this.providers.find(p => p.providerId === provider.providerId)) {
14
- this.providers.push(provider);
15
- }
16
- }
17
-
18
- // Replace a provider (useful for configuring with different URLs)
19
- static replaceProvider(provider: ContentProviderInterface): void {
20
- const index = this.providers.findIndex(p => p.providerId === provider.providerId);
21
- if (index >= 0) {
22
- this.providers[index] = provider;
23
- } else {
24
- this.providers.push(provider);
25
- }
26
- }
27
-
28
- // Main method: populate planItems with their content
29
- static async populate(
30
- plan: PlanInterface,
31
- planItems: PlanItemInterface[]
32
- ): Promise<PlanItemInterface[]> {
33
- // Flatten hierarchy to get all items
34
- const allItems = this.flattenItems(planItems);
35
-
36
- // Group items by provider
37
- const itemsByProvider = new Map<ContentProviderInterface, PlanItemInterface[]>();
38
-
39
- for (const item of allItems) {
40
- for (const provider of this.providers) {
41
- if (provider.canHandle(plan, item)) {
42
- const existing = itemsByProvider.get(provider) || [];
43
- existing.push(item);
44
- itemsByProvider.set(provider, existing);
45
- break; // First matching provider wins
46
- }
47
- }
48
- }
49
-
50
- // Fetch content from each provider in parallel
51
- const fetchPromises: Promise<void>[] = [];
52
- const contentMap = new Map<string, PlanItemContentInterface>();
53
-
54
- for (const [provider, items] of itemsByProvider) {
55
- fetchPromises.push(
56
- provider.fetchContent(plan, items).then(providerContent => {
57
- for (const [itemId, content] of providerContent) {
58
- contentMap.set(itemId, content);
59
- }
60
- })
61
- );
62
- }
63
-
64
- await Promise.all(fetchPromises);
65
-
66
- // Attach content to items (mutates in place for efficiency)
67
- this.attachContent(planItems, contentMap);
68
-
69
- return planItems;
70
- }
71
-
72
- // Flatten nested planItems for processing
73
- private static flattenItems(items: PlanItemInterface[]): PlanItemInterface[] {
74
- const result: PlanItemInterface[] = [];
75
-
76
- const collect = (itemList: PlanItemInterface[]) => {
77
- for (const item of itemList) {
78
- result.push(item);
79
- if (item.children?.length) {
80
- collect(item.children);
81
- }
82
- }
83
- };
84
-
85
- collect(items);
86
- return result;
87
- }
88
-
89
- // Attach content to items recursively
90
- private static attachContent(
91
- items: PlanItemInterface[],
92
- contentMap: Map<string, PlanItemContentInterface>
93
- ): void {
94
- for (const item of items) {
95
- const content = contentMap.get(item.id);
96
- if (content) {
97
- item.content = content;
98
- }
99
- if (item.children?.length) {
100
- this.attachContent(item.children, contentMap);
101
- }
102
- }
103
- }
104
-
105
- // Convenience: Get the lessons provider for direct lesson operations
106
- static getLessonsProvider(): LessonsContentProvider {
107
- return this.providers.find(p => p.providerId === "lessons") as LessonsContentProvider;
108
- }
109
-
110
- // ============================================
111
- // Time Formatting Utilities
112
- // ============================================
113
-
114
- /**
115
- * Format seconds as MM:SS string
116
- * @param seconds - Number of seconds to format
117
- * @returns Formatted time string (e.g., "3:45")
118
- */
119
- static formatTime(seconds: number): string {
120
- const mins = Math.floor(seconds / 60);
121
- const secs = seconds % 60;
122
- return mins + ":" + (secs < 10 ? "0" : "") + secs;
123
- }
124
-
125
- /**
126
- * Calculate total duration of a section's children
127
- * @param section - PlanItem with children
128
- * @returns Total seconds from all children
129
- */
130
- static getSectionDuration(section: PlanItemInterface): number {
131
- let totalSeconds = 0;
132
- section.children?.forEach((child) => {
133
- totalSeconds += child.seconds || 0;
134
- });
135
- return totalSeconds;
136
- }
137
- }
@@ -1,36 +0,0 @@
1
- export class UniqueIdHelper {
2
-
3
- static chars = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
4
- "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
5
- "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
6
- "-", "_"];
7
-
8
- static alphanumericChars = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
9
- "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
10
- "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
11
-
12
- public static isMissing(obj: any) {
13
- if (obj === undefined || obj === null) return true;
14
- else if (obj.toString() === "") return true;
15
- else return false;
16
- }
17
-
18
- public static shortId() {
19
- return this.generate(UniqueIdHelper.chars, 11);
20
- }
21
-
22
- public static generateAlphanumeric() {
23
- return this.generate(UniqueIdHelper.alphanumericChars, 11);
24
- }
25
-
26
- public static generate(charList: string[], length: number) {
27
- let result = "";
28
- for (let i = 0; i < length; i++) {
29
- const idx = Math.floor(Math.random() * charList.length);
30
- const c = charList[idx];
31
- result += c;
32
- }
33
- return result;
34
- }
35
-
36
- }