feat: implement lesson creation directly on the schedule calendar via a new API endpoint and database updates.

This commit is contained in:
kertenkerem
2026-01-08 02:57:38 +03:00
parent 721e003253
commit cd542259a9
5 changed files with 287 additions and 18 deletions

Binary file not shown.

View File

@@ -0,0 +1,65 @@
import { prisma } from "@/infrastructure/db/prisma";
import { NextResponse } from "next/server";
export async function GET() {
try {
const lessons = await prisma.lesson.findMany({
include: {
instructor: true,
branch: true,
class: true,
},
});
return NextResponse.json(lessons);
} catch (error) {
return NextResponse.json({ error: "Dersler yüklenemedi" }, { status: 500 });
}
}
export async function POST(request: Request) {
try {
const json = await request.json();
const { startTime, endTime, instructorId, branchId, classId, type, hallNumber, occurrenceCount = 1, period = 'NONE' } = json;
const start = new Date(startTime);
const end = new Date(endTime);
const lessons = [];
for (let i = 0; i < occurrenceCount; i++) {
const currentStart = new Date(start);
const currentEnd = new Date(end);
if (period === 'DAILY') {
currentStart.setDate(start.getDate() + i);
currentEnd.setDate(end.getDate() + i);
} else if (period === 'WEEKLY') {
currentStart.setDate(start.getDate() + (i * 7));
currentEnd.setDate(end.getDate() + (i * 7));
} else if (period === 'MONTHLY') {
currentStart.setMonth(start.getMonth() + i);
currentEnd.setMonth(end.getMonth() + i);
} else if (period === 'SIX_MONTHLY') {
currentStart.setMonth(start.getMonth() + (i * 6));
currentEnd.setMonth(end.getMonth() + (i * 6));
}
const lesson = await prisma.lesson.create({
data: {
startTime: currentStart,
endTime: currentEnd,
type,
hallNumber: parseInt(hallNumber),
branchId,
instructorId,
classId: classId || null,
}
});
lessons.push(lesson);
}
return NextResponse.json({ message: `${lessons.length} ders başarıyla oluşturuldu`, lessons });
} catch (error: any) {
console.error('Lesson creation error:', error);
return NextResponse.json({ error: "Ders oluşturulamadı", details: error.message }, { status: 500 });
}
}

View File

