Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Edfermachado/proyectoSistemas/llms.txt

Use this file to discover all available pages before exploring further.

Every event in UniEvents is anchored to two things: the tenant (faculty) that owns it and the space (physical venue) where it will take place. This tight coupling lets the system enforce two critical invariants automatically — capacity limits and schedule conflicts. When an event is created or edited, the service layer validates that the chosen space is free for the entire duration of the new event before writing anything to the database.

Spaces

A space is a physical venue that belongs to a single tenant. Tenants manage their own inventory of spaces (lecture halls, auditoriums, sports courts, etc.) and every event must be assigned to one.
FieldTypeNotes
iduuidPrimary key, auto-generated
namevarchar(255)Display name (e.g., “Auditorium A”)
capacityintegerMaximum number of attendees the venue holds
tenantIduuidFK → tenants.id (non-nullable)
createdAttimestampAuto-set on creation
// src/db/schema.ts
export const spaces = pgTable('spaces', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: varchar('name', { length: 255 }).notNull(),
  capacity: integer('capacity').notNull(),
  tenantId: uuid('tenant_id').references(() => tenants.id).notNull(),
  createdAt: timestamp('created_at').defaultNow(),
});
When an event does not specify its own capacity, the system falls back to the space’s capacity during attendee registration. This means the venue’s physical limit is always the ceiling, even if an organizer forgets to set an event-level cap.

Events

The events table is the central entity of the platform. An event links a tenant, a space, an optional manager, and all the scheduling and payment metadata needed to run a public or private gathering.
FieldTypeNotes
iduuidPrimary key, auto-generated
titlevarchar(255)Display title
slugvarchar(350)URL-safe unique identifier
descriptiontextOptional rich description
datetimestampStart date and time
durationintegerDuration in minutes (default: 60)
pricedecimal(10,2)Admission price; 0 means free
imageUrlvarchar(500)Cover image URL
tenantIduuidFK → tenants.id (non-nullable)
spaceIduuidFK → spaces.id (non-nullable)
capacityintegerOptional event-level cap (overrides space capacity)
visibilityenumpublico or privado
statusvarchar(50)pendiente, aprobado, or rechazado
requiresIpProtectionbooleanEnables IP-based access restrictions
isFeaturedbooleanSurfaces the event on the home page
paymentPhonevarchar(50)Mobile payment number for transfers
paymentIdvarchar(50)Payment account identifier
paymentBankvarchar(100)Bank name for payment instructions
managerIduuidFK → users.id — the event manager
createdAttimestampAuto-set on creation
// src/db/schema.ts
export const events = pgTable('events', {
  id: uuid('id').primaryKey().defaultRandom(),
  title: varchar('title', { length: 255 }).notNull(),
  slug: varchar('slug', { length: 350 }).unique(),
  description: text('description'),
  date: timestamp('date').notNull(),
  price: decimal('price', { precision: 10, scale: 2 }).default('0'),
  imageUrl: varchar('image_url', { length: 500 }),
  duration: integer('duration').notNull().default(60),
  tenantId: uuid('tenant_id').references(() => tenants.id).notNull(),
  spaceId: uuid('space_id').references(() => spaces.id).notNull(),
  capacity: integer('capacity'),
  visibility: visibilityEnum('visibility').default('publico'),
  status: varchar('status', { length: 50 }).default('aprobado'),
  requiresIpProtection: boolean('requires_ip_protection').default(false),
  isFeatured: boolean('is_featured').default(false),
  paymentPhone: varchar('payment_phone', { length: 50 }),
  paymentId: varchar('payment_id', { length: 50 }),
  paymentBank: varchar('payment_bank', { length: 100 }),
  managerId: uuid('manager_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
  tenantIdx: index('events_tenant_idx').on(table.tenantId),
  visibilityIdx: index('events_visibility_idx').on(table.visibility),
  statusIdx: index('events_status_idx').on(table.status),
  dateIdx: index('events_date_idx').on(table.date),
}));

