250 lines
6.4 KiB
TypeScript
250 lines
6.4 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
import { Prisma } from "@/app/generated/prisma/client";
|
|
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
|
|
import {
|
|
utcStartOfDay,
|
|
utcDayStartFromYmd,
|
|
utcDayEndFromYmd,
|
|
maxUtcDate,
|
|
} from "@/lib/trip-dates";
|
|
|
|
export type GroupSize = "SMALL" | "MEDIUM" | "LARGE";
|
|
|
|
export interface TripFilters {
|
|
q?: string;
|
|
from?: string;
|
|
to?: string;
|
|
category?: ActivityCategory;
|
|
vibe?: Vibe;
|
|
groupSize?: GroupSize;
|
|
}
|
|
|
|
export const tripRepo = {
|
|
async findAll() {
|
|
return prisma.trip.findMany({
|
|
include: {
|
|
organizer: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
image: true,
|
|
organizerVerification: { select: { status: true } },
|
|
},
|
|
},
|
|
images: { orderBy: { order: "asc" }, take: 1 },
|
|
_count: {
|
|
select: {
|
|
participants: { where: { status: { not: "CANCELLED" } } },
|
|
},
|
|
},
|
|
},
|
|
orderBy: { date: "asc" },
|
|
});
|
|
},
|
|
|
|
async findOpen(filters?: TripFilters) {
|
|
const todayStart = utcStartOfDay(new Date());
|
|
|
|
const andParts: Prisma.TripWhereInput[] = [{ status: "OPEN" }];
|
|
|
|
if (filters?.category) {
|
|
andParts.push({ category: filters.category });
|
|
}
|
|
|
|
if (filters?.vibe) {
|
|
andParts.push({ vibe: filters.vibe });
|
|
}
|
|
|
|
if (filters?.groupSize === "SMALL") {
|
|
andParts.push({ maxParticipants: { lte: 10 } });
|
|
} else if (filters?.groupSize === "MEDIUM") {
|
|
andParts.push({ maxParticipants: { gte: 11, lte: 20 } });
|
|
} else if (filters?.groupSize === "LARGE") {
|
|
andParts.push({ maxParticipants: { gte: 21 } });
|
|
}
|
|
|
|
if (!filters?.from && !filters?.to) {
|
|
andParts.push({ date: { gte: todayStart } });
|
|
} else {
|
|
const userRangeStart = filters.from
|
|
? utcDayStartFromYmd(filters.from)
|
|
: todayStart;
|
|
const userRangeEnd = filters.to
|
|
? utcDayEndFromYmd(filters.to)
|
|
: utcDayEndFromYmd("2099-12-31");
|
|
|
|
const rangeStart = maxUtcDate(todayStart, userRangeStart);
|
|
const rangeEnd = userRangeEnd;
|
|
|
|
andParts.push({
|
|
OR: [
|
|
{
|
|
AND: [
|
|
{ endDate: { not: null } },
|
|
{ date: { lte: rangeEnd } },
|
|
{ endDate: { gte: rangeStart } },
|
|
],
|
|
},
|
|
{
|
|
AND: [
|
|
{ endDate: null },
|
|
{ date: { gte: rangeStart } },
|
|
{ date: { lte: rangeEnd } },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
if (filters?.q) {
|
|
andParts.push({
|
|
OR: [
|
|
{ title: { contains: filters.q, mode: "insensitive" } },
|
|
{ destination: { contains: filters.q, mode: "insensitive" } },
|
|
{ location: { contains: filters.q, mode: "insensitive" } },
|
|
],
|
|
});
|
|
}
|
|
|
|
const where: Prisma.TripWhereInput = { AND: andParts };
|
|
|
|
return prisma.trip.findMany({
|
|
where,
|
|
include: {
|
|
organizer: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
image: true,
|
|
organizerVerification: { select: { status: true } },
|
|
},
|
|
},
|
|
images: { orderBy: { order: "asc" }, take: 1 },
|
|
participants: {
|
|
where: { status: "CONFIRMED" },
|
|
take: 10,
|
|
orderBy: { createdAt: "asc" },
|
|
select: {
|
|
id: true,
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
image: true,
|
|
profile: { select: { interests: true } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
_count: {
|
|
select: {
|
|
participants: { where: { status: { not: "CANCELLED" } } },
|
|
},
|
|
},
|
|
},
|
|
orderBy: { date: "asc" },
|
|
});
|
|
},
|
|
|
|
async findById(id: string) {
|
|
return prisma.trip.findUnique({
|
|
where: { id },
|
|
include: {
|
|
organizer: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
image: true,
|
|
organizerVerification: { select: { status: true } },
|
|
},
|
|
},
|
|
images: { orderBy: { order: "asc" } },
|
|
participants: {
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
image: true,
|
|
profile: { select: { city: true, interests: true } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
reviews: {
|
|
orderBy: { createdAt: "desc" },
|
|
include: {
|
|
user: { select: { id: true, name: true, image: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
},
|
|
|
|
async countByOrganizerSince(organizerId: string, since: Date) {
|
|
return prisma.trip.count({
|
|
where: { organizerId, createdAt: { gte: since } },
|
|
});
|
|
},
|
|
|
|
/** Semua trip yang dibuat user (semua status), terbaru dulu — untuk profil. */
|
|
async findByOrganizerId(organizerId: string) {
|
|
return prisma.trip.findMany({
|
|
where: { organizerId },
|
|
include: {
|
|
images: { orderBy: { order: "asc" }, take: 1 },
|
|
_count: {
|
|
select: {
|
|
participants: { where: { status: { not: "CANCELLED" } } },
|
|
},
|
|
},
|
|
},
|
|
orderBy: { date: "desc" },
|
|
});
|
|
},
|
|
|
|
async create(data: Prisma.TripCreateInput) {
|
|
return prisma.trip.create({ data });
|
|
},
|
|
|
|
async updateStatus(id: string, status: "OPEN" | "FULL" | "CLOSED" | "COMPLETED") {
|
|
return prisma.trip.update({ where: { id }, data: { status } });
|
|
},
|
|
|
|
/**
|
|
* Bulk transisi trip yang sudah lewat `cutoff` (start of today UTC) dari
|
|
* status OPEN/FULL ke COMPLETED. Idempotent — second run tidak akan match
|
|
* apa-apa karena status sudah berubah.
|
|
*
|
|
* Returns daftar id yang ter-update untuk telemetri/log.
|
|
*/
|
|
async bulkCompletePastTrips(cutoff: Date) {
|
|
const trips = await prisma.trip.findMany({
|
|
where: {
|
|
status: { in: ["OPEN", "FULL"] },
|
|
OR: [
|
|
{ endDate: { lt: cutoff } },
|
|
{ AND: [{ endDate: null }, { date: { lt: cutoff } }] },
|
|
],
|
|
},
|
|
select: { id: true },
|
|
});
|
|
|
|
if (trips.length === 0) {
|
|
return { count: 0, ids: [] as string[] };
|
|
}
|
|
|
|
const ids = trips.map((t) => t.id);
|
|
const result = await prisma.trip.updateMany({
|
|
where: {
|
|
id: { in: ids },
|
|
status: { in: ["OPEN", "FULL"] },
|
|
},
|
|
data: { status: "COMPLETED" },
|
|
});
|
|
|
|
return { count: result.count, ids };
|
|
},
|
|
};
|