UNPKG

@crescender/calendar

Version:

A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.

1,589 lines (1,582 loc) 43.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/shared/utils.ts function formatDateAustralian(date) { const months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; const day = date.getUTCDate().toString().padStart(2, "0"); const month = months[date.getUTCMonth()]; const year = date.getUTCFullYear(); return `${day}/${month}/${year}`; } __name(formatDateAustralian, "formatDateAustralian"); function getDurationMinutes(start, end) { return Math.round((end.getTime() - start.getTime()) / (1e3 * 60)); } __name(getDurationMinutes, "getDurationMinutes"); function isSameDay(date1, date2) { return date1.toDateString() === date2.toDateString(); } __name(isSameDay, "isSameDay"); function getStartOfWeek(date) { const result = new Date(date); const day = result.getDay(); const diff = result.getDate() - day + (day === 0 ? -6 : 1); result.setDate(diff); result.setHours(0, 0, 0, 0); return result; } __name(getStartOfWeek, "getStartOfWeek"); function getEndOfWeek(date) { const result = new Date(date); const day = result.getDay(); const diff = result.getDate() - day + (day === 0 ? 0 : 7); result.setDate(diff); result.setHours(23, 59, 59, 999); return result; } __name(getEndOfWeek, "getEndOfWeek"); function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } __name(isValidEmail, "isValidEmail"); function isValidPhone(phone) { const phoneRegex = /^(\+61|0)[2-9]\d{8}$/; return phoneRegex.test(phone.replace(/\s/g, "")); } __name(isValidPhone, "isValidPhone"); function generateTempId() { return `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } __name(generateTempId, "generateTempId"); // src/shared/constants.ts var DEFAULT_CURRENCY = "AUD"; var MAX_EVENT_DURATION_HOURS = 24; var MAX_RECURRENCE_OCCURRENCES = 365; var DEFAULT_EVENT_DURATION_MINUTES = 60; var MAX_LENGTHS = { EVENT_TITLE: 200, EVENT_DESCRIPTION: 1e3, CALENDAR_NAME: 100, CALENDAR_DESCRIPTION: 500, VENUE_NAME: 200, VENUE_ADDRESS: 300, VENUE_CITY: 100, CONTACT_NAME: 200, CONTACT_ROLE: 100, NOTES: 1e3, INCOME_DESCRIPTION: 200, EXPENSE_DESCRIPTION: 200, RECEIPT_PATH: 500 }; var DATE_FORMATS = { AUSTRALIAN: "DD/MM/YYYY", AMERICAN: "MM/DD/YYYY", ISO: "YYYY-MM-DD" }; var TIME_FORMATS = { TWELVE_HOUR: "12h", TWENTY_FOUR_HOUR: "24h" }; var CALENDAR_VIEWS = { DAY: "day", WEEK: "week", MONTH: "month", LIST: "list" }; var DEFAULT_START_OF_WEEK = 1; var AUSTRALIAN_STATES = { NSW: "New South Wales", VIC: "Victoria", QLD: "Queensland", WA: "Western Australia", SA: "South Australia", TAS: "Tasmania", ACT: "Australian Capital Territory", NT: "Northern Territory" }; var CURRENCIES = { AUD: "Australian Dollar", USD: "US Dollar", EUR: "Euro", GBP: "British Pound", CAD: "Canadian Dollar", NZD: "New Zealand Dollar" }; var VALIDATION_PATTERNS = { EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, PHONE_AU: /^(\+61|0)[2-478](?:[ -]?[0-9]){8}$/, CURRENCY_CODE: /^[A-Z]{3}$/, HEX_COLOR: /^#[0-9A-F]{6}$/i, URL: /^https?:\/\/.+/ }; var API_CONFIG = { DEFAULT_PAGE_SIZE: 20, MAX_PAGE_SIZE: 100, REQUEST_TIMEOUT: 3e4, RETRY_ATTEMPTS: 3 }; var FILE_UPLOAD = { MAX_SIZE_MB: 10, ALLOWED_TYPES: [ "image/jpeg", "image/png", "image/gif", "application/pdf" ], ALLOWED_EXTENSIONS: [ ".jpg", ".jpeg", ".png", ".gif", ".pdf" ] }; // src/shared/enums.ts var EVENT_TYPES = { GIG: "gig", LESSON: "lesson", AUDITION: "audition", PRACTICE: "practice", REHEARSAL: "rehearsal", RECORDING: "recording", MEETING: "meeting" }; var PAYMENT_STATUS = { PENDING: "Pending", PAID: "Paid", OVERDUE: "Overdue", CANCELLED: "Cancelled" }; var EVENT_STATUS = { CONFIRMED: "Confirmed", TENTATIVE: "Tentative", CANCELLED: "Cancelled", COMPLETED: "Completed" }; var CALENDAR_TYPES = { INDIVIDUAL: "individual", GROUP: "group", SHARED: "shared" }; var STUDENT_LEVELS = { BEGINNER: "Beginner", INTERMEDIATE: "Intermediate", ADVANCED: "Advanced", PROFESSIONAL: "Professional" }; var DIFFICULTY_LEVELS = { EASY: "Easy", MEDIUM: "Medium", HARD: "Hard", EXPERT: "Expert" }; var GENRES = { CLASSICAL: "Classical", JAZZ: "Jazz", ROCK: "Rock", POP: "Pop", BLUES: "Blues", COUNTRY: "Country", FOLK: "Folk", ELECTRONIC: "Electronic", WORLD: "World Music", OTHER: "Other" }; var INSTRUMENTS = { PIANO: "Piano", GUITAR: "Guitar", VIOLIN: "Violin", DRUMS: "Drums", BASS: "Bass", SAXOPHONE: "Saxophone", TRUMPET: "Trumpet", FLUTE: "Flute", CELLO: "Cello", VOICE: "Voice", OTHER: "Other" }; // src/server/database/entities.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn, JoinColumn } from "typeorm"; function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } __name(_ts_decorate, "_ts_decorate"); function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } __name(_ts_metadata, "_ts_metadata"); var _Calendar = class _Calendar { constructor() { __publicField(this, "id"); __publicField(this, "name"); __publicField(this, "description"); /** * The type of calendar. * 'individual' - A personal calendar for a single user. * 'group' - A shared calendar for a team or band. */ __publicField(this, "type"); } }; __name(_Calendar, "Calendar"); var Calendar = _Calendar; _ts_decorate([ PrimaryGeneratedColumn("uuid"), _ts_metadata("design:type", String) ], Calendar.prototype, "id", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], Calendar.prototype, "name", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Calendar.prototype, "description", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], Calendar.prototype, "type", void 0); Calendar = _ts_decorate([ Entity() ], Calendar); var _Venue = class _Venue { constructor() { __publicField(this, "id"); __publicField(this, "name"); __publicField(this, "address"); __publicField(this, "city"); __publicField(this, "state"); __publicField(this, "country"); __publicField(this, "website"); __publicField(this, "contactName"); __publicField(this, "contactEmail"); __publicField(this, "contactPhone"); __publicField(this, "notes"); } }; __name(_Venue, "Venue"); var Venue = _Venue; _ts_decorate([ PrimaryGeneratedColumn("uuid"), _ts_metadata("design:type", String) ], Venue.prototype, "id", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], Venue.prototype, "name", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "address", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "city", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "state", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "country", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "website", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "contactName", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "contactEmail", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "contactPhone", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Venue.prototype, "notes", void 0); Venue = _ts_decorate([ Entity() ], Venue); var _Contact = class _Contact { constructor() { __publicField(this, "id"); __publicField(this, "name"); __publicField(this, "email"); __publicField(this, "phone"); __publicField(this, "role"); __publicField(this, "notes"); } }; __name(_Contact, "Contact"); var Contact = _Contact; _ts_decorate([ PrimaryGeneratedColumn("uuid"), _ts_metadata("design:type", String) ], Contact.prototype, "id", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], Contact.prototype, "name", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Contact.prototype, "email", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Contact.prototype, "phone", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Contact.prototype, "role", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Contact.prototype, "notes", void 0); Contact = _ts_decorate([ Entity() ], Contact); var _Event = class _Event { constructor() { __publicField(this, "id"); __publicField(this, "summary"); __publicField(this, "description"); __publicField(this, "start"); __publicField(this, "end"); /** * An RFC 5545 recurrence rule string. * @see https://icalendar.org/rrule-tool.html * @example 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20251231T235959Z' */ __publicField(this, "recurrenceRule"); /** * Event type: 'gig', 'lesson', 'audition', 'practice', 'rehearsal', 'recording', 'meeting', etc. */ __publicField(this, "type"); // Musician-specific fields __publicField(this, "genre"); __publicField(this, "instrument"); __publicField(this, "difficulty"); __publicField(this, "repertoire"); __publicField(this, "setList"); __publicField(this, "equipmentNeeded"); __publicField(this, "dresscode"); __publicField(this, "soundcheckTime"); __publicField(this, "loadInTime"); __publicField(this, "paymentStatus"); __publicField(this, "paymentDueDate"); __publicField(this, "studentLevel"); __publicField(this, "lessonFocus"); __publicField(this, "auditionPiece"); __publicField(this, "auditionRequirements"); __publicField(this, "practiceGoals"); __publicField(this, "rehearsalNotes"); __publicField(this, "status"); __publicField(this, "createdAt"); __publicField(this, "updatedAt"); // Relationships __publicField(this, "calendar"); __publicField(this, "venue"); __publicField(this, "primaryContact"); __publicField(this, "income"); __publicField(this, "expenses"); } }; __name(_Event, "Event"); var Event = _Event; _ts_decorate([ PrimaryGeneratedColumn("uuid"), _ts_metadata("design:type", String) ], Event.prototype, "id", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], Event.prototype, "summary", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "description", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "start", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "end", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "recurrenceRule", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], Event.prototype, "type", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "genre", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "instrument", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "difficulty", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "repertoire", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "setList", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "equipmentNeeded", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "dresscode", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "soundcheckTime", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "loadInTime", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "paymentStatus", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "paymentDueDate", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "studentLevel", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "lessonFocus", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "auditionPiece", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "auditionRequirements", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "practiceGoals", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "rehearsalNotes", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], Event.prototype, "status", void 0); _ts_decorate([ CreateDateColumn(), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "createdAt", void 0); _ts_decorate([ UpdateDateColumn(), _ts_metadata("design:type", typeof Date === "undefined" ? Object : Date) ], Event.prototype, "updatedAt", void 0); _ts_decorate([ ManyToOne(() => Calendar), JoinColumn(), _ts_metadata("design:type", typeof Calendar === "undefined" ? Object : Calendar) ], Event.prototype, "calendar", void 0); _ts_decorate([ ManyToOne(() => Venue, { nullable: true }), JoinColumn(), _ts_metadata("design:type", typeof Venue === "undefined" ? Object : Venue) ], Event.prototype, "venue", void 0); _ts_decorate([ ManyToOne(() => Contact, { nullable: true }), JoinColumn(), _ts_metadata("design:type", typeof Contact === "undefined" ? Object : Contact) ], Event.prototype, "primaryContact", void 0); _ts_decorate([ OneToMany(() => EventIncome, (income) => income.event, { cascade: true }), _ts_metadata("design:type", Array) ], Event.prototype, "income", void 0); _ts_decorate([ OneToMany(() => EventExpense, (expense) => expense.event, { cascade: true }), _ts_metadata("design:type", Array) ], Event.prototype, "expenses", void 0); Event = _ts_decorate([ Entity() ], Event); var _EventIncome = class _EventIncome { constructor() { __publicField(this, "id"); __publicField(this, "description"); __publicField(this, "amount"); __publicField(this, "currency"); __publicField(this, "notes"); __publicField(this, "event"); } }; __name(_EventIncome, "EventIncome"); var EventIncome = _EventIncome; _ts_decorate([ PrimaryGeneratedColumn("uuid"), _ts_metadata("design:type", String) ], EventIncome.prototype, "id", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], EventIncome.prototype, "description", void 0); _ts_decorate([ Column("decimal", { precision: 10, scale: 2 }), _ts_metadata("design:type", Number) ], EventIncome.prototype, "amount", void 0); _ts_decorate([ Column({ default: "AUD" }), _ts_metadata("design:type", String) ], EventIncome.prototype, "currency", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], EventIncome.prototype, "notes", void 0); _ts_decorate([ ManyToOne(() => Event, (event) => event.income), JoinColumn(), _ts_metadata("design:type", typeof Event === "undefined" ? Object : Event) ], EventIncome.prototype, "event", void 0); EventIncome = _ts_decorate([ Entity() ], EventIncome); var _EventExpense = class _EventExpense { constructor() { __publicField(this, "id"); __publicField(this, "description"); __publicField(this, "amount"); __publicField(this, "currency"); __publicField(this, "receipt"); __publicField(this, "notes"); __publicField(this, "event"); } }; __name(_EventExpense, "EventExpense"); var EventExpense = _EventExpense; _ts_decorate([ PrimaryGeneratedColumn("uuid"), _ts_metadata("design:type", String) ], EventExpense.prototype, "id", void 0); _ts_decorate([ Column(), _ts_metadata("design:type", String) ], EventExpense.prototype, "description", void 0); _ts_decorate([ Column("decimal", { precision: 10, scale: 2 }), _ts_metadata("design:type", Number) ], EventExpense.prototype, "amount", void 0); _ts_decorate([ Column({ default: "AUD" }), _ts_metadata("design:type", String) ], EventExpense.prototype, "currency", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], EventExpense.prototype, "receipt", void 0); _ts_decorate([ Column({ nullable: true }), _ts_metadata("design:type", String) ], EventExpense.prototype, "notes", void 0); _ts_decorate([ ManyToOne(() => Event, (event) => event.expenses), JoinColumn(), _ts_metadata("design:type", typeof Event === "undefined" ? Object : Event) ], EventExpense.prototype, "event", void 0); EventExpense = _ts_decorate([ Entity() ], EventExpense); // src/server/services/events.ts import { Between } from "typeorm"; var db; var eventRepository; var calendarRepository; var incomeRepository; var expenseRepository; var venueRepository; var contactRepository; function initDb(dataSource) { db = dataSource; eventRepository = dataSource.getRepository(Event); calendarRepository = dataSource.getRepository(Calendar); incomeRepository = dataSource.getRepository(EventIncome); expenseRepository = dataSource.getRepository(EventExpense); venueRepository = dataSource.getRepository(Venue); contactRepository = dataSource.getRepository(Contact); return db; } __name(initDb, "initDb"); function getDb() { if (!db) { throw new Error("Database has not been initialized. Call initDb() first."); } return db; } __name(getDb, "getDb"); async function fetchChanges(calendarId, since) { const eventRepository2 = getDb().getRepository(Event); const updated = await eventRepository2.find({ where: { calendar: { id: calendarId }, updatedAt: Between(since, /* @__PURE__ */ new Date()) }, relations: [ "calendar", "venue", "primaryContact" ] }); const deleted = []; return { updated: updated.map((event) => ({ id: event.id, summary: event.summary, description: event.description, start: event.start, end: event.end, recurrenceRule: event.recurrenceRule, type: event.type, genre: event.genre, instrument: event.instrument, difficulty: event.difficulty, repertoire: event.repertoire, setList: event.setList, equipmentNeeded: event.equipmentNeeded, dresscode: event.dresscode, soundcheckTime: event.soundcheckTime, loadInTime: event.loadInTime, paymentStatus: event.paymentStatus, paymentDueDate: event.paymentDueDate, studentLevel: event.studentLevel, lessonFocus: event.lessonFocus, auditionPiece: event.auditionPiece, auditionRequirements: event.auditionRequirements, practiceGoals: event.practiceGoals, rehearsalNotes: event.rehearsalNotes, status: event.status, createdAt: event.createdAt, updatedAt: event.updatedAt, calendarId: event.calendar.id, venueId: event.venue?.id, primaryContactId: event.primaryContact?.id })), deleted }; } __name(fetchChanges, "fetchChanges"); async function createEvent(calendarId, eventData) { const eventRepository2 = getDb().getRepository(Event); const calendarRepository2 = getDb().getRepository(Calendar); const calendar = await calendarRepository2.findOneBy({ id: calendarId }); if (!calendar) { throw new Error(`Calendar with ID "${calendarId}" not found.`); } const newEvent = eventRepository2.create({ ...eventData, calendar }); return eventRepository2.save(newEvent); } __name(createEvent, "createEvent"); async function updateEvent(eventId, updates) { const eventRepository2 = getDb().getRepository(Event); await eventRepository2.update(eventId, updates); const updatedEvent = await eventRepository2.findOneBy({ id: eventId }); if (!updatedEvent) { throw new Error(`Event with ID "${eventId}" not found.`); } return updatedEvent; } __name(updateEvent, "updateEvent"); async function deleteEvent(eventId) { const eventRepository2 = getDb().getRepository(Event); await eventRepository2.delete({ id: eventId }); } __name(deleteEvent, "deleteEvent"); async function getEventsByCalendar(calendarId) { const eventRepository2 = getDb().getRepository(Event); return eventRepository2.find({ where: { calendar: { id: calendarId } }, relations: [ "calendar", "venue", "primaryContact", "income", "expenses" ] }); } __name(getEventsByCalendar, "getEventsByCalendar"); async function getEventsByType(calendarId, type) { const eventRepository2 = getDb().getRepository(Event); return eventRepository2.find({ where: { calendar: { id: calendarId }, type }, relations: [ "calendar", "venue", "primaryContact", "income", "expenses" ], order: { start: "ASC" } }); } __name(getEventsByType, "getEventsByType"); async function getUpcomingGigs(calendarId, limit = 10) { const eventRepository2 = getDb().getRepository(Event); const now = /* @__PURE__ */ new Date(); return eventRepository2.find({ where: { calendar: { id: calendarId }, type: "gig", start: Between(now, new Date(now.getTime() + 365 * 24 * 60 * 60 * 1e3)) // Next year }, relations: [ "calendar", "venue", "primaryContact", "income", "expenses" ], order: { start: "ASC" }, take: limit }); } __name(getUpcomingGigs, "getUpcomingGigs"); async function getStudentLessons(calendarId, studentContactId) { const eventRepository2 = getDb().getRepository(Event); return eventRepository2.find({ where: { calendar: { id: calendarId }, type: "lesson", primaryContact: { id: studentContactId } }, relations: [ "calendar", "venue", "primaryContact", "income", "expenses" ], order: { start: "ASC" } }); } __name(getStudentLessons, "getStudentLessons"); async function addEventIncome(eventId, income) { const event = await eventRepository.findOne({ where: { id: eventId } }); if (!event) { throw new Error(`Event with id ${eventId} not found`); } const eventIncome = incomeRepository.create({ ...income, event }); return await incomeRepository.save(eventIncome); } __name(addEventIncome, "addEventIncome"); async function addEventExpense(eventId, expense) { const event = await eventRepository.findOne({ where: { id: eventId } }); if (!event) { throw new Error(`Event with id ${eventId} not found`); } const eventExpense = expenseRepository.create({ ...expense, event }); return await expenseRepository.save(eventExpense); } __name(addEventExpense, "addEventExpense"); async function getEventIncome(eventId) { return await incomeRepository.find({ where: { event: { id: eventId } } }); } __name(getEventIncome, "getEventIncome"); async function getEventExpenses(eventId) { return await expenseRepository.find({ where: { event: { id: eventId } } }); } __name(getEventExpenses, "getEventExpenses"); async function calculateEventIncome(eventId) { const income = await getEventIncome(eventId); return income.reduce((total, item) => total + Number(item.amount), 0); } __name(calculateEventIncome, "calculateEventIncome"); async function calculateEventExpenses(eventId) { const expenses = await getEventExpenses(eventId); return expenses.reduce((total, item) => total + Number(item.amount), 0); } __name(calculateEventExpenses, "calculateEventExpenses"); async function calculateEventProfit(eventId) { const totalIncome = await calculateEventIncome(eventId); const totalExpenses = await calculateEventExpenses(eventId); return totalIncome - totalExpenses; } __name(calculateEventProfit, "calculateEventProfit"); async function getFinancialSummary(eventIds) { let totalIncome = 0; let totalExpenses = 0; for (const eventId of eventIds) { totalIncome += await calculateEventIncome(eventId); totalExpenses += await calculateEventExpenses(eventId); } const netProfit = totalIncome - totalExpenses; const eventCount = eventIds.length; const averageProfitPerEvent = eventCount > 0 ? netProfit / eventCount : 0; return { totalIncome, totalExpenses, netProfit, eventCount, averageProfitPerEvent }; } __name(getFinancialSummary, "getFinancialSummary"); async function updateEventIncome(incomeId, updates) { await incomeRepository.update(incomeId, updates); const updatedIncome = await incomeRepository.findOneBy({ id: incomeId }); if (!updatedIncome) { throw new Error(`Income with ID "${incomeId}" not found.`); } return updatedIncome; } __name(updateEventIncome, "updateEventIncome"); async function updateEventExpense(expenseId, updates) { await expenseRepository.update(expenseId, updates); const updatedExpense = await expenseRepository.findOneBy({ id: expenseId }); if (!updatedExpense) { throw new Error(`Expense with ID "${expenseId}" not found.`); } return updatedExpense; } __name(updateEventExpense, "updateEventExpense"); async function deleteEventIncome(incomeId) { await incomeRepository.delete({ id: incomeId }); } __name(deleteEventIncome, "deleteEventIncome"); async function deleteEventExpense(expenseId) { await expenseRepository.delete({ id: expenseId }); } __name(deleteEventExpense, "deleteEventExpense"); async function createVenue(venue) { const newVenue = venueRepository.create(venue); return await venueRepository.save(newVenue); } __name(createVenue, "createVenue"); async function createContact(contact) { const newContact = contactRepository.create(contact); return await contactRepository.save(newContact); } __name(createContact, "createContact"); // src/server/services/ics.ts import ical from "ical-generator"; async function generateIcs(calendar, calendarId) { const cal = ical({ name: calendar.name, description: calendar.description, timezone: "Australia/Sydney" // Default to Australian timezone }); const events = await getEventsByCalendar(calendarId); events.forEach((event) => { const icalEvent = cal.createEvent({ start: event.start, end: event.end, summary: event.summary, description: event.description, location: event.venue ? [ event.venue.name, event.venue.address, event.venue.city, event.venue.state, event.venue.country ].filter(Boolean).join(", ") : void 0 }); if (event.type) { icalEvent.x("X-EVENT-TYPE", event.type); } if (event.genre) { icalEvent.x("X-GENRE", event.genre); } if (event.instrument) { icalEvent.x("X-INSTRUMENT", event.instrument); } if (event.paymentStatus) { icalEvent.x("X-PAYMENT-STATUS", event.paymentStatus); } if (event.status) { icalEvent.x("X-STATUS", event.status); } if (event.primaryContact) { icalEvent.x("X-CONTACT-NAME", event.primaryContact.name); if (event.primaryContact.email) { icalEvent.x("X-CONTACT-EMAIL", event.primaryContact.email); } if (event.primaryContact.phone) { icalEvent.x("X-CONTACT-PHONE", event.primaryContact.phone); } } if (event.recurrenceRule) { icalEvent.repeating(event.recurrenceRule); } }); return cal.toString(); } __name(generateIcs, "generateIcs"); async function generateIcsByType(calendar, calendarId, eventType) { const cal = ical({ name: `${calendar.name} - ${eventType.charAt(0).toUpperCase() + eventType.slice(1)}s`, description: `${eventType} events from ${calendar.name}`, timezone: "Australia/Sydney" }); const events = await getEventsByCalendar(calendarId); const filteredEvents = events.filter((event) => event.type === eventType); filteredEvents.forEach((event) => { const icalEvent = cal.createEvent({ start: event.start, end: event.end, summary: event.summary, description: event.description, location: event.venue ? [ event.venue.name, event.venue.address, event.venue.city, event.venue.state, event.venue.country ].filter(Boolean).join(", ") : void 0 }); icalEvent.x("X-EVENT-TYPE", event.type); if (event.genre) icalEvent.x("X-GENRE", event.genre); if (event.instrument) icalEvent.x("X-INSTRUMENT", event.instrument); if (event.paymentStatus) icalEvent.x("X-PAYMENT-STATUS", event.paymentStatus); if (event.status) icalEvent.x("X-STATUS", event.status); if (event.primaryContact) { icalEvent.x("X-CONTACT-NAME", event.primaryContact.name); if (event.primaryContact.email) icalEvent.x("X-CONTACT-EMAIL", event.primaryContact.email); if (event.primaryContact.phone) icalEvent.x("X-CONTACT-PHONE", event.primaryContact.phone); } if (event.recurrenceRule) { icalEvent.repeating(event.recurrenceRule); } }); return cal.toString(); } __name(generateIcsByType, "generateIcsByType"); async function generateUpcomingIcs(calendar, calendarId) { const cal = ical({ name: `${calendar.name} - Upcoming Events`, description: `Upcoming events from ${calendar.name}`, timezone: "Australia/Sydney" }); const events = await getEventsByCalendar(calendarId); const now = /* @__PURE__ */ new Date(); const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1e3); const upcomingEvents = events.filter((event) => event.start >= now && event.start <= thirtyDaysFromNow); upcomingEvents.forEach((event) => { const icalEvent = cal.createEvent({ start: event.start, end: event.end, summary: event.summary, description: event.description, location: event.venue ? [ event.venue.name, event.venue.address, event.venue.city, event.venue.state, event.venue.country ].filter(Boolean).join(", ") : void 0 }); icalEvent.x("X-EVENT-TYPE", event.type); if (event.genre) icalEvent.x("X-GENRE", event.genre); if (event.instrument) icalEvent.x("X-INSTRUMENT", event.instrument); if (event.paymentStatus) icalEvent.x("X-PAYMENT-STATUS", event.paymentStatus); if (event.status) icalEvent.x("X-STATUS", event.status); if (event.primaryContact) { icalEvent.x("X-CONTACT-NAME", event.primaryContact.name); if (event.primaryContact.email) icalEvent.x("X-CONTACT-EMAIL", event.primaryContact.email); if (event.primaryContact.phone) icalEvent.x("X-CONTACT-PHONE", event.primaryContact.phone); } if (event.recurrenceRule) { icalEvent.repeating(event.recurrenceRule); } }); return cal.toString(); } __name(generateUpcomingIcs, "generateUpcomingIcs"); // src/server/validation.ts async function validateEventWithDb(data, dataSource, eventId) { const errors = {}; const warnings = {}; if (!data.summary || data.summary.trim().length === 0) { errors.summary = [ "Event title is required" ]; } else if (data.summary.length > 200) { errors.summary = [ "Event title must not exceed 200 characters" ]; } if (!data.start) { errors.start = [ "Start date/time is required" ]; } if (!data.end) { errors.end = [ "End date/time is required" ]; } if (data.start && data.end) { if (data.start >= data.end) { errors.end = [ "End time must be after start time" ]; } const durationHours = (data.end.getTime() - data.start.getTime()) / (1e3 * 60 * 60); if (durationHours > 24) { errors.end = [ "Event duration cannot exceed 24 hours" ]; } } if (!data.type) { errors.type = [ "Event type is required" ]; } else if (!Object.values(EVENT_TYPES).includes(data.type)) { errors.type = [ "Invalid event type" ]; } if (data.calendar) { const calendarRepo = dataSource.getRepository(Calendar); const calendar = await calendarRepo.findOneBy({ id: data.calendar.id }); if (!calendar) { errors.calendar = [ "Selected calendar does not exist" ]; } } if (data.venue) { const venueRepo = dataSource.getRepository(Venue); const venue = await venueRepo.findOneBy({ id: data.venue.id }); if (!venue) { errors.venue = [ "Selected venue does not exist" ]; } } if (data.primaryContact) { const contactRepo = dataSource.getRepository(Contact); const contact = await contactRepo.findOneBy({ id: data.primaryContact.id }); if (!contact) { errors.primaryContact = [ "Selected contact does not exist" ]; } } if (data.start && data.end && data.calendar) { const eventRepo = dataSource.getRepository(Event); const overlappingEvents = await eventRepo.createQueryBuilder("event").where("event.calendarId = :calendarId", { calendarId: data.calendar.id }).andWhere("event.start < :end", { end: data.end }).andWhere("event.end > :start", { start: data.start }).andWhere(eventId ? "event.id != :eventId" : "1=1", { eventId }).getMany(); if (overlappingEvents.length > 0) { warnings.schedule = [ `This event overlaps with ${overlappingEvents.length} other event(s)` ]; } } if (data.type === EVENT_TYPES.LESSON) { if (!data.primaryContact) { errors.primaryContact = [ "Student contact is required for lessons" ]; } if (!data.studentLevel) { warnings.studentLevel = [ "Student level is recommended for lessons" ]; } } if (data.type === EVENT_TYPES.GIG) { if (!data.venue) { warnings.venue = [ "Venue is recommended for gigs" ]; } if (!data.paymentStatus) { warnings.paymentStatus = [ "Payment status is recommended for gigs" ]; } } if (data.paymentStatus === PAYMENT_STATUS.OVERDUE && data.paymentDueDate) { const now = /* @__PURE__ */ new Date(); if (data.paymentDueDate > now) { errors.paymentStatus = [ "Payment cannot be overdue if due date is in the future" ]; } } return { isValid: Object.keys(errors).length === 0, errors, warnings: Object.keys(warnings).length > 0 ? warnings : void 0 }; } __name(validateEventWithDb, "validateEventWithDb"); async function validateCalendarWithDb(data, dataSource, calendarId) { const errors = {}; if (!data.name || data.name.trim().length === 0) { errors.name = [ "Calendar name is required" ]; } else if (data.name.length > 100) { errors.name = [ "Calendar name must not exceed 100 characters" ]; } if (data.description && data.description.length > 500) { errors.description = [ "Description must not exceed 500 characters" ]; } if (!data.type) { errors.type = [ "Calendar type is required" ]; } else if (![ "individual", "group" ].includes(data.type)) { errors.type = [ 'Calendar type must be either "individual" or "group"' ]; } if (data.name) { const calendarRepo = dataSource.getRepository(Calendar); const existingCalendar = await calendarRepo.createQueryBuilder("calendar").where("LOWER(calendar.name) = LOWER(:name)", { name: data.name }).andWhere(calendarId ? "calendar.id != :calendarId" : "1=1", { calendarId }).getOne(); if (existingCalendar) { errors.name = [ "A calendar with this name already exists" ]; } } return { isValid: Object.keys(errors).length === 0, errors }; } __name(validateCalendarWithDb, "validateCalendarWithDb"); async function validateVenueWithDb(data, dataSource, venueId) { const errors = {}; if (!data.name || data.name.trim().length === 0) { errors.name = [ "Venue name is required" ]; } else if (data.name.length > 200) { errors.name = [ "Venue name must not exceed 200 characters" ]; } if (data.contactEmail && !isValidEmail(data.contactEmail)) { errors.contactEmail = [ "Contact email must be a valid email address" ]; } if (data.contactPhone && !isValidPhone(data.contactPhone)) { errors.contactPhone = [ "Contact phone must be a valid Australian phone number" ]; } if (data.website && !/^https?:\/\/.+/.test(data.website)) { errors.website = [ "Website must be a valid URL starting with http:// or https://" ]; } if (data.name && data.city) { const venueRepo = dataSource.getRepository(Venue); const existingVenue = await venueRepo.createQueryBuilder("venue").where("LOWER(venue.name) = LOWER(:name)", { name: data.name }).andWhere("LOWER(venue.city) = LOWER(:city)", { city: data.city }).andWhere(venueId ? "venue.id != :venueId" : "1=1", { venueId }).getOne(); if (existingVenue) { errors.name = [ "A venue with this name already exists in this city" ]; } } return { isValid: Object.keys(errors).length === 0, errors }; } __name(validateVenueWithDb, "validateVenueWithDb"); async function validateContactWithDb(data, dataSource, contactId) { const errors = {}; if (!data.name || data.name.trim().length === 0) { errors.name = [ "Contact name is required" ]; } else if (data.name.length > 200) { errors.name = [ "Contact name must not exceed 200 characters" ]; } if (data.email && !isValidEmail(data.email)) { errors.email = [ "Email must be a valid email address" ]; } if (data.phone && !isValidPhone(data.phone)) { errors.phone = [ "Phone must be a valid Australian phone number" ]; } if (data.email) { const contactRepo = dataSource.getRepository(Contact); const existingContact = await contactRepo.createQueryBuilder("contact").where("LOWER(contact.email) = LOWER(:email)", { email: data.email }).andWhere(contactId ? "contact.id != :contactId" : "1=1", { contactId }).getOne(); if (existingContact) { errors.email = [ "A contact with this email address already exists" ]; } } return { isValid: Object.keys(errors).length === 0, errors }; } __name(validateContactWithDb, "validateContactWithDb"); async function validateEventScheduling(eventData, dataSource) { const errors = {}; const warnings = {}; if (!eventData.start || !eventData.end || !eventData.calendar) { return { isValid: true, errors: {} }; } const eventRepo = dataSource.getRepository(Event); const conflictingEvents = await eventRepo.createQueryBuilder("event").where("event.calendarId = :calendarId", { calendarId: eventData.calendar.id }).andWhere("event.start < :end", { end: eventData.end }).andWhere("event.end > :start", { start: eventData.start }).getMany(); if (conflictingEvents.length > 0) { errors.schedule = [ "This time slot conflicts with existing events" ]; } const dayOfWeek = eventData.start.getDay(); const hour = eventData.start.getHours(); if (eventData.type === EVENT_TYPES.LESSON) { if (dayOfWeek === 0 || dayOfWeek === 6) { warnings.schedule = [ "Weekend lessons are unusual - please confirm this is correct" ]; } if (hour < 8 || hour > 20) { warnings.schedule = [ "Lessons outside 8am-8pm are unusual - please confirm this is correct" ]; } } if (eventData.type === EVENT_TYPES.GIG) { if (hour < 10 || hour > 23) { warnings.schedule = [ "Gig times outside 10am-11pm are unusual - please confirm this is correct" ]; } } return { isValid: Object.keys(errors).length === 0, errors, warnings: Object.keys(warnings).length > 0 ? warnings : void 0 }; } __name(validateEventScheduling, "validateEventScheduling"); export { API_CONFIG, AUSTRALIAN_STATES, CALENDAR_TYPES, CALENDAR_VIEWS, CURRENCIES, Calendar, Contact, DATE_FORMATS, DEFAULT_CURRENCY, DEFAULT_EVENT_DURATION_MINUTES, DEFAULT_START_OF_WEEK, DIFFICULTY_LEVELS, EVENT_STATUS, EVENT_TYPES, Event, EventExpense, EventIncome, FILE_UPLOAD, GENRES, INSTRUMENTS, MAX_EVENT_DURATION_HOURS, MAX_LENGTHS, MAX_RECURRENCE_OCCURRENCES, PAYMENT_STATUS, STUDENT_LEVELS, TIME_FORMATS, VALIDATION_PATTERNS, Venue, addEventExpense, addEventIncome, calculateEventExpenses, calculateEventIncome, calculateEventProfit, createContact, createEvent, createVenue, deleteEvent, deleteEventExpense, deleteEventIncome, fetchChanges, formatDateAustralian, generateIcs, generateIcsByType, generateTempId, generateUpcomingIcs, getDurationMinutes, getEndOfWeek, getEventExpenses, getEventIncome, getEventsByCalendar, getEventsByType, getFinancialSummary, getStartOfWeek, getStudentLessons, getUpcomingGigs, initDb, isSameDay, isValidEmail, isValidPhone, updateEvent, updateEventExpense, updateEventIncome, validateCalendarWithDb, validateContactWithDb, validateEventScheduling, validateEventWithDb, validateVenueWithDb }; //# sourceMappingURL=index.mjs.map