Event Status Lifecycle

The status field controls whether an event is publicly visible and whether attendees can register.
1

event_manager creates the event

When an event_manager submits a new event, it is stored with status = 'pendiente'. It is not yet visible to the public and cannot accept registrations.
2

tenant_admin reviews

The faculty admin reviews the event details in the /faculty-admin portal.
3

Approved or rejected

  • Approvedstatus is updated to 'aprobado'. The event becomes public (subject to visibility rules) and registrations open.
  • Rejectedstatus is updated to 'rechazado'. The event_manager can revise and resubmit.
Events created directly by a tenant_admin or superadmin bypass the approval step and are stored with status = 'aprobado' immediately.
event_manager creates event


   status: 'pendiente'

  tenant_admin reviews

   ┌────┴────┐
   ▼         ▼
'aprobado' 'rechazado'

Space Conflict Detection

Before any event is written to the database, EventsService.checkSpaceConflict verifies that the target space is available for the entire requested time window. The algorithm uses interval overlap detection:
Two intervals [startA, endA) and [startB, endB) overlap if and only if startA < endB && startB < endA.
// src/services/events.service.ts
static async checkSpaceConflict(
  spaceId: string,
  eventDate: Date,
  durationMinutes: number,
  excludeEventId?: string
): Promise<boolean> {
  // Calculate the new event's start and end timestamps
  const newStart = eventDate.getTime();
  const newEnd = newStart + durationMinutes * 60 * 1000;

  // Query events within a ±1 day window to handle midnight edge cases
  const oneDay = 24 * 60 * 60 * 1000;
  const lowerBound = new Date(newStart - oneDay);
  const upperBound = new Date(newEnd + oneDay);

  const overlappingEvents = await db.query.events.findMany({
    where: and(
      eq(events.spaceId, spaceId),
      gte(events.date, lowerBound),
      lte(events.date, upperBound)
    ),
  });

  for (const event of overlappingEvents) {
    // Skip the event being edited (update flow)
    if (excludeEventId && event.id === excludeEventId) continue;

    const eventStart = event.date.getTime();
    const eventEnd = eventStart + event.duration * 60 * 1000;

    // Overlap condition: startA < endB && startB < endA
    if (newStart < eventEnd && eventStart < newEnd) {
      return true; // conflict found
    }
  }

  return false; // space is free
}
If a conflict is detected, createEvent throws with error code CONF_001:
// src/services/events.service.ts
static async createEvent(data: { ... }) {
  const hasConflict = await this.checkSpaceConflict(
    data.spaceId,
    data.date,
    data.duration
  );

  if (hasConflict) {
    throw new Error('CONF_001: El espacio ya está reservado para esa fecha y hora.');
  }

  const slug = await generateUniqueSlug('events', data.title);
  const [newEvent] = await db.insert(events).values({ ...data, slug }).returning();
  return newEvent;
}
The same check runs during updateEvent, passing the current event’s id as excludeEventId so the event does not conflict with itself:
// src/services/events.service.ts — update flow
const hasConflict = await this.checkSpaceConflict(
  spaceId,
  eventDate,
  duration,
  id // exclude this event from the conflict check
);
Conflict detection only applies to events within the same spaceId. Two events in different spaces — even at the same time — do not conflict with each other.

Visibility

The visibility enum controls who can discover and view an event.
ValueBehaviour
publicoListed on public event pages; discoverable by all visitors
privadoHidden from public listings; accessible only via direct link or by authenticated tenant users
// src/db/schema.ts
export const visibilityEnum = pgEnum('visibility', ['publico', 'privado']);
Events with isFeatured = true are surfaced prominently on the home page regardless of which tenant created them. Only tenant_admin and superadmin can toggle the featured flag.
Use privado visibility for internal faculty events (staff meetings, restricted workshops) and reserve isFeatured for high-profile events you want to promote across the entire university portal.

Build docs developers (and LLMs) love