@crescender/calendar
Version:
A comprehensive TypeScript calendar library with musician-specific capabilities, architected for client/server separation.
1 lines • 68 kB
Source Map (JSON)
{"version":3,"sources":["../../src/shared/utils.ts","../../src/shared/constants.ts","../../src/shared/enums.ts","../../src/server/database/entities.ts","../../src/server/services/events.ts","../../src/server/services/ics.ts","../../src/server/validation.ts"],"sourcesContent":["/**\n * @file Shared utility functions for the calendar library.\n * These utilities are safe to use in both client and server environments.\n */\n\n/**\n * Formats a date in Australian format (dd/mmm/yyyy).\n * Uses UTC date to avoid timezone issues in tests.\n */\nexport function formatDateAustralian(date: Date): string {\n const months = [\n 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'\n ];\n \n const day = date.getUTCDate().toString().padStart(2, '0');\n const month = months[date.getUTCMonth()];\n const year = date.getUTCFullYear();\n \n return `${day}/${month}/${year}`;\n}\n\n/**\n * Calculates the duration between two dates in minutes.\n */\nexport function getDurationMinutes(start: Date, end: Date): number {\n return Math.round((end.getTime() - start.getTime()) / (1000 * 60));\n}\n\n/**\n * Checks if two dates are on the same day.\n */\nexport function isSameDay(date1: Date, date2: Date): boolean {\n return date1.toDateString() === date2.toDateString();\n}\n\n/**\n * Gets the start of week (Monday) for a given date.\n */\nexport function getStartOfWeek(date: Date): Date {\n const result = new Date(date);\n const day = result.getDay();\n const diff = result.getDate() - day + (day === 0 ? -6 : 1); // Adjust when day is Sunday\n result.setDate(diff);\n result.setHours(0, 0, 0, 0);\n return result;\n}\n\n/**\n * Gets the end of week (Sunday) for a given date.\n */\nexport function getEndOfWeek(date: Date): Date {\n const result = new Date(date);\n const day = result.getDay();\n const diff = result.getDate() - day + (day === 0 ? 0 : 7); // Adjust when day is Sunday\n result.setDate(diff);\n result.setHours(23, 59, 59, 999);\n return result;\n}\n\n/**\n * Validates an email address.\n */\nexport function isValidEmail(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n}\n\n/**\n * Validates a phone number (basic validation).\n */\nexport function isValidPhone(phone: string): boolean {\n // Basic validation for Australian phone numbers\n const phoneRegex = /^(\\+61|0)[2-9]\\d{8}$/;\n return phoneRegex.test(phone.replace(/\\s/g, ''));\n}\n\n/**\n * Generates a unique ID (simple implementation for client-side).\n * Note: For production, use a proper UUID library on the server.\n */\nexport function generateTempId(): string {\n return `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n} ","/**\n * @file Shared constants for the calendar library.\n * These constants are safe to use in both client and server environments.\n */\n\n/**\n * Default currency for financial calculations.\n */\nexport const DEFAULT_CURRENCY = 'AUD';\n\n/**\n * Maximum event duration in hours.\n */\nexport const MAX_EVENT_DURATION_HOURS = 24;\n\n/**\n * Maximum number of recurrence occurrences.\n */\nexport const MAX_RECURRENCE_OCCURRENCES = 365;\n\n/**\n * Default event duration in minutes.\n */\nexport const DEFAULT_EVENT_DURATION_MINUTES = 60;\n\n/**\n * Maximum field lengths for validation.\n */\nexport const MAX_LENGTHS = {\n EVENT_TITLE: 200,\n EVENT_DESCRIPTION: 1000,\n CALENDAR_NAME: 100,\n CALENDAR_DESCRIPTION: 500,\n VENUE_NAME: 200,\n VENUE_ADDRESS: 300,\n VENUE_CITY: 100,\n CONTACT_NAME: 200,\n CONTACT_ROLE: 100,\n NOTES: 1000,\n INCOME_DESCRIPTION: 200,\n EXPENSE_DESCRIPTION: 200,\n RECEIPT_PATH: 500\n} as const;\n\n/**\n * Date format patterns for different locales.\n */\nexport const DATE_FORMATS = {\n AUSTRALIAN: 'DD/MM/YYYY',\n AMERICAN: 'MM/DD/YYYY',\n ISO: 'YYYY-MM-DD'\n} as const;\n\n/**\n * Time format patterns.\n */\nexport const TIME_FORMATS = {\n TWELVE_HOUR: '12h',\n TWENTY_FOUR_HOUR: '24h'\n} as const;\n\n/**\n * Default calendar view options.\n */\nexport const CALENDAR_VIEWS = {\n DAY: 'day',\n WEEK: 'week',\n MONTH: 'month',\n LIST: 'list'\n} as const;\n\n/**\n * Default start of week (0 = Sunday, 1 = Monday, etc.)\n */\nexport const DEFAULT_START_OF_WEEK = 1; // Monday\n\n/**\n * Australian states and territories.\n */\nexport const AUSTRALIAN_STATES = {\n NSW: 'New South Wales',\n VIC: 'Victoria',\n QLD: 'Queensland',\n WA: 'Western Australia',\n SA: 'South Australia',\n TAS: 'Tasmania',\n ACT: 'Australian Capital Territory',\n NT: 'Northern Territory'\n} as const;\n\n/**\n * Common currency codes.\n */\nexport const CURRENCIES = {\n AUD: 'Australian Dollar',\n USD: 'US Dollar',\n EUR: 'Euro',\n GBP: 'British Pound',\n CAD: 'Canadian Dollar',\n NZD: 'New Zealand Dollar'\n} as const;\n\n/**\n * Validation patterns.\n */\nexport const VALIDATION_PATTERNS = {\n EMAIL: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,\n PHONE_AU: /^(\\+61|0)[2-478](?:[ -]?[0-9]){8}$/,\n CURRENCY_CODE: /^[A-Z]{3}$/,\n HEX_COLOR: /^#[0-9A-F]{6}$/i,\n URL: /^https?:\\/\\/.+/\n} as const;\n\n/**\n * API configuration constants.\n */\nexport const API_CONFIG = {\n DEFAULT_PAGE_SIZE: 20,\n MAX_PAGE_SIZE: 100,\n REQUEST_TIMEOUT: 30000, // 30 seconds\n RETRY_ATTEMPTS: 3\n} as const;\n\n/**\n * File upload constraints.\n */\nexport const FILE_UPLOAD = {\n MAX_SIZE_MB: 10,\n ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'],\n ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.pdf']\n} as const; ","/**\n * @file Shared enums for the calendar library.\n * These enums are safe to use in both client and server environments.\n */\n\n/**\n * Event types for musician-specific functionality.\n */\nexport const EVENT_TYPES = {\n GIG: 'gig',\n LESSON: 'lesson',\n AUDITION: 'audition',\n PRACTICE: 'practice',\n REHEARSAL: 'rehearsal',\n RECORDING: 'recording',\n MEETING: 'meeting'\n} as const;\n\nexport type EventType = typeof EVENT_TYPES[keyof typeof EVENT_TYPES];\n\n/**\n * Payment status options.\n */\nexport const PAYMENT_STATUS = {\n PENDING: 'Pending',\n PAID: 'Paid',\n OVERDUE: 'Overdue',\n CANCELLED: 'Cancelled'\n} as const;\n\nexport type PaymentStatus = typeof PAYMENT_STATUS[keyof typeof PAYMENT_STATUS];\n\n/**\n * Event status options.\n */\nexport const EVENT_STATUS = {\n CONFIRMED: 'Confirmed',\n TENTATIVE: 'Tentative',\n CANCELLED: 'Cancelled',\n COMPLETED: 'Completed'\n} as const;\n\nexport type EventStatus = typeof EVENT_STATUS[keyof typeof EVENT_STATUS];\n\n/**\n * Calendar types.\n */\nexport const CALENDAR_TYPES = {\n INDIVIDUAL: 'individual',\n GROUP: 'group',\n SHARED: 'shared'\n} as const;\n\nexport type CalendarType = typeof CALENDAR_TYPES[keyof typeof CALENDAR_TYPES];\n\n/**\n * Student levels for lessons.\n */\nexport const STUDENT_LEVELS = {\n BEGINNER: 'Beginner',\n INTERMEDIATE: 'Intermediate',\n ADVANCED: 'Advanced',\n PROFESSIONAL: 'Professional'\n} as const;\n\nexport type StudentLevel = typeof STUDENT_LEVELS[keyof typeof STUDENT_LEVELS];\n\n/**\n * Difficulty levels for pieces/repertoire.\n */\nexport const DIFFICULTY_LEVELS = {\n EASY: 'Easy',\n MEDIUM: 'Medium',\n HARD: 'Hard',\n EXPERT: 'Expert'\n} as const;\n\nexport type DifficultyLevel = typeof DIFFICULTY_LEVELS[keyof typeof DIFFICULTY_LEVELS];\n\n/**\n * Common musical genres.\n */\nexport const GENRES = {\n CLASSICAL: 'Classical',\n JAZZ: 'Jazz',\n ROCK: 'Rock',\n POP: 'Pop',\n BLUES: 'Blues',\n COUNTRY: 'Country',\n FOLK: 'Folk',\n ELECTRONIC: 'Electronic',\n WORLD: 'World Music',\n OTHER: 'Other'\n} as const;\n\nexport type Genre = typeof GENRES[keyof typeof GENRES];\n\n/**\n * Common instruments.\n */\nexport const INSTRUMENTS = {\n PIANO: 'Piano',\n GUITAR: 'Guitar',\n VIOLIN: 'Violin',\n DRUMS: 'Drums',\n BASS: 'Bass',\n SAXOPHONE: 'Saxophone',\n TRUMPET: 'Trumpet',\n FLUTE: 'Flute',\n CELLO: 'Cello',\n VOICE: 'Voice',\n OTHER: 'Other'\n} as const;\n\nexport type Instrument = typeof INSTRUMENTS[keyof typeof INSTRUMENTS]; ","/**\n * @file TypeORM entities for the calendar library.\n * These entities are for server-side use only and require Node.js/TypeORM.\n */\n\nimport { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn, JoinColumn } from 'typeorm';\n\n/**\n * Represents a calendar.\n */\n@Entity()\nexport class Calendar {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n @Column()\n name!: string;\n\n @Column({ nullable: true })\n description?: string;\n\n /**\n * The type of calendar.\n * 'individual' - A personal calendar for a single user.\n * 'group' - A shared calendar for a team or band.\n */\n @Column()\n type!: string;\n}\n\n/**\n * Represents a venue or location for events.\n */\n@Entity()\nexport class Venue {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n @Column()\n name!: string;\n\n @Column({ nullable: true })\n address?: string;\n\n @Column({ nullable: true })\n city?: string;\n\n @Column({ nullable: true })\n state?: string;\n\n @Column({ nullable: true })\n country?: string;\n\n @Column({ nullable: true })\n website?: string;\n\n @Column({ nullable: true })\n contactName?: string;\n\n @Column({ nullable: true })\n contactEmail?: string;\n\n @Column({ nullable: true })\n contactPhone?: string;\n\n @Column({ nullable: true })\n notes?: string;\n}\n\n/**\n * Represents a person or organization (student, band member, promoter, etc.)\n */\n@Entity()\nexport class Contact {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n @Column()\n name!: string;\n\n @Column({ nullable: true })\n email?: string;\n\n @Column({ nullable: true })\n phone?: string;\n\n @Column({ nullable: true })\n role?: string; // 'student', 'band-member', 'promoter', 'sound-engineer', etc.\n\n @Column({ nullable: true })\n notes?: string;\n}\n\n/**\n * Represents a calendar event with musician-specific capabilities.\n */\n@Entity()\nexport class Event {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n @Column()\n summary!: string;\n\n @Column({ nullable: true })\n description?: string;\n\n @Column()\n start!: Date;\n\n @Column()\n end!: Date;\n\n /**\n * An RFC 5545 recurrence rule string.\n * @see https://icalendar.org/rrule-tool.html\n * @example 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20251231T235959Z'\n */\n @Column({ nullable: true })\n recurrenceRule?: string;\n\n /**\n * Event type: 'gig', 'lesson', 'audition', 'practice', 'rehearsal', 'recording', 'meeting', etc.\n */\n @Column()\n type!: string;\n\n // Musician-specific fields\n @Column({ nullable: true })\n genre?: string; // 'Jazz', 'Classical', 'Rock', etc.\n\n @Column({ nullable: true })\n instrument?: string; // Primary instrument for this event\n\n @Column({ nullable: true })\n difficulty?: string; // 'Beginner', 'Intermediate', 'Advanced', 'Professional'\n\n @Column({ nullable: true })\n repertoire?: string; // Songs/pieces to be performed or practiced\n\n @Column({ nullable: true })\n setList?: string; // JSON string of songs in order\n\n @Column({ nullable: true })\n equipmentNeeded?: string; // JSON array of required equipment\n\n @Column({ nullable: true })\n dresscode?: string; // 'Formal', 'Casual', 'Black tie', etc.\n\n @Column({ nullable: true })\n soundcheckTime?: Date; // For gigs\n\n @Column({ nullable: true })\n loadInTime?: Date; // For gigs\n\n @Column({ nullable: true })\n paymentStatus?: string; // 'Pending', 'Paid', 'Overdue', 'Cancelled'\n\n @Column({ nullable: true })\n paymentDueDate?: Date;\n\n @Column({ nullable: true })\n studentLevel?: string; // For lessons: 'Grade 1', 'Grade 8', 'Diploma', etc.\n\n @Column({ nullable: true })\n lessonFocus?: string; // For lessons: 'Technique', 'Theory', 'Repertoire', etc.\n\n @Column({ nullable: true })\n auditionPiece?: string; // For auditions\n\n @Column({ nullable: true })\n auditionRequirements?: string; // For auditions\n\n @Column({ nullable: true })\n practiceGoals?: string; // For practice sessions\n\n @Column({ nullable: true })\n rehearsalNotes?: string; // For rehearsals\n\n @Column({ nullable: true })\n status?: string; // 'Confirmed', 'Tentative', 'Cancelled', 'Completed'\n\n @CreateDateColumn()\n createdAt!: Date;\n\n @UpdateDateColumn()\n updatedAt!: Date;\n\n // Relationships\n @ManyToOne(() => Calendar)\n @JoinColumn()\n calendar!: Calendar;\n\n @ManyToOne(() => Venue, { nullable: true })\n @JoinColumn()\n venue?: Venue;\n\n @ManyToOne(() => Contact, { nullable: true })\n @JoinColumn()\n primaryContact?: Contact; // Main contact for this event (student, promoter, etc.)\n\n @OneToMany(() => EventIncome, (income) => income.event, { cascade: true })\n income?: EventIncome[];\n\n @OneToMany(() => EventExpense, (expense) => expense.event, { cascade: true })\n expenses?: EventExpense[];\n}\n\n/**\n * Represents income for an event (gig fees, lesson payments, etc.)\n */\n@Entity()\nexport class EventIncome {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n @Column()\n description!: string; // 'Door charge', 'Flat fee', 'Merch sales', 'Tips', etc.\n\n @Column('decimal', { precision: 10, scale: 2 })\n amount!: number;\n\n @Column({ default: 'AUD' })\n currency!: string;\n\n @Column({ nullable: true })\n notes?: string;\n\n @ManyToOne(() => Event, (event) => event.income)\n @JoinColumn()\n event!: Event;\n}\n\n/**\n * Represents expenses for an event (gear hire, travel, food, etc.)\n */\n@Entity()\nexport class EventExpense {\n @PrimaryGeneratedColumn('uuid')\n id!: string;\n\n @Column()\n description!: string; // 'Gear hire', 'Food & bev', 'Marketing', 'Travel', etc.\n\n @Column('decimal', { precision: 10, scale: 2 })\n amount!: number;\n\n @Column({ default: 'AUD' })\n currency!: string;\n\n @Column({ nullable: true })\n receipt?: string; // Path to receipt image/file\n\n @Column({ nullable: true })\n notes?: string;\n\n @ManyToOne(() => Event, (event) => event.expenses)\n @JoinColumn()\n event!: Event;\n} ","/**\n * @file Server-side event services for the calendar library.\n * These services handle all database operations and business logic for events.\n */\n\nimport { DataSource, Between, Repository } from 'typeorm';\nimport { Event, Calendar, EventIncome, EventExpense, Venue, Contact } from '../database/entities';\nimport type { Changes, FinancialSummary } from '../../shared/types';\n\nlet db: DataSource;\nlet eventRepository: Repository<Event>;\nlet calendarRepository: Repository<Calendar>;\nlet incomeRepository: Repository<EventIncome>;\nlet expenseRepository: Repository<EventExpense>;\nlet venueRepository: Repository<Venue>;\nlet contactRepository: Repository<Contact>;\n\n/**\n * Initializes the database connection and repositories.\n */\nexport function initDb(dataSource: DataSource): DataSource {\n db = dataSource;\n eventRepository = dataSource.getRepository(Event);\n calendarRepository = dataSource.getRepository(Calendar);\n incomeRepository = dataSource.getRepository(EventIncome);\n expenseRepository = dataSource.getRepository(EventExpense);\n venueRepository = dataSource.getRepository(Venue);\n contactRepository = dataSource.getRepository(Contact);\n return db;\n}\n\nfunction getDb(): DataSource {\n if (!db) {\n throw new Error('Database has not been initialized. Call initDb() first.');\n }\n return db;\n}\n\n/**\n * Fetches changes to events since a given date for sync purposes.\n */\nexport async function fetchChanges(calendarId: string, since: Date): Promise<Changes> {\n const eventRepository = getDb().getRepository(Event);\n\n const updated = await eventRepository.find({\n where: {\n calendar: { id: calendarId },\n updatedAt: Between(since, new Date()),\n },\n relations: ['calendar', 'venue', 'primaryContact'],\n });\n\n // TypeORM doesn't have a built-in way to track deletions,\n // so we assume the consumer application handles this, possibly with a soft-delete mechanism.\n // For this implementation, we'll return an empty array for deleted events.\n const deleted: string[] = [];\n\n return {\n updated: updated.map(event => ({\n id: event.id,\n summary: event.summary,\n description: event.description,\n start: event.start,\n end: event.end,\n recurrenceRule: event.recurrenceRule,\n type: event.type,\n genre: event.genre,\n instrument: event.instrument,\n difficulty: event.difficulty,\n repertoire: event.repertoire,\n setList: event.setList,\n equipmentNeeded: event.equipmentNeeded,\n dresscode: event.dresscode,\n soundcheckTime: event.soundcheckTime,\n loadInTime: event.loadInTime,\n paymentStatus: event.paymentStatus,\n paymentDueDate: event.paymentDueDate,\n studentLevel: event.studentLevel,\n lessonFocus: event.lessonFocus,\n auditionPiece: event.auditionPiece,\n auditionRequirements: event.auditionRequirements,\n practiceGoals: event.practiceGoals,\n rehearsalNotes: event.rehearsalNotes,\n status: event.status,\n createdAt: event.createdAt,\n updatedAt: event.updatedAt,\n calendarId: event.calendar.id,\n venueId: event.venue?.id,\n primaryContactId: event.primaryContact?.id,\n })),\n deleted,\n };\n}\n\n/**\n * Creates a new event.\n */\nexport async function createEvent(\n calendarId: string,\n eventData: Omit<Event, 'id' | 'updatedAt' | 'createdAt' | 'calendar'>\n): Promise<Event> {\n const eventRepository = getDb().getRepository(Event);\n const calendarRepository = getDb().getRepository(Calendar);\n \n const calendar = await calendarRepository.findOneBy({ id: calendarId });\n if (!calendar) {\n throw new Error(`Calendar with ID \"${calendarId}\" not found.`);\n }\n\n const newEvent = eventRepository.create({\n ...eventData,\n calendar,\n });\n return eventRepository.save(newEvent);\n}\n\n/**\n * Updates an existing event.\n */\nexport async function updateEvent(\n eventId: string,\n updates: Partial<Omit<Event, 'id' | 'calendar' | 'createdAt' | 'updatedAt'>>\n): Promise<Event> {\n const eventRepository = getDb().getRepository(Event);\n \n await eventRepository.update(eventId, updates);\n const updatedEvent = await eventRepository.findOneBy({ id: eventId });\n \n if (!updatedEvent) {\n throw new Error(`Event with ID \"${eventId}\" not found.`);\n }\n \n return updatedEvent;\n}\n\n/**\n * Deletes an event.\n */\nexport async function deleteEvent(eventId: string): Promise<void> {\n const eventRepository = getDb().getRepository(Event);\n await eventRepository.delete({ id: eventId });\n}\n\n/**\n * Gets all events for a calendar.\n */\nexport async function getEventsByCalendar(calendarId: string): Promise<Event[]> {\n const eventRepository = getDb().getRepository(Event);\n return eventRepository.find({\n where: { calendar: { id: calendarId } },\n relations: ['calendar', 'venue', 'primaryContact', 'income', 'expenses'],\n });\n}\n\n/**\n * Gets events by type for a calendar.\n */\nexport async function getEventsByType(calendarId: string, type: string): Promise<Event[]> {\n const eventRepository = getDb().getRepository(Event);\n return eventRepository.find({\n where: { \n calendar: { id: calendarId },\n type: type\n },\n relations: ['calendar', 'venue', 'primaryContact', 'income', 'expenses'],\n order: { start: 'ASC' }\n });\n}\n\n/**\n * Gets upcoming gigs for a calendar.\n */\nexport async function getUpcomingGigs(calendarId: string, limit: number = 10): Promise<Event[]> {\n const eventRepository = getDb().getRepository(Event);\n const now = new Date();\n \n return eventRepository.find({\n where: { \n calendar: { id: calendarId },\n type: 'gig',\n start: Between(now, new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000)) // Next year\n },\n relations: ['calendar', 'venue', 'primaryContact', 'income', 'expenses'],\n order: { start: 'ASC' },\n take: limit\n });\n}\n\n/**\n * Gets student lessons for a specific student contact.\n */\nexport async function getStudentLessons(calendarId: string, studentContactId: string): Promise<Event[]> {\n const eventRepository = getDb().getRepository(Event);\n \n return eventRepository.find({\n where: { \n calendar: { id: calendarId },\n type: 'lesson',\n primaryContact: { id: studentContactId }\n },\n relations: ['calendar', 'venue', 'primaryContact', 'income', 'expenses'],\n order: { start: 'ASC' }\n });\n}\n\n/**\n * Adds income to an event.\n */\nexport async function addEventIncome(eventId: string, income: Omit<EventIncome, 'id' | 'event'>): Promise<EventIncome> {\n const event = await eventRepository.findOne({ where: { id: eventId } });\n if (!event) {\n throw new Error(`Event with id ${eventId} not found`);\n }\n\n const eventIncome = incomeRepository.create({\n ...income,\n event\n });\n\n return await incomeRepository.save(eventIncome);\n}\n\n/**\n * Adds expense to an event.\n */\nexport async function addEventExpense(eventId: string, expense: Omit<EventExpense, 'id' | 'event'>): Promise<EventExpense> {\n const event = await eventRepository.findOne({ where: { id: eventId } });\n if (!event) {\n throw new Error(`Event with id ${eventId} not found`);\n }\n\n const eventExpense = expenseRepository.create({\n ...expense,\n event\n });\n\n return await expenseRepository.save(eventExpense);\n}\n\n/**\n * Gets all income for an event.\n */\nexport async function getEventIncome(eventId: string): Promise<EventIncome[]> {\n return await incomeRepository.find({\n where: { event: { id: eventId } }\n });\n}\n\n/**\n * Gets all expenses for an event.\n */\nexport async function getEventExpenses(eventId: string): Promise<EventExpense[]> {\n return await expenseRepository.find({\n where: { event: { id: eventId } }\n });\n}\n\n/**\n * Calculates total income for an event.\n */\nexport async function calculateEventIncome(eventId: string): Promise<number> {\n const income = await getEventIncome(eventId);\n return income.reduce((total, item) => total + Number(item.amount), 0);\n}\n\n/**\n * Calculates total expenses for an event.\n */\nexport async function calculateEventExpenses(eventId: string): Promise<number> {\n const expenses = await getEventExpenses(eventId);\n return expenses.reduce((total, item) => total + Number(item.amount), 0);\n}\n\n/**\n * Calculates net profit/loss for an event.\n */\nexport async function calculateEventProfit(eventId: string): Promise<number> {\n const totalIncome = await calculateEventIncome(eventId);\n const totalExpenses = await calculateEventExpenses(eventId);\n return totalIncome - totalExpenses;\n}\n\n/**\n * Gets financial summary for multiple events.\n */\nexport async function getFinancialSummary(eventIds: string[]): Promise<FinancialSummary> {\n let totalIncome = 0;\n let totalExpenses = 0;\n\n for (const eventId of eventIds) {\n totalIncome += await calculateEventIncome(eventId);\n totalExpenses += await calculateEventExpenses(eventId);\n }\n\n const netProfit = totalIncome - totalExpenses;\n const eventCount = eventIds.length;\n const averageProfitPerEvent = eventCount > 0 ? netProfit / eventCount : 0;\n\n return {\n totalIncome,\n totalExpenses,\n netProfit,\n eventCount,\n averageProfitPerEvent,\n };\n}\n\n/**\n * Updates event income.\n */\nexport async function updateEventIncome(incomeId: string, updates: Partial<EventIncome>): Promise<EventIncome> {\n await incomeRepository.update(incomeId, updates);\n const updatedIncome = await incomeRepository.findOneBy({ id: incomeId });\n \n if (!updatedIncome) {\n throw new Error(`Income with ID \"${incomeId}\" not found.`);\n }\n \n return updatedIncome;\n}\n\n/**\n * Updates event expense.\n */\nexport async function updateEventExpense(expenseId: string, updates: Partial<EventExpense>): Promise<EventExpense> {\n await expenseRepository.update(expenseId, updates);\n const updatedExpense = await expenseRepository.findOneBy({ id: expenseId });\n \n if (!updatedExpense) {\n throw new Error(`Expense with ID \"${expenseId}\" not found.`);\n }\n \n return updatedExpense;\n}\n\n/**\n * Deletes event income.\n */\nexport async function deleteEventIncome(incomeId: string): Promise<void> {\n await incomeRepository.delete({ id: incomeId });\n}\n\n/**\n * Deletes event expense.\n */\nexport async function deleteEventExpense(expenseId: string): Promise<void> {\n await expenseRepository.delete({ id: expenseId });\n}\n\n/**\n * Creates a new venue.\n */\nexport async function createVenue(venue: Omit<Venue, 'id'>): Promise<Venue> {\n const newVenue = venueRepository.create(venue);\n return await venueRepository.save(newVenue);\n}\n\n/**\n * Creates a new contact.\n */\nexport async function createContact(contact: Omit<Contact, 'id'>): Promise<Contact> {\n const newContact = contactRepository.create(contact);\n return await contactRepository.save(newContact);\n} ","/**\n * @file Server-side ICS generation service for the calendar library.\n * This service handles iCalendar feed generation and requires Node.js.\n */\n\nimport ical from 'ical-generator';\nimport { getEventsByCalendar } from './events';\nimport type { Calendar } from '../database/entities';\n\n/**\n * Generates an iCalendar (.ics) feed for a given calendar.\n * This is a server-side operation that requires Node.js.\n *\n * @param calendar The calendar object.\n * @param calendarId The ID of the calendar to generate the feed for.\n * @returns A promise that resolves with the iCalendar feed as a string.\n */\nexport async function generateIcs(calendar: Calendar, calendarId: string): Promise<string> {\n const cal = ical({ \n name: calendar.name,\n description: calendar.description,\n timezone: 'Australia/Sydney' // Default to Australian timezone\n });\n \n const events = await getEventsByCalendar(calendarId);\n \n events.forEach(event => {\n const icalEvent = cal.createEvent({\n start: event.start,\n end: event.end,\n summary: event.summary,\n description: event.description,\n location: event.venue ? [\n event.venue.name,\n event.venue.address,\n event.venue.city,\n event.venue.state,\n event.venue.country\n ].filter(Boolean).join(', ') : undefined,\n });\n\n // Add musician-specific properties as extended properties\n if (event.type) {\n icalEvent.x('X-EVENT-TYPE', event.type);\n }\n \n if (event.genre) {\n icalEvent.x('X-GENRE', event.genre);\n }\n \n if (event.instrument) {\n icalEvent.x('X-INSTRUMENT', event.instrument);\n }\n \n if (event.paymentStatus) {\n icalEvent.x('X-PAYMENT-STATUS', event.paymentStatus);\n }\n \n if (event.status) {\n icalEvent.x('X-STATUS', event.status);\n }\n\n // Add contact information if available\n if (event.primaryContact) {\n icalEvent.x('X-CONTACT-NAME', event.primaryContact.name);\n if (event.primaryContact.email) {\n icalEvent.x('X-CONTACT-EMAIL', event.primaryContact.email);\n }\n if (event.primaryContact.phone) {\n icalEvent.x('X-CONTACT-PHONE', event.primaryContact.phone);\n }\n }\n\n // Add recurrence rule if present\n if (event.recurrenceRule) {\n icalEvent.repeating(event.recurrenceRule);\n }\n });\n\n return cal.toString();\n}\n\n/**\n * Generates an ICS feed for a specific event type (e.g., only gigs).\n */\nexport async function generateIcsByType(\n calendar: Calendar, \n calendarId: string, \n eventType: string\n): Promise<string> {\n const cal = ical({ \n name: `${calendar.name} - ${eventType.charAt(0).toUpperCase() + eventType.slice(1)}s`,\n description: `${eventType} events from ${calendar.name}`,\n timezone: 'Australia/Sydney'\n });\n \n const events = await getEventsByCalendar(calendarId);\n const filteredEvents = events.filter(event => event.type === eventType);\n \n filteredEvents.forEach(event => {\n const icalEvent = cal.createEvent({\n start: event.start,\n end: event.end,\n summary: event.summary,\n description: event.description,\n location: event.venue ? [\n event.venue.name,\n event.venue.address,\n event.venue.city,\n event.venue.state,\n event.venue.country\n ].filter(Boolean).join(', ') : undefined,\n });\n\n // Add extended properties\n icalEvent.x('X-EVENT-TYPE', event.type);\n \n if (event.genre) icalEvent.x('X-GENRE', event.genre);\n if (event.instrument) icalEvent.x('X-INSTRUMENT', event.instrument);\n if (event.paymentStatus) icalEvent.x('X-PAYMENT-STATUS', event.paymentStatus);\n if (event.status) icalEvent.x('X-STATUS', event.status);\n \n if (event.primaryContact) {\n icalEvent.x('X-CONTACT-NAME', event.primaryContact.name);\n if (event.primaryContact.email) icalEvent.x('X-CONTACT-EMAIL', event.primaryContact.email);\n if (event.primaryContact.phone) icalEvent.x('X-CONTACT-PHONE', event.primaryContact.phone);\n }\n\n if (event.recurrenceRule) {\n icalEvent.repeating(event.recurrenceRule);\n }\n });\n\n return cal.toString();\n}\n\n/**\n * Generates an ICS feed for upcoming events only (next 30 days).\n */\nexport async function generateUpcomingIcs(calendar: Calendar, calendarId: string): Promise<string> {\n const cal = ical({ \n name: `${calendar.name} - Upcoming Events`,\n description: `Upcoming events from ${calendar.name}`,\n timezone: 'Australia/Sydney'\n });\n \n const events = await getEventsByCalendar(calendarId);\n const now = new Date();\n const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);\n \n const upcomingEvents = events.filter(event => \n event.start >= now && event.start <= thirtyDaysFromNow\n );\n \n upcomingEvents.forEach(event => {\n const icalEvent = cal.createEvent({\n start: event.start,\n end: event.end,\n summary: event.summary,\n description: event.description,\n location: event.venue ? [\n event.venue.name,\n event.venue.address,\n event.venue.city,\n event.venue.state,\n event.venue.country\n ].filter(Boolean).join(', ') : undefined,\n });\n\n // Add extended properties\n icalEvent.x('X-EVENT-TYPE', event.type);\n \n if (event.genre) icalEvent.x('X-GENRE', event.genre);\n if (event.instrument) icalEvent.x('X-INSTRUMENT', event.instrument);\n if (event.paymentStatus) icalEvent.x('X-PAYMENT-STATUS', event.paymentStatus);\n if (event.status) icalEvent.x('X-STATUS', event.status);\n \n if (event.primaryContact) {\n icalEvent.x('X-CONTACT-NAME', event.primaryContact.name);\n if (event.primaryContact.email) icalEvent.x('X-CONTACT-EMAIL', event.primaryContact.email);\n if (event.primaryContact.phone) icalEvent.x('X-CONTACT-PHONE', event.primaryContact.phone);\n }\n\n if (event.recurrenceRule) {\n icalEvent.repeating(event.recurrenceRule);\n }\n });\n\n return cal.toString();\n} ","/**\n * @file Server-side validation for the calendar library.\n * These validations can include database checks and are more comprehensive than client-side validation.\n */\n\nimport { DataSource } from 'typeorm';\nimport { Event, Calendar, Venue, Contact } from './database/entities';\nimport { isValidEmail, isValidPhone } from '../shared/utils';\nimport { EVENT_TYPES, PAYMENT_STATUS, EVENT_STATUS } from '../shared/enums';\n\nexport interface ServerValidationResult {\n isValid: boolean;\n errors: Record<string, string[]>;\n warnings?: Record<string, string[]>;\n}\n\n/**\n * Validates event data with database checks.\n */\nexport async function validateEventWithDb(\n data: Partial<Event>, \n dataSource: DataSource,\n eventId?: string\n): Promise<ServerValidationResult> {\n const errors: Record<string, string[]> = {};\n const warnings: Record<string, string[]> = {};\n\n // Basic validation (same as client-side)\n if (!data.summary || data.summary.trim().length === 0) {\n errors.summary = ['Event title is required'];\n } else if (data.summary.length > 200) {\n errors.summary = ['Event title must not exceed 200 characters'];\n }\n\n if (!data.start) {\n errors.start = ['Start date/time is required'];\n }\n\n if (!data.end) {\n errors.end = ['End date/time is required'];\n }\n\n if (data.start && data.end) {\n if (data.start >= data.end) {\n errors.end = ['End time must be after start time'];\n }\n \n const durationHours = (data.end.getTime() - data.start.getTime()) / (1000 * 60 * 60);\n if (durationHours > 24) {\n errors.end = ['Event duration cannot exceed 24 hours'];\n }\n }\n\n if (!data.type) {\n errors.type = ['Event type is required'];\n } else if (!Object.values(EVENT_TYPES).includes(data.type as any)) {\n errors.type = ['Invalid event type'];\n }\n\n // Database-specific validations\n if (data.calendar) {\n const calendarRepo = dataSource.getRepository(Calendar);\n const calendar = await calendarRepo.findOneBy({ id: data.calendar.id });\n if (!calendar) {\n errors.calendar = ['Selected calendar does not exist'];\n }\n }\n\n if (data.venue) {\n const venueRepo = dataSource.getRepository(Venue);\n const venue = await venueRepo.findOneBy({ id: data.venue.id });\n if (!venue) {\n errors.venue = ['Selected venue does not exist'];\n }\n }\n\n if (data.primaryContact) {\n const contactRepo = dataSource.getRepository(Contact);\n const contact = await contactRepo.findOneBy({ id: data.primaryContact.id });\n if (!contact) {\n errors.primaryContact = ['Selected contact does not exist'];\n }\n }\n\n // Check for overlapping events (warning, not error)\n if (data.start && data.end && data.calendar) {\n const eventRepo = dataSource.getRepository(Event);\n const overlappingEvents = await eventRepo\n .createQueryBuilder('event')\n .where('event.calendarId = :calendarId', { calendarId: data.calendar.id })\n .andWhere('event.start < :end', { end: data.end })\n .andWhere('event.end > :start', { start: data.start })\n .andWhere(eventId ? 'event.id != :eventId' : '1=1', { eventId })\n .getMany();\n\n if (overlappingEvents.length > 0) {\n warnings.schedule = [`This event overlaps with ${overlappingEvents.length} other event(s)`];\n }\n }\n\n // Musician-specific business logic validations\n if (data.type === EVENT_TYPES.LESSON) {\n if (!data.primaryContact) {\n errors.primaryContact = ['Student contact is required for lessons'];\n }\n \n if (!data.studentLevel) {\n warnings.studentLevel = ['Student level is recommended for lessons'];\n }\n }\n\n if (data.type === EVENT_TYPES.GIG) {\n if (!data.venue) {\n warnings.venue = ['Venue is recommended for gigs'];\n }\n \n if (!data.paymentStatus) {\n warnings.paymentStatus = ['Payment status is recommended for gigs'];\n }\n }\n\n if (data.paymentStatus === PAYMENT_STATUS.OVERDUE && data.paymentDueDate) {\n const now = new Date();\n if (data.paymentDueDate > now) {\n errors.paymentStatus = ['Payment cannot be overdue if due date is in the future'];\n }\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors,\n warnings: Object.keys(warnings).length > 0 ? warnings : undefined\n };\n}\n\n/**\n * Validates calendar data with database checks.\n */\nexport async function validateCalendarWithDb(\n data: Partial<Calendar>,\n dataSource: DataSource,\n calendarId?: string\n): Promise<ServerValidationResult> {\n const errors: Record<string, string[]> = {};\n\n if (!data.name || data.name.trim().length === 0) {\n errors.name = ['Calendar name is required'];\n } else if (data.name.length > 100) {\n errors.name = ['Calendar name must not exceed 100 characters'];\n }\n\n if (data.description && data.description.length > 500) {\n errors.description = ['Description must not exceed 500 characters'];\n }\n\n if (!data.type) {\n errors.type = ['Calendar type is required'];\n } else if (!['individual', 'group'].includes(data.type)) {\n errors.type = ['Calendar type must be either \"individual\" or \"group\"'];\n }\n\n // Check for duplicate calendar names (warning)\n if (data.name) {\n const calendarRepo = dataSource.getRepository(Calendar);\n const existingCalendar = await calendarRepo\n .createQueryBuilder('calendar')\n .where('LOWER(calendar.name) = LOWER(:name)', { name: data.name })\n .andWhere(calendarId ? 'calendar.id != :calendarId' : '1=1', { calendarId })\n .getOne();\n\n if (existingCalendar) {\n errors.name = ['A calendar with this name already exists'];\n }\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates venue data with database checks.\n */\nexport async function validateVenueWithDb(\n data: Partial<Venue>,\n dataSource: DataSource,\n venueId?: string\n): Promise<ServerValidationResult> {\n const errors: Record<string, string[]> = {};\n\n if (!data.name || data.name.trim().length === 0) {\n errors.name = ['Venue name is required'];\n } else if (data.name.length > 200) {\n errors.name = ['Venue name must not exceed 200 characters'];\n }\n\n if (data.contactEmail && !isValidEmail(data.contactEmail)) {\n errors.contactEmail = ['Contact email must be a valid email address'];\n }\n\n if (data.contactPhone && !isValidPhone(data.contactPhone)) {\n errors.contactPhone = ['Contact phone must be a valid Australian phone number'];\n }\n\n if (data.website && !/^https?:\\/\\/.+/.test(data.website)) {\n errors.website = ['Website must be a valid URL starting with http:// or https://'];\n }\n\n // Check for duplicate venue names in the same city (warning)\n if (data.name && data.city) {\n const venueRepo = dataSource.getRepository(Venue);\n const existingVenue = await venueRepo\n .createQueryBuilder('venue')\n .where('LOWER(venue.name) = LOWER(:name)', { name: data.name })\n .andWhere('LOWER(venue.city) = LOWER(:city)', { city: data.city })\n .andWhere(venueId ? 'venue.id != :venueId' : '1=1', { venueId })\n .getOne();\n\n if (existingVenue) {\n errors.name = ['A venue with this name already exists in this city'];\n }\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates contact data with database checks.\n */\nexport async function validateContactWithDb(\n data: Partial<Contact>,\n dataSource: DataSource,\n contactId?: string\n): Promise<ServerValidationResult> {\n const errors: Record<string, string[]> = {};\n\n if (!data.name || data.name.trim().length === 0) {\n errors.name = ['Contact name is required'];\n } else if (data.name.length > 200) {\n errors.name = ['Contact name must not exceed 200 characters'];\n }\n\n if (data.email && !isValidEmail(data.email)) {\n errors.email = ['Email must be a valid email address'];\n }\n\n if (data.phone && !isValidPhone(data.phone)) {\n errors.phone = ['Phone must be a valid Australian phone number'];\n }\n\n // Check for duplicate email addresses (error)\n if (data.email) {\n const contactRepo = dataSource.getRepository(Contact);\n const existingContact = await contactRepo\n .createQueryBuilder('contact')\n .where('LOWER(contact.email) = LOWER(:email)', { email: data.email })\n .andWhere(contactId ? 'contact.id != :contactId' : '1=1', { contactId })\n .getOne();\n\n if (existingContact) {\n errors.email = ['A contact with this email address already exists'];\n }\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors\n };\n}\n\n/**\n * Validates business rules for event scheduling.\n */\nexport async function validateEventScheduling(\n eventData: Partial<Event>,\n dataSource: DataSource\n): Promise<ServerValidationResult> {\n const errors: Record<string, string[]> = {};\n const warnings: Record<string, string[]> = {};\n\n if (!eventData.start || !eventData.end || !eventData.calendar) {\n return { isValid: true, errors: {} }; // Skip if basic data is missing\n }\n\n const eventRepo = dataSource.getRepository(Event);\n\n // Check for double-booking (same time, same calendar)\n const conflictingEvents = await eventRepo\n .createQueryBuilder('event')\n .where('event.calendarId = :calendarId', { calendarId: eventData.calendar.id })\n .andWhere('event.start < :end', { end: eventData.end })\n .andWhere('event.end > :start', { start: eventData.start })\n .getMany();\n\n if (conflictingEvents.length > 0) {\n errors.schedule = ['This time slot conflicts with existing events'];\n }\n\n // Check for reasonable scheduling patterns\n const dayOfWeek = eventData.start.getDay();\n const hour = eventData.start.getHours();\n\n // Warn about unusual scheduling\n if (eventData.type === EVENT_TYPES.LESSON) {\n if (dayOfWeek === 0 || dayOfWeek === 6) { // Weekend\n warnings.schedule = ['Weekend lessons are unusual - please confirm this is correct'];\n }\n \n if (hour < 8 || hour > 20) { // Very early or late\n warnings.schedule = ['Lessons outside 8am-8pm are unusual - please confirm this is correct'];\n }\n }\n\n if (eventData.type === EVENT_TYPES.GIG) {\n if (hour < 10 || hour > 23) { // Very early or very late\n warnings.schedule = ['Gig times outside 10am-11pm are unusual - please confirm this is correct'];\n }\n }\n\n return {\n isValid: Object.keys(errors).length === 0,\n errors,\n warnings: Object.keys(warnings).length > 0 ? warnings : undefined\n };\n} "],"mappings":"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