feat: Implement student CRUD operations in admin panel with dedicated API routes and updated Prisma schema.

This commit is contained in:
kertenkerem
2026-01-08 01:52:00 +03:00
parent b7f98ed3ad
commit 6880c738f9
11 changed files with 251 additions and 153 deletions

Binary file not shown.

View File

@@ -19,12 +19,12 @@ model Branch {
name String name String
address String? address String?
phone String? phone String?
instructors Instructor[] // Implicit many-to-many createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
classes DanceClass[] classes DanceClass[]
lessons Lesson[] lessons Lesson[]
students Student[] students Student[]
createdAt DateTime @default(now()) instructors Instructor[] @relation("BranchToInstructor")
updatedAt DateTime @updatedAt
} }
model Instructor { model Instructor {
@@ -32,41 +32,41 @@ model Instructor {
name String name String
bio String? bio String?
phone String? phone String?
branches Branch[] // Implicit many-to-many
classes DanceClass[]
lessons Lesson[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
classes DanceClass[]
lessons Lesson[]
branches Branch[] @relation("BranchToInstructor")
} }
model DanceClass { model DanceClass {
id String @id @default(uuid()) id String @id @default(uuid())
name String name String
description String? description String?
branchId String branchId String
branch Branch @relation(fields: [branchId], references: [id])
instructorId String? instructorId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
instructor Instructor? @relation(fields: [instructorId], references: [id]) instructor Instructor? @relation(fields: [instructorId], references: [id])
lessons Lesson[] branch Branch @relation(fields: [branchId], references: [id])
fees Fee[] fees Fee[]
createdAt DateTime @default(now()) lessons Lesson[]
updatedAt DateTime @updatedAt
} }
model Lesson { model Lesson {
id String @id @default(uuid()) id String @id @default(uuid())
name String? // Optional name e.g. "Special Workshop" name String?
startTime DateTime startTime DateTime
endTime DateTime endTime DateTime
type String // GROUP, PRIVATE type String
branchId String branchId String
branch Branch @relation(fields: [branchId], references: [id])
instructorId String instructorId String
instructor Instructor @relation(fields: [instructorId], references: [id])
classId String? classId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
class DanceClass? @relation(fields: [classId], references: [id]) class DanceClass? @relation(fields: [classId], references: [id])
createdAt DateTime @default(now()) instructor Instructor @relation(fields: [instructorId], references: [id])
updatedAt DateTime @updatedAt branch Branch @relation(fields: [branchId], references: [id])
} }
model Fee { model Fee {
@@ -74,11 +74,11 @@ model Fee {
name String name String
amount Float amount Float
currency String @default("TRY") currency String @default("TRY")
type String // MONTHLY, PER_LESSON, PACKAGE type String
classId String? classId String?
class DanceClass? @relation(fields: [classId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
class DanceClass? @relation(fields: [classId], references: [id])
} }
model Student { model Student {
@@ -88,7 +88,7 @@ model Student {
phone String phone String
birthDate DateTime birthDate DateTime
branchId String? branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
branch Branch? @relation(fields: [branchId], references: [id])
} }

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import styles from './register.module.css'; import { useRouter } from 'next/navigation';
import styles from '../branches/branches.module.css';
export function RegisterForm({ branches }: { branches: any[] }) { export function RegisterForm({ branches }: { branches: any[] }) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -10,12 +11,10 @@ export function RegisterForm({ branches }: { branches: any[] }) {
birthDate: '', birthDate: '',
branchId: '' branchId: ''
}); });
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle'); const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setStatus('idle');
const res = await fetch('/api/register', { const res = await fetch('/api/register', {
method: 'POST', method: 'POST',
body: JSON.stringify(formData), body: JSON.stringify(formData),
@@ -23,10 +22,8 @@ export function RegisterForm({ branches }: { branches: any[] }) {
}); });
if (res.ok) { if (res.ok) {
setStatus('success');
setFormData({ name: '', email: '', phone: '', birthDate: '', branchId: '' }); setFormData({ name: '', email: '', phone: '', birthDate: '', branchId: '' });
} else { router.refresh();
setStatus('error');
} }
}; };
@@ -35,41 +32,38 @@ export function RegisterForm({ branches }: { branches: any[] }) {
}; };
return ( return (
<form onSubmit={handleSubmit} className={styles.card}> <form onSubmit={handleSubmit} className={styles.form}>
{status === 'success' && <div className={styles.success}>Kayıt başarıyla oluşturuldu!</div>} <div className={styles.inputGroup}>
{status === 'error' && <div className={styles.error}>Bir hata oluştu. Lütfen tekrar deneyin.</div>} <label>Ad Soyad</label>
<div className={styles.formGroup}>
<label className={styles.label}>Ad Soyad</label>
<input name="name" value={formData.name} onChange={handleChange} className={styles.input} required placeholder="Adınız" /> <input name="name" value={formData.name} onChange={handleChange} className={styles.input} required placeholder="Adınız" />
</div> </div>
<div className={styles.formGroup}> <div className={styles.inputGroup}>
<label className={styles.label}>Email (İsteğe bağlı)</label> <label>Email</label>
<input name="email" type="email" value={formData.email} onChange={handleChange} className={styles.input} placeholder="ornek@email.com" /> <input name="email" type="email" value={formData.email} onChange={handleChange} className={styles.input} placeholder="ornek@email.com" />
</div> </div>
<div className={styles.formGroup}> <div className={styles.inputGroup}>
<label className={styles.label}>Telefon</label> <label>Telefon</label>
<input name="phone" type="tel" value={formData.phone} onChange={handleChange} className={styles.input} required placeholder="5XX XXX XX XX" /> <input name="phone" type="tel" value={formData.phone} onChange={handleChange} className={styles.input} required placeholder="5XX XXX XX XX" />
</div> </div>
<div className={styles.formGroup}> <div className={styles.inputGroup}>
<label className={styles.label}>Doğum Tarihi</label> <label>Doğum Tarihi</label>
<input name="birthDate" type="date" value={formData.birthDate} onChange={handleChange} className={styles.input} required /> <input name="birthDate" type="date" value={formData.birthDate} onChange={handleChange} className={styles.input} required />
</div> </div>
<div className={styles.formGroup}> <div className={styles.inputGroup}>
<label className={styles.label}>Tercih Edilen Şube</label> <label>Şube</label>
<select name="branchId" value={formData.branchId} onChange={handleChange} className={styles.select}> <select name="branchId" value={formData.branchId} onChange={handleChange} className={styles.input}>
<option value="">Seçiniz (Opsiyonel)</option> <option value="">Seçiniz</option>
{branches.map(b => ( {branches.map(b => (
<option key={b.id} value={b.id}>{b.name}</option> <option key={b.id} value={b.id}>{b.name}</option>
))} ))}
</select> </select>
</div> </div>
<button type="submit" className={styles.button}>Kayıt Ol</button> <button type="submit" className={styles.btn}>Kaydet</button>
</form> </form>
); );
} }

View File

@@ -0,0 +1,134 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import styles from '../branches/branches.module.css';
export function StudentList({ students, branches }: { students: any[], branches: any[] }) {
const router = useRouter();
const [showSensitive, setShowSensitive] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editForm, setEditForm] = useState<any>(null);
const handleDelete = async (id: string) => {
if (!confirm('Kaydı silmek istediğinize emin misiniz?')) return;
const res = await fetch(`/api/students/${id}`, { method: 'DELETE' });
if (res.ok) {
router.refresh();
} else {
alert('Silme işlemi başarısız oldu.');
}
};
const startEdit = (student: any) => {
setEditingId(student.id);
setEditForm({
name: student.name,
email: student.email || '',
phone: student.phone,
branchId: student.branchId || '',
birthDate: student.birthDate ? new Date(student.birthDate).toISOString().split('T')[0] : ''
});
};
const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch(`/api/students/${editingId}`, {
method: 'PATCH',
body: JSON.stringify(editForm),
headers: { 'Content-Type': 'application/json' }
});
if (res.ok) {
setEditingId(null);
router.refresh();
} else {
alert('Güncelleme başarısız oldu.');
}
};
const maskInfo = (text: string) => {
if (!text) return 'Yok';
if (showSensitive) return text;
return text.replace(/.(?=.{4})/g, '*');
};
return (
<div className={styles.list}>
<div style={{ marginBottom: '1rem', display: 'flex', justifyContent: 'flex-end' }}>
<button
onClick={() => setShowSensitive(!showSensitive)}
className={styles.btn}
style={{ background: showSensitive ? '#666' : '#0056b3' }}
>
{showSensitive ? 'Bilgileri Gizle' : 'Bilgileri Göster'}
</button>
</div>
{students.map(s => (
<div key={s.id} className={styles.card}>
{editingId === s.id ? (
<form onSubmit={handleUpdate} style={{ width: '100%', display: 'flex', gap: '1rem', flexWrap: 'wrap', alignItems: 'flex-end' }}>
<div className={styles.inputGroup}>
<label>Ad Soyad</label>
<input
value={editForm.name}
onChange={e => setEditForm({ ...editForm, name: e.target.value })}
className={styles.input}
required
/>
</div>
<div className={styles.inputGroup}>
<label>Telefon</label>
<input
value={editForm.phone}
onChange={e => setEditForm({ ...editForm, phone: e.target.value })}
className={styles.input}
required
/>
</div>
<div className={styles.inputGroup}>
<label>Email</label>
<input
value={editForm.email}
onChange={e => setEditForm({ ...editForm, email: e.target.value })}
className={styles.input}
/>
</div>
<div className={styles.inputGroup}>
<label>Şube</label>
<select
value={editForm.branchId}
onChange={e => setEditForm({ ...editForm, branchId: e.target.value })}
className={styles.input}
>
<option value="">Seçiniz</option>
{branches.map(b => (
<option key={b.id} value={b.id}>{b.name}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button type="submit" className={styles.btn} style={{ background: '#2e7d32' }}>Kaydet</button>
<button type="button" onClick={() => setEditingId(null)} className={styles.btn}>İptal</button>
</div>
</form>
) : (
<>
<div>
<h3>{s.name}</h3>
<p>
<strong>Tel:</strong> {maskInfo(s.phone)} |
<strong> Email:</strong> {maskInfo(s.email)}
</p>
<small>{s.branch?.name || 'Şube seçilmemiş'}</small>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button onClick={() => startEdit(s)} className={styles.btn} style={{ background: '#ffa000' }}>Düzenle</button>
<button onClick={() => handleDelete(s.id)} className={`${styles.btn} ${styles.btnDelete}`}>Sil</button>
</div>
</>
)}
</div>
))}
</div>
);
}

View File

@@ -1,18 +1,27 @@
import { prisma } from '@/infrastructure/db/prisma'; import { prisma } from '@/infrastructure/db/prisma';
import { RegisterForm } from './RegisterForm'; import { RegisterForm } from './RegisterForm';
import styles from './register.module.css'; import { StudentList } from './StudentList';
import styles from '../branches/branches.module.css';
export default async function RegisterPage() { export default async function Page() {
const branches = await prisma.branch.findMany({ const [branches, students] = await Promise.all([
select: { id: true, name: true } prisma.branch.findMany({ select: { id: true, name: true } }),
}); prisma.student.findMany({
include: { branch: true },
orderBy: { createdAt: 'desc' }
})
]);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<h1 className={styles.title}>Yeni Öğrenci Kaydı</h1> <h1>Öğrenci Kayıt İşlemleri</h1>
</div> </div>
<RegisterForm branches={branches} /> <RegisterForm branches={branches} />
<div className={styles.header} style={{ marginTop: '2rem' }}>
<h2>Kayıtlı Öğrenciler</h2>
</div>
<StudentList students={students} branches={branches} />
</div> </div>
); );
} }

View File

@@ -1,90 +0,0 @@
.container {
padding: 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.title {
font-size: 2rem;
font-weight: 600;
color: #333;
}
.card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
max-width: 800px;
}
.formGroup {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.label {
font-weight: 500;
color: #555;
font-size: 0.95rem;
}
.input,
.select {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
color: #333;
background-color: #fff;
transition: border-color 0.2s;
}
.input:focus,
.select:focus {
outline: none;
border-color: #333;
}
.button {
padding: 0.75rem 2rem;
background: #333;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 1rem;
align-self: flex-start;
}
.button:hover {
background: #000;
}
.success {
background-color: #e8f5e9;
color: #2e7d32;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #c8e6c9;
}
.error {
background-color: #ffebee;
color: #c62828;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
border: 1px solid #ffcdd2;
}

View File

@@ -3,11 +3,12 @@ import { prisma } from '@/infrastructure/db/prisma';
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { id } = await params;
await prisma.branch.delete({ await prisma.branch.delete({
where: { id: params.id }, where: { id },
}); });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {
@@ -17,12 +18,13 @@ export async function DELETE(
export async function PUT( export async function PUT(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { id } = await params;
const json = await request.json(); const json = await request.json();
const branch = await prisma.branch.update({ const branch = await prisma.branch.update({
where: { id: params.id }, where: { id },
data: json, data: json,
}); });
return NextResponse.json(branch); return NextResponse.json(branch);

View File

@@ -3,11 +3,12 @@ import { prisma } from '@/infrastructure/db/prisma';
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { id } = await params;
await prisma.danceClass.delete({ await prisma.danceClass.delete({
where: { id: params.id }, where: { id },
}); });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {

View File

@@ -3,11 +3,12 @@ import { prisma } from '@/infrastructure/db/prisma';
export async function DELETE( export async function DELETE(
request: Request, request: Request,
{ params }: { params: { id: string } } { params }: { params: Promise<{ id: string }> }
) { ) {
try { try {
const { id } = await params;
await prisma.instructor.delete({ await prisma.instructor.delete({
where: { id: params.id }, where: { id },
}); });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {

View File

@@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/infrastructure/db/prisma';
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const json = await request.json();
const student = await prisma.student.update({
where: { id },
data: {
name: json.name,
email: json.email || null,
phone: json.phone,
birthDate: json.birthDate ? new Date(json.birthDate) : undefined,
branchId: json.branchId || null
},
});
return NextResponse.json({ success: true, student });
} catch (error: any) {
console.error('Update error:', error);
return NextResponse.json({ error: 'Failed to update student', details: error.message }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
await prisma.student.delete({
where: { id },
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Delete error:', error);
return NextResponse.json({ error: 'Failed to delete student' }, { status: 500 });
}
}

View File

@@ -2,8 +2,13 @@ import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient }; const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = const prismaInstance = globalForPrisma.prisma || new PrismaClient();
globalForPrisma.prisma ||
new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma; if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prismaInstance;
// Log models to verify generation
const models = Object.keys(prismaInstance).filter(k => !k.startsWith('_') && !k.startsWith('$'));
console.log('Prisma models available:', models);
}
export const prisma = prismaInstance;