diff --git a/Dockerfile 2 b/Dockerfile 2 deleted file mode 100644 index 5e60bfdc7..000000000 --- a/Dockerfile 2 +++ /dev/null @@ -1 +0,0 @@ -FROM postgres:16.8-alpine3.21 diff --git a/Dockerfile 3 b/Dockerfile 3 deleted file mode 100644 index 5e60bfdc7..000000000 --- a/Dockerfile 3 +++ /dev/null @@ -1 +0,0 @@ -FROM postgres:16.8-alpine3.21 diff --git a/docker-compose 2.yml b/docker-compose 2.yml deleted file mode 100644 index 7adeff53d..000000000 --- a/docker-compose 2.yml +++ /dev/null @@ -1,23 +0,0 @@ -services: - db: - build: - context: . - dockerfile: Dockerfile - container_name: zotmeet-db - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: zotmeet - ports: - - "5432:5432" - volumes: - - zotmeet-db-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d zotmeet"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - zotmeet-db-data: diff --git a/docker-compose 3.yml b/docker-compose 3.yml deleted file mode 100644 index 7adeff53d..000000000 --- a/docker-compose 3.yml +++ /dev/null @@ -1,23 +0,0 @@ -services: - db: - build: - context: . - dockerfile: Dockerfile - container_name: zotmeet-db - restart: unless-stopped - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: zotmeet - ports: - - "5432:5432" - volumes: - - zotmeet-db-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres -d zotmeet"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - zotmeet-db-data: diff --git a/drizzle.config.ts b/drizzle.config.ts index 2d8599d05..dce3de099 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -3,7 +3,7 @@ import "dotenv/config"; import { defineConfig } from "drizzle-kit"; export default defineConfig({ - schema: "./src/db/schema.ts", + schema: ["./src/db/schema.ts", "./src/db/relations.ts"], out: "./src/db/migrations", dialect: "postgresql", dbCredentials: { diff --git a/src/server/actions/availability/free-times/action.ts b/src/server/actions/availability/free-times/action.ts new file mode 100644 index 000000000..0311496da --- /dev/null +++ b/src/server/actions/availability/free-times/action.ts @@ -0,0 +1,84 @@ +"use server"; + +import { getUserAvailabilitiesAndScheduled } from "@data/availability/queries"; +import { fromZonedTime } from "date-fns-tz"; +import { getCurrentSession } from "@/lib/auth"; +import { BLOCK_LENGTH } from "@/lib/availability/utils"; + +function expandScheduledBlockToISOSlots( + scheduledDate: Date, + fromTime: string, + toTime: string, + timezone: string, +): Set { + const slots = new Set(); + const datePart = scheduledDate.toISOString().substring(0, 10); + + const [fh, fm] = fromTime.split(":").map(Number); + const [th, tm] = toTime.split(":").map(Number); + + let currentMinutes = fh * 60 + fm; + const endMinutes = th * 60 + tm; + const effectiveEndMinutes = + endMinutes < currentMinutes ? endMinutes + 1440 : endMinutes; + while (currentMinutes < effectiveEndMinutes) { + const wrappedMinutes = currentMinutes % 1440; + const h = Math.floor(wrappedMinutes / 60) + .toString() + .padStart(2, "0"); + const m = (wrappedMinutes % 60).toString().padStart(2, "0"); + let slotDatePart = datePart; + if (currentMinutes >= 1440) { + const nextDay = new Date(scheduledDate); + nextDay.setUTCDate(nextDay.getUTCDate() + 1); + slotDatePart = nextDay.toISOString().substring(0, 10); + } + const utcDate = fromZonedTime(`${slotDatePart}T${h}:${m}:00`, timezone); + slots.add(utcDate.toISOString()); + currentMinutes += BLOCK_LENGTH; + } + + return slots; +} + +export async function getUserFreeTimes(): Promise< + { freeTimes: string[] } | { error: string } +> { + const { user } = await getCurrentSession(); + + if (!user) { + return { error: "You must be logged in to view free times." }; + } + + try { + const { userAvailabilities, scheduledBlocks } = + await getUserAvailabilitiesAndScheduled(user.memberId); + + const scheduledSlots = new Set(); + for (const block of scheduledBlocks) { + const blockSlots = expandScheduledBlockToISOSlots( + block.scheduledDate, + block.scheduledFromTime, + block.scheduledToTime, + block.timezone, + ); + for (const slot of blockSlots) { + scheduledSlots.add(slot); + } + } + + const freeTimes = new Set(); + for (const avail of userAvailabilities) { + for (const isoString of avail.meetingAvailabilities) { + if (!scheduledSlots.has(isoString)) { + freeTimes.add(isoString); + } + } + } + + return { freeTimes: [...freeTimes].sort() }; + } catch (error) { + console.error("Failed to fetch free times:", error); + return { error: "Failed to fetch free times." }; + } +} diff --git a/src/server/data/availability/queries.ts b/src/server/data/availability/queries.ts index 1e48feae1..3191d989b 100644 --- a/src/server/data/availability/queries.ts +++ b/src/server/data/availability/queries.ts @@ -1,8 +1,8 @@ import "server-only"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import { db } from "@/db"; -import { availabilities } from "@/db/schema"; +import { availabilities, meetings, scheduledMeetings } from "@/db/schema"; export async function getMemberMeetingAvailability({ memberId, @@ -24,3 +24,34 @@ export async function getMemberMeetingAvailability({ return availabilityData; } + +export async function getUserAvailabilitiesAndScheduled(memberId: string) { + const userAvailabilities = await db + .select({ + meetingId: availabilities.meetingId, + meetingAvailabilities: availabilities.meetingAvailabilities, + }) + .from(availabilities) + .innerJoin(meetings, eq(availabilities.meetingId, meetings.id)) + .where( + and(eq(availabilities.memberId, memberId), eq(meetings.archived, false)), + ); + + if (userAvailabilities.length === 0) { + return { userAvailabilities: [], scheduledBlocks: [] }; + } + const meetingIds = userAvailabilities.map((a) => a.meetingId); + + const scheduledBlocks = await db + .select({ + scheduledDate: scheduledMeetings.scheduledDate, + scheduledFromTime: scheduledMeetings.scheduledFromTime, + scheduledToTime: scheduledMeetings.scheduledToTime, + timezone: meetings.timezone, + }) + .from(scheduledMeetings) + .innerJoin(meetings, eq(scheduledMeetings.meetingId, meetings.id)) + .where(inArray(scheduledMeetings.meetingId, meetingIds)); + + return { userAvailabilities, scheduledBlocks }; +}