Files
setrip/lib/itinerary.ts
T
arifal c4efe4453b -
- 
- 
- 
2026-05-18 18:31:16 +07:00

115 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { LIMITS } from "@/lib/limits";
/**
* Item itinerary terstruktur. Format jam: "HH:mm" 24-jam.
* Bentuk ini dipakai di form, action, dan render — DB-nya pakai
* `TripItineraryItem` (lihat schema.prisma).
*/
export interface ItineraryItemInput {
day: number;
startTime: string;
endTime?: string | null;
activity: string;
}
const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
export function isValidTimeFormat(value: string): boolean {
return TIME_RE.test(value);
}
/**
* Konversi "HH:mm" ke total menit sejak 00:00. Pakai untuk perbandingan
* jam mulai vs jam selesai. Mengembalikan NaN kalau format invalid.
*/
export function timeToMinutes(value: string): number {
if (!isValidTimeFormat(value)) return Number.NaN;
const [h, m] = value.split(":").map(Number);
return h * 60 + m;
}
/**
* Format display jam (sudah HH:mm di DB, sekedar pass-through dengan
* trimming defensif).
*/
export function formatItineraryTime(value: string): string {
return value.trim();
}
/**
* Kelompokkan items per hari, urut ascending. Item dalam satu hari diurut
* berdasarkan `order` lalu `startTime`.
*/
export function groupItineraryByDay<
T extends { day: number; order: number; startTime: string },
>(items: T[]): Map<number, T[]> {
const grouped = new Map<number, T[]>();
for (const item of items) {
const list = grouped.get(item.day) ?? [];
list.push(item);
grouped.set(item.day, list);
}
for (const list of grouped.values()) {
list.sort((a, b) => {
if (a.order !== b.order) return a.order - b.order;
return timeToMinutes(a.startTime) - timeToMinutes(b.startTime);
});
}
return new Map(
[...grouped.entries()].sort(([a], [b]) => a - b)
);
}
/**
* Validasi semantik (selain Zod): jam selesai harus >= jam mulai (kalau diisi),
* dan jumlah day harus rapat (1..N tanpa lompat) untuk menjaga UX builder
* tetap deterministik. Return error pertama yang ditemui, atau null kalau OK.
*/
export function validateItineraryItems(
items: ItineraryItemInput[]
): string | null {
if (items.length === 0) return null;
if (items.length > LIMITS.MAX_ITINERARY_ITEMS) {
return `Maksimal ${LIMITS.MAX_ITINERARY_ITEMS} aktivitas total`;
}
const days = new Set<number>();
for (const item of items) {
if (!Number.isInteger(item.day) || item.day < 1) {
return "Nomor hari tidak valid";
}
if (item.day > LIMITS.MAX_ITINERARY_DAYS) {
return `Maksimal ${LIMITS.MAX_ITINERARY_DAYS} hari`;
}
if (!isValidTimeFormat(item.startTime)) {
return "Format jam mulai harus HH:mm (00:00 23:59)";
}
if (item.endTime && !isValidTimeFormat(item.endTime)) {
return "Format jam selesai harus HH:mm (00:00 23:59)";
}
if (
item.endTime &&
timeToMinutes(item.endTime) < timeToMinutes(item.startTime)
) {
return "Jam selesai tidak boleh sebelum jam mulai";
}
const trimmedActivity = item.activity.trim();
if (trimmedActivity.length === 0) {
return "Deskripsi aktivitas harus diisi";
}
if (trimmedActivity.length > LIMITS.MAX_ITINERARY_ACTIVITY_LENGTH) {
return `Aktivitas maksimal ${LIMITS.MAX_ITINERARY_ACTIVITY_LENGTH} karakter`;
}
days.add(item.day);
}
const sortedDays = [...days].sort((a, b) => a - b);
for (let i = 0; i < sortedDays.length; i++) {
if (sortedDays[i] !== i + 1) {
return "Nomor hari harus berurutan mulai dari Hari 1 (tidak boleh lompat)";
}
}
return null;
}