@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
JavaScript
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