feat: implement lesson creation directly on the schedule calendar via a new API endpoint and database updates.
This commit is contained in:
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
65
src/app/api/lessons/route.ts
Normal file
65
src/app/api/lessons/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user