@@ -46,7 +46,7 @@ export default async function Home() {
<main className={styles.main}>
<ScheduleCalendar lessons={lessons} hallCount={hallCount} />
<ScheduleCalendar lessons={lessons} hallCount={hallCount} branchId={branch?.id} />
</main>
<footer className={styles.footer}>

View File

@@ -1,5 +1,6 @@
'use client';
import { useMemo, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
interface Lesson {
id: string;
@@ -13,13 +14,28 @@ interface Lesson {
hallNumber?: number | null;
}
export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[], hallCount?: number }) {
export function ScheduleCalendar({ lessons, hallCount = 1, branchId }: { lessons: Lesson[], hallCount?: number, branchId?: string }) {
const router = useRouter();
const days = ['Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi', 'Pazar'];
const hours = Array.from({ length: 24 }, (_, i) => i);
const scrollRef = useRef<HTMLDivElement>(null);
const [now, setNow] = useState(new Date());
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedSlot, setSelectedSlot] = useState<{ dayIdx: number, hour: number, hNum: number } | null>(null);
const [instructors, setInstructors] = useState<any[]>([]);
const [classes, setClasses] = useState<any[]>([]);
const [formData, setFormData] = useState({
instructorId: '',
classId: '',
type: 'Group',
period: 'NONE',
occurrenceCount: 1,
duration: 60
});
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 60000);
if (scrollRef.current) {
@@ -30,6 +46,66 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
return () => clearInterval(timer);
}, []);
const fetchFormData = async () => {
const [instRes, classRes] = await Promise.all([
fetch('/api/instructors'),
fetch('/api/classes')
]);
const instData = await instRes.json();
const classData = await classRes.json();
setInstructors(instData);
setClasses(classData);
};
const handleSlotClick = (dayIdx: number, hour: number, hNum: number) => {
if (!branchId) return;
setSelectedSlot({ dayIdx, hour, hNum });
setIsModalOpen(true);
fetchFormData();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedSlot || !branchId) return;
// Calculate actual date for the selected slot in the current week
const now = new Date();
const dayOfWeek = now.getDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
const startOfWeek = new Date(now.setDate(diff));
const startTime = new Date(startOfWeek);
startTime.setDate(startOfWeek.getDate() + selectedSlot.dayIdx);
startTime.setHours(selectedSlot.hour, 0, 0, 0);
const endTime = new Date(startTime);
endTime.setMinutes(startTime.getMinutes() + parseInt(formData.duration.toString()));
try {
const res = await fetch('/api/lessons', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formData,
branchId,
hallNumber: selectedSlot.hNum,
startTime,
endTime
})
});
if (res.ok) {
setIsModalOpen(false);
router.refresh();
} else {
const errData = await res.json();
alert(`Ders oluşturulamadı: ${errData.details || errData.error || 'Bilinmeyen hata'}`);
}
} catch (error) {
console.error(error);
}
};
const nowTop = (now.getHours() * 60) + now.getMinutes();
const groupedLessons = useMemo(() => {
@@ -54,10 +130,8 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
return groups;
}, [lessons]);
// Dimensions
const timeWidth = 60;
const dayMinWidth = 150;
const hallMinWidth = dayMinWidth / hallCount;
const dayMinWidth = 180;
return (
<div style={{
@@ -71,11 +145,10 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
fontFamily: 'sans-serif'
}}>
<h1 style={{ textAlign: 'center', padding: '10px', margin: 0, fontSize: '1.2rem', borderBottom: '1px solid #333' }}>
Haftalık Ders Programı (Rebuilt)
Haftalık Ders Programı
</h1>
<div ref={scrollRef} style={{ flex: 1, overflow: 'auto', position: 'relative' }}>
{/* Header (Sticky) */}
<div style={{ position: 'sticky', top: 0, zIndex: 100, backgroundColor: '#000', borderBottom: '1px solid #333' }}>
<div style={{ display: 'flex', marginLeft: timeWidth }}>
{days.map((day, idx) => {
@@ -106,8 +179,9 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
textAlign: 'center',
fontSize: '0.6rem',
color: '#666',
borderRight: '1px solid #222',
padding: '2px 0'
borderRight: hIdx === hallCount - 1 ? '1px solid #333' : '1px solid #222',
padding: '4px 0',
backgroundColor: '#050505'
}}>
S{hIdx + 1}
</div>
@@ -118,9 +192,7 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
)}
</div>
{/* Grid Body */}
<div style={{ display: 'flex', minHeight: '1440px', position: 'relative' }}>
{/* Time Axis */}
<div style={{ width: timeWidth, position: 'sticky', left: 0, zIndex: 50, backgroundColor: '#000', borderRight: '1px solid #333' }}>
{hours.map(h => (
<div key={h} style={{ height: '60px', paddingRight: '5px', textAlign: 'right', fontSize: '0.7rem', color: '#666', paddingTop: '2px' }}>
@@ -129,9 +201,7 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
))}
</div>
{/* Main Content Area */}
<div style={{ flex: 1, position: 'relative', display: 'flex', backgroundImage: 'linear-gradient(#222 1px, transparent 1px)', backgroundSize: '100% 60px' }}>
{/* Now Line */}
<div style={{ position: 'absolute', top: nowTop, left: 0, right: 0, height: '2px', backgroundColor: 'red', zIndex: 40, pointerEvents: 'none' }}>
<div style={{ position: 'absolute', left: '-5px', top: '-4px', width: '10px', height: '10px', borderRadius: '50%', backgroundColor: 'red' }} />
</div>
@@ -142,6 +212,30 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
const hNum = hIdx + 1;
return (
<div key={hIdx} style={{ flex: 1, position: 'relative', borderRight: hallCount > 1 ? '1px solid #111' : 'none' }}>
{/* Hoverable Slots */}
{hours.map(h => (
<div
key={h}
onClick={() => handleSlotClick(dIdx, h, hNum)}
style={{ height: '60px', position: 'relative', cursor: 'pointer' }}
className="slot-hover"
>
<div className="plus-btn" style={{
position: 'absolute',
right: '5px',
top: '5px',
width: '20px',
height: '20px',
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: '50%',
display: 'none',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px'
}}>+</div>
</div>
))}
{groupedLessons[dIdx]?.[hNum]?.map(lesson => (
<div key={lesson.id} style={{
position: 'absolute',
@@ -149,13 +243,13 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
height: lesson.height,
left: '2px',
right: '2px',
backgroundColor: lesson.type === 'Individual' ? 'rgba(255,0,0,0.1)' : 'rgba(0,120,255,0.1)',
borderLeft: `3px solid ${lesson.type === 'Individual' ? 'red' : '#0070ff'}`,
backgroundColor: lesson.type === 'Individual' ? 'rgba(242, 139, 130, 0.2)' : 'rgba(138, 180, 248, 0.2)',
borderLeft: `3px solid ${lesson.type === 'Individual' ? '#f28b82' : '#8ab4f8'}`,
borderRadius: '4px',
padding: '4px',
fontSize: '0.7rem',
overflow: 'hidden',
zIndex: 1
zIndex: 10
}}>
<div style={{ fontWeight: 'bold', fontSize: '0.6rem', opacity: 0.8 }}>{lesson.startTimeStr}</div>
<div style={{ fontWeight: 'bold', margin: '2px 0' }}>{lesson.class?.name || lesson.name}</div>
@@ -170,6 +264,117 @@ export function ScheduleCalendar({ lessons, hallCount = 1 }: { lessons: Lesson[]
</div>
</div>
</div>
{/* Reservation Modal */}
{isModalOpen && (
<div style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 200
}}>
<form onSubmit={handleSubmit} style={{
backgroundColor: '#1e1e1e',
padding: '2rem',
borderRadius: '8px',
width: '400px',
border: '1px solid #333',
display: 'flex',
flexDirection: 'column',
gap: '1rem'
}}>
<h3>Yeni Ders Rezervasyonu</h3>
<p style={{ fontSize: '0.8rem', color: '#9aa0a6' }}>
{days[selectedSlot!.dayIdx]} - Saat: {selectedSlot!.hour}:00 - Salon: {selectedSlot!.hNum}
</p>
<label>Eğitmen</label>
<select
required
value={formData.instructorId}
onChange={(e) => setFormData({ ...formData, instructorId: e.target.value })}
style={{ padding: '8px', backgroundColor: '#2b2b2b', color: '#fff', border: '1px solid #444' }}
>
<option value="">Seçiniz</option>
{instructors.map(i => <option key={i.id} value={i.id}>{i.name}</option>)}
</select>
<label>Sınıf (Opsiyonel)</label>
<select
value={formData.classId}
onChange={(e) => setFormData({ ...formData, classId: e.target.value })}
style={{ padding: '8px', backgroundColor: '#2b2b2b', color: '#fff', border: '1px solid #444' }}
>
<option value="">Seçiniz</option>
{classes.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
<label>Ders Türü</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
style={{ padding: '8px', backgroundColor: '#2b2b2b', color: '#fff', border: '1px solid #444' }}
>
<option value="Group">Grup Dersi</option>
<option value="Individual">Özel Ders</option>
</select>
<label>Süre (Dakika)</label>
<input
type="number"
value={formData.duration}
onChange={(e) => setFormData({ ...formData, duration: parseInt(e.target.value) })}
style={{ padding: '8px', backgroundColor: '#2b2b2b', color: '#fff', border: '1px solid #444' }}
/>
<label>Tekrar</label>
<select
value={formData.period}
onChange={(e) => setFormData({ ...formData, period: e.target.value })}
style={{ padding: '8px', backgroundColor: '#2b2b2b', color: '#fff', border: '1px solid #444' }}
>
<option value="NONE">Tek Seferlik</option>
<option value="DAILY">Günlük</option>
<option value="WEEKLY">Haftalık</option>
<option value="MONTHLY">Aylık</option>
<option value="SIX_MONTHLY">6 Aylık</option>
</select>
{formData.period !== 'NONE' && (
<>
<label>Tekrar Sayısı</label>
<input
type="number"
value={formData.occurrenceCount}
onChange={(e) => setFormData({ ...formData, occurrenceCount: parseInt(e.target.value) })}
style={{ padding: '8px', backgroundColor: '#2b2b2b', color: '#fff', border: '1px solid #444' }}
/>
</>
)}
<div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
<button type="submit" style={{ flex: 1, padding: '10px', backgroundColor: '#8ab4f8', color: '#000', border: 'none', borderRadius: '4px', fontWeight: 'bold', cursor: 'pointer' }}>
Kaydet
</button>
<button type="button" onClick={() => setIsModalOpen(false)} style={{ flex: 1, padding: '10px', backgroundColor: 'transparent', color: '#fff', border: '1px solid #444', borderRadius: '4px', cursor: 'pointer' }}>
İptal
</button>
</div>
</form>
</div>
)}
<style jsx>{`
.slot-hover:hover .plus-btn {
display: flex !important;
}
.slot-hover:hover {
background-color: rgba(255,255,255,0.05);
}
`}</style>
</div>
);
}

View File

@@ -8,12 +8,11 @@ export function Sidebar() {
const router = useRouter();
const navItems = [
{ name: 'Takvime Git 🗓️', href: '/' },
{ name: 'Dashboard', href: '/admin/dashboard' },
{ name: 'Şubeler', href: '/admin/branches' },
{ name: 'Hocalar', href: '/admin/instructors' },
{ name: 'Sınıflar', href: '/admin/classes' },
{ name: 'Ders Programı', href: '/admin/lessons' },
{ name: 'Ücretler', href: '/admin/fees' },
{ name: 'Kayıt', href: '/admin/register' },
];