feat: Implement student CRUD operations in admin panel with dedicated API routes and updated Prisma schema.
This commit is contained in:
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
@@ -19,12 +19,12 @@ model Branch {
|
||||
name String
|
||||
address String?
|
||||
phone String?
|
||||
instructors Instructor[] // Implicit many-to-many
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
classes DanceClass[]
|
||||
lessons Lesson[]
|
||||
students Student[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
instructors Instructor[] @relation("BranchToInstructor")
|
||||
}
|
||||
|
||||
model Instructor {
|
||||
@@ -32,11 +32,11 @@ model Instructor {
|
||||
name String
|
||||
bio String?
|
||||
phone String?
|
||||
branches Branch[] // Implicit many-to-many
|
||||
classes DanceClass[]
|
||||
lessons Lesson[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
classes DanceClass[]
|
||||
lessons Lesson[]
|
||||
branches Branch[] @relation("BranchToInstructor")
|
||||
}
|
||||
|
||||
model DanceClass {
|
||||
@@ -44,29 +44,29 @@ model DanceClass {
|
||||
name String
|
||||
description String?
|
||||
branchId String
|
||||
branch Branch @relation(fields: [branchId], references: [id])
|
||||
instructorId String?
|
||||
instructor Instructor? @relation(fields: [instructorId], references: [id])
|
||||
lessons Lesson[]
|
||||
fees Fee[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
instructor Instructor? @relation(fields: [instructorId], references: [id])
|
||||
branch Branch @relation(fields: [branchId], references: [id])
|
||||
fees Fee[]
|
||||
lessons Lesson[]
|
||||
}
|
||||
|
||||
model Lesson {
|
||||
id String @id @default(uuid())
|
||||
name String? // Optional name e.g. "Special Workshop"
|
||||
name String?
|
||||
startTime DateTime
|
||||
endTime DateTime
|
||||
type String // GROUP, PRIVATE
|
||||
type String
|
||||
branchId String
|
||||
branch Branch @relation(fields: [branchId], references: [id])
|
||||
instructorId String
|
||||
instructor Instructor @relation(fields: [instructorId], references: [id])
|
||||
classId String?
|
||||
class DanceClass? @relation(fields: [classId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
class DanceClass? @relation(fields: [classId], references: [id])
|
||||
instructor Instructor @relation(fields: [instructorId], references: [id])
|
||||
branch Branch @relation(fields: [branchId], references: [id])
|
||||
}
|
||||
|
||||
model Fee {
|
||||
@@ -74,11 +74,11 @@ model Fee {
|
||||
name String
|
||||
amount Float
|
||||
currency String @default("TRY")
|
||||
type String // MONTHLY, PER_LESSON, PACKAGE
|
||||
type String
|
||||
classId String?
|
||||
class DanceClass? @relation(fields: [classId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
class DanceClass? @relation(fields: [classId], references: [id])
|
||||
}
|
||||
|
||||
model Student {
|
||||
@@ -88,7 +88,7 @@ model Student {
|
||||
phone String
|
||||
birthDate DateTime
|
||||
branchId String?
|
||||
branch Branch? @relation(fields: [branchId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
branch Branch? @relation(fields: [branchId], references: [id])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
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[] }) {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -10,12 +11,10 @@ export function RegisterForm({ branches }: { branches: any[] }) {
|
||||
birthDate: '',
|
||||
branchId: ''
|
||||
});
|
||||
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('idle');
|
||||
|
||||
const res = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData),
|
||||
@@ -23,10 +22,8 @@ export function RegisterForm({ branches }: { branches: any[] }) {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setStatus('success');
|
||||
setFormData({ name: '', email: '', phone: '', birthDate: '', branchId: '' });
|
||||
} else {
|
||||
setStatus('error');
|
||||
router.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,41 +32,38 @@ export function RegisterForm({ branches }: { branches: any[] }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={styles.card}>
|
||||
{status === 'success' && <div className={styles.success}>Kayıt başarıyla oluşturuldu!</div>}
|
||||
{status === 'error' && <div className={styles.error}>Bir hata oluştu. Lütfen tekrar deneyin.</div>}
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Ad Soyad</label>
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<div className={styles.inputGroup}>
|
||||
<label>Ad Soyad</label>
|
||||
<input name="name" value={formData.name} onChange={handleChange} className={styles.input} required placeholder="Adınız" />
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Email (İsteğe bağlı)</label>
|
||||
<div className={styles.inputGroup}>
|
||||
<label>Email</label>
|
||||
<input name="email" type="email" value={formData.email} onChange={handleChange} className={styles.input} placeholder="ornek@email.com" />
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Telefon</label>
|
||||
<div className={styles.inputGroup}>
|
||||
<label>Telefon</label>
|
||||
<input name="phone" type="tel" value={formData.phone} onChange={handleChange} className={styles.input} required placeholder="5XX XXX XX XX" />
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Doğum Tarihi</label>
|
||||
<div className={styles.inputGroup}>
|
||||
<label>Doğum Tarihi</label>
|
||||
<input name="birthDate" type="date" value={formData.birthDate} onChange={handleChange} className={styles.input} required />
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.label}>Tercih Edilen Şube</label>
|
||||
<select name="branchId" value={formData.branchId} onChange={handleChange} className={styles.select}>
|
||||
<option value="">Seçiniz (Opsiyonel)</option>
|
||||
<div className={styles.inputGroup}>
|
||||
<label>Şube</label>
|
||||
<select name="branchId" value={formData.branchId} onChange={handleChange} className={styles.input}>
|
||||
<option value="">Seçiniz</option>
|
||||
{branches.map(b => (
|
||||
<option key={b.id} value={b.id}>{b.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.button}>Kayıt Ol</button>
|
||||
<button type="submit" className={styles.btn}>Kaydet</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
134
src/app/admin/register/StudentList.tsx
Normal file
134
src/app/admin/register/StudentList.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,27 @@
|
||||
import { prisma } from '@/infrastructure/db/prisma';
|
||||
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() {
|
||||
const branches = await prisma.branch.findMany({
|
||||
select: { id: true, name: true }
|
||||
});
|
||||
export default async function Page() {
|
||||
const [branches, students] = await Promise.all([
|
||||
prisma.branch.findMany({ select: { id: true, name: true } }),
|
||||
prisma.student.findMany({
|
||||
include: { branch: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1 className={styles.title}>Yeni Öğrenci Kaydı</h1>
|
||||
<h1>Öğrenci Kayıt İşlemleri</h1>
|
||||
</div>
|
||||
<RegisterForm branches={branches} />
|
||||
<div className={styles.header} style={{ marginTop: '2rem' }}>
|
||||
<h2>Kayıtlı Öğrenciler</h2>
|
||||
</div>
|
||||
<StudentList students={students} branches={branches} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import { prisma } from '@/infrastructure/db/prisma';
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
await prisma.branch.delete({
|
||||
where: { id: params.id },
|
||||
where: { id },
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
@@ -17,12 +18,13 @@ export async function DELETE(
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const json = await request.json();
|
||||
const branch = await prisma.branch.update({
|
||||
where: { id: params.id },
|
||||
where: { id },
|
||||
data: json,
|
||||
});
|
||||
return NextResponse.json(branch);
|
||||
|
||||
@@ -3,11 +3,12 @@ import { prisma } from '@/infrastructure/db/prisma';
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
await prisma.danceClass.delete({
|
||||
where: { id: params.id },
|
||||
where: { id },
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,11 +3,12 @@ import { prisma } from '@/infrastructure/db/prisma';
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
await prisma.instructor.delete({
|
||||
where: { id: params.id },
|
||||
where: { id },
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
42
src/app/api/students/[id]/route.ts
Normal file
42
src/app/api/students/[id]/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,13 @@ import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient();
|
||||
const prismaInstance = 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;
|
||||
|
||||
Reference in New Issue
Block